Repository: joel1st/championweb Branch: master Commit: f98e5d775bc9 Files: 97 Total size: 2.4 MB Directory structure: gitextract_n_uzk69e/ ├── .gitignore ├── Procfile ├── README.md ├── api_data/ │ ├── champions.json │ ├── dd_patch.json │ ├── index.js │ ├── items.json │ ├── masteries.json │ ├── runes.js │ ├── skills.json │ └── summoners.json ├── app.js ├── bash.txt ├── bin/ │ ├── update_server.sh │ └── www.js ├── config/ │ └── config.js ├── db/ │ └── championgg/ │ ├── webchampionpages.bson │ ├── webchampionpages.metadata.json │ ├── webchampionroles.bson │ ├── webchampionroles.metadata.json │ ├── webhomepagesummaries.bson │ ├── webhomepagesummaries.metadata.json │ ├── webmatchuppages.bson │ ├── webmatchuppages.metadata.json │ ├── weboverallroledatas.bson │ ├── weboverallroledatas.metadata.json │ ├── weboverallstats.bson │ ├── weboverallstats.metadata.json │ ├── webstatisticspages.bson │ └── webstatisticspages.metadata.json ├── db.js ├── gruntfile.js ├── headline.js ├── logic/ │ ├── lower_case_champ.js │ ├── produce_error.js │ └── role_hash_table.js ├── middleware/ │ └── overall_data.js ├── models/ │ ├── web_champion_page.js │ ├── web_champion_roles.js │ ├── web_home_page_summaries.js │ ├── web_matchup_page.js │ ├── web_overall_role_data.js │ ├── web_overall_stats.js │ └── web_statistics_page.js ├── package.json ├── public/ │ ├── cpmstar/ │ │ └── cpmstar_siteskin_iframebuster.html │ ├── css/ │ │ ├── master.css │ │ └── sprite.css │ ├── dist/ │ │ └── js/ │ │ ├── angular-bootstrap.js │ │ ├── angular.js │ │ ├── chart.js │ │ ├── dirDisqus.js │ │ └── tc-angular-chartjs.js │ ├── googled8153283379da1fb.html │ ├── js/ │ │ ├── app.js │ │ ├── champion_data.js │ │ ├── champion_page.js │ │ ├── championgg_tooltip.js │ │ ├── chart_options.js │ │ ├── matchup_page.js │ │ ├── statistics_jquery.js │ │ └── statistics_page.js │ ├── opensearchdescription.xml │ ├── riot.html │ └── template/ │ └── typeahead/ │ └── typeahead-popup.html ├── routes/ │ ├── api_static.js │ ├── champion.js │ ├── faq.js │ ├── index.js │ ├── matchup.js │ ├── matchup_json.js │ └── statistics.js ├── update_data.sh ├── update_server.sh └── views/ ├── champion/ │ ├── advertisement.ejs │ ├── champion_image_roles.ejs │ ├── champion_statistics.ejs │ ├── core_build.ejs │ ├── counters_matchups.ejs │ ├── first_items.ejs │ ├── gamelength_experience_summoners.ejs │ ├── masteries.ejs │ ├── reddit.ejs │ ├── runes.ejs │ ├── skill_order.ejs │ ├── viktor_upgrade.ejs │ └── winrate_playrate_damage_advert_trinket.ejs ├── champion.ejs ├── error.ejs ├── faq.ejs ├── footer.ejs ├── header.ejs ├── index.ejs ├── matchup.ejs ├── new_champion.ejs ├── scripts.ejs └── statistics.ejs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ### Node ### # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules ### Intellij ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm *.iml ## Directory-based project format: .idea/ # if you remove the above rule, at least ignore the following: # User-specific stuff: # .idea/workspace.xml # .idea/tasks.xml # .idea/dictionaries # Sensitive or high-churn files: # .idea/dataSources.ids # .idea/dataSources.xml # .idea/sqlDataSources.xml # .idea/dynamic.xml # .idea/uiDesigner.xml # Gradle: # .idea/gradle.xml # .idea/libraries # Mongo Explorer plugin: # .idea/mongoSettings.xml ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties ### SublimeText ### # cache files for sublime text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # workspace files are user-specific *.sublime-workspace # project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using SublimeText # *.sublime-project # sftp configuration file sftp-config.json ================================================ FILE: Procfile ================================================ worker: node worker.js web: npm start clock: node clock.js ================================================ FILE: README.md ================================================ champion.gg ======== WEBSITE: http://champion.gg A MEAN project (with a dash of angular). In order to get a local version of champion.gg running you need to have MongoDb, Node and NPM installed. (ensure MongoDB is running when trying to run champion.gg) To get a working version set up you'll need to clone the repo, install the dependencies, build the database and then start the server from the command line. The commands to enter are listed below. # Getting set up Clone champion.gg: ```sh git clone https://github.com/joel1st/championweb.git ``` Install dependencies from project directory: ```sh npm install ``` Restore database from project directory ```sh mongorestore --db championgg --collection webchampionpages --drop db/championgg/webchampionpages.bson mongorestore --db championgg --collection webchampionroles --drop db/championgg/webchampionroles.bson mongorestore --db championgg --collection webmatchuppages --drop db/championgg/webmatchuppages.bson mongorestore --db championgg --collection weboverallroledatas --drop db/championgg/weboverallroledatas.bson mongorestore --db championgg --collection weboverallstats --drop db/championgg/weboverallstats.bson mongorestore --db championgg --collection webhomepagesummaries --drop db/championgg/webhomepagesummaries.bson mongorestore --db championgg --collection webstatisticspages --drop db/championgg/webstatisticspages.bson ``` Start Champion.gg ```sh npm start #if you have another web server running on port 80 you can set the port as such PORT=8888 npm start ``` You can now access champion.gg on http://localhost/ or if you set a port number http://localhost:8888/ # Development In order to work on champion.gg more effectively I've created a grunt tasks to facilitate automation of javascript hinting (helps avoid nasty javascript errors). ```sh grunt watch ``` To get assets ready for production: ```sh grunt production ``` ================================================ FILE: api_data/champions.json ================================================ {"Aatrox":{"id":266,"key":"Aatrox","name":"Aatrox","title":"the Darkin Blade"},"Ahri":{"id":103,"key":"Ahri","name":"Ahri","title":"the Nine-Tailed Fox"},"Akali":{"id":84,"key":"Akali","name":"Akali","title":"the Fist of Shadow"},"Alistar":{"id":12,"key":"Alistar","name":"Alistar","title":"the Minotaur"},"Amumu":{"id":32,"key":"Amumu","name":"Amumu","title":"the Sad Mummy"},"Anivia":{"id":34,"key":"Anivia","name":"Anivia","title":"the Cryophoenix"},"Annie":{"id":1,"key":"Annie","name":"Annie","title":"the Dark Child"},"Ashe":{"id":22,"key":"Ashe","name":"Ashe","title":"the Frost Archer"},"AurelionSol":{"id":136,"key":"AurelionSol","name":"Aurelion Sol","title":"The Star Forger"},"Azir":{"id":268,"key":"Azir","name":"Azir","title":"the Emperor of the Sands"},"Bard":{"id":432,"key":"Bard","name":"Bard","title":"the Wandering Caretaker"},"Blitzcrank":{"id":53,"key":"Blitzcrank","name":"Blitzcrank","title":"the Great Steam Golem"},"Brand":{"id":63,"key":"Brand","name":"Brand","title":"the Burning Vengeance"},"Braum":{"id":201,"key":"Braum","name":"Braum","title":"the Heart of the Freljord"},"Caitlyn":{"id":51,"key":"Caitlyn","name":"Caitlyn","title":"the Sheriff of Piltover"},"Camille":{"id":164,"key":"Camille","name":"Camille","title":"the Steel Shadow"},"Cassiopeia":{"id":69,"key":"Cassiopeia","name":"Cassiopeia","title":"the Serpent's Embrace"},"Chogath":{"id":31,"key":"Chogath","name":"Cho'Gath","title":"the Terror of the Void"},"Corki":{"id":42,"key":"Corki","name":"Corki","title":"the Daring Bombardier"},"Darius":{"id":122,"key":"Darius","name":"Darius","title":"the Hand of Noxus"},"Diana":{"id":131,"key":"Diana","name":"Diana","title":"Scorn of the Moon"},"Draven":{"id":119,"key":"Draven","name":"Draven","title":"the Glorious Executioner"},"DrMundo":{"id":36,"key":"DrMundo","name":"Dr. Mundo","title":"the Madman of Zaun"},"Ekko":{"id":245,"key":"Ekko","name":"Ekko","title":"the Boy Who Shattered Time"},"Elise":{"id":60,"key":"Elise","name":"Elise","title":"the Spider Queen"},"Evelynn":{"id":28,"key":"Evelynn","name":"Evelynn","title":"the Widowmaker"},"Ezreal":{"id":81,"key":"Ezreal","name":"Ezreal","title":"the Prodigal Explorer"},"Fiddlesticks":{"id":9,"key":"Fiddlesticks","name":"Fiddlesticks","title":"the Harbinger of Doom"},"Fiora":{"id":114,"key":"Fiora","name":"Fiora","title":"the Grand Duelist"},"Fizz":{"id":105,"key":"Fizz","name":"Fizz","title":"the Tidal Trickster"},"Galio":{"id":3,"key":"Galio","name":"Galio","title":"the Colossus"},"Gangplank":{"id":41,"key":"Gangplank","name":"Gangplank","title":"the Saltwater Scourge"},"Garen":{"id":86,"key":"Garen","name":"Garen","title":"The Might of Demacia"},"Gnar":{"id":150,"key":"Gnar","name":"Gnar","title":"the Missing Link"},"Gragas":{"id":79,"key":"Gragas","name":"Gragas","title":"the Rabble Rouser"},"Graves":{"id":104,"key":"Graves","name":"Graves","title":"the Outlaw"},"Hecarim":{"id":120,"key":"Hecarim","name":"Hecarim","title":"the Shadow of War"},"Heimerdinger":{"id":74,"key":"Heimerdinger","name":"Heimerdinger","title":"the Revered Inventor"},"Illaoi":{"id":420,"key":"Illaoi","name":"Illaoi","title":"the Kraken Priestess"},"Irelia":{"id":39,"key":"Irelia","name":"Irelia","title":"the Will of the Blades"},"Ivern":{"id":427,"key":"Ivern","name":"Ivern","title":"the Green Father"},"Janna":{"id":40,"key":"Janna","name":"Janna","title":"the Storm's Fury"},"JarvanIV":{"id":59,"key":"JarvanIV","name":"Jarvan IV","title":"the Exemplar of Demacia"},"Jax":{"id":24,"key":"Jax","name":"Jax","title":"Grandmaster at Arms"},"Jayce":{"id":126,"key":"Jayce","name":"Jayce","title":"the Defender of Tomorrow"},"Jhin":{"id":202,"key":"Jhin","name":"Jhin","title":"the Virtuoso"},"Jinx":{"id":222,"key":"Jinx","name":"Jinx","title":"the Loose Cannon"},"Kalista":{"id":429,"key":"Kalista","name":"Kalista","title":"the Spear of Vengeance"},"Karma":{"id":43,"key":"Karma","name":"Karma","title":"the Enlightened One"},"Karthus":{"id":30,"key":"Karthus","name":"Karthus","title":"the Deathsinger"},"Kassadin":{"id":38,"key":"Kassadin","name":"Kassadin","title":"the Void Walker"},"Katarina":{"id":55,"key":"Katarina","name":"Katarina","title":"the Sinister Blade"},"Kayle":{"id":10,"key":"Kayle","name":"Kayle","title":"The Judicator"},"Kennen":{"id":85,"key":"Kennen","name":"Kennen","title":"the Heart of the Tempest"},"Khazix":{"id":121,"key":"Khazix","name":"Kha'Zix","title":"the Voidreaver"},"Kindred":{"id":203,"key":"Kindred","name":"Kindred","title":"The Eternal Hunters"},"Kled":{"id":240,"key":"Kled","name":"Kled","title":"the Cantankerous Cavalier"},"KogMaw":{"id":96,"key":"KogMaw","name":"Kog'Maw","title":"the Mouth of the Abyss"},"Leblanc":{"id":7,"key":"Leblanc","name":"LeBlanc","title":"the Deceiver"},"LeeSin":{"id":64,"key":"LeeSin","name":"Lee Sin","title":"the Blind Monk"},"Leona":{"id":89,"key":"Leona","name":"Leona","title":"the Radiant Dawn"},"Lissandra":{"id":127,"key":"Lissandra","name":"Lissandra","title":"the Ice Witch"},"Lucian":{"id":236,"key":"Lucian","name":"Lucian","title":"the Purifier"},"Lulu":{"id":117,"key":"Lulu","name":"Lulu","title":"the Fae Sorceress"},"Lux":{"id":99,"key":"Lux","name":"Lux","title":"the Lady of Luminosity"},"Malphite":{"id":54,"key":"Malphite","name":"Malphite","title":"Shard of the Monolith"},"Malzahar":{"id":90,"key":"Malzahar","name":"Malzahar","title":"the Prophet of the Void"},"Maokai":{"id":57,"key":"Maokai","name":"Maokai","title":"the Twisted Treant"},"MasterYi":{"id":11,"key":"MasterYi","name":"Master Yi","title":"the Wuju Bladesman"},"MissFortune":{"id":21,"key":"MissFortune","name":"Miss Fortune","title":"the Bounty Hunter"},"MonkeyKing":{"id":62,"key":"MonkeyKing","name":"Wukong","title":"the Monkey King"},"Mordekaiser":{"id":82,"key":"Mordekaiser","name":"Mordekaiser","title":"the Iron Revenant"},"Morgana":{"id":25,"key":"Morgana","name":"Morgana","title":"Fallen Angel"},"Nami":{"id":267,"key":"Nami","name":"Nami","title":"the Tidecaller"},"Nasus":{"id":75,"key":"Nasus","name":"Nasus","title":"the Curator of the Sands"},"Nautilus":{"id":111,"key":"Nautilus","name":"Nautilus","title":"the Titan of the Depths"},"Nidalee":{"id":76,"key":"Nidalee","name":"Nidalee","title":"the Bestial Huntress"},"Nocturne":{"id":56,"key":"Nocturne","name":"Nocturne","title":"the Eternal Nightmare"},"Nunu":{"id":20,"key":"Nunu","name":"Nunu","title":"the Yeti Rider"},"Olaf":{"id":2,"key":"Olaf","name":"Olaf","title":"the Berserker"},"Orianna":{"id":61,"key":"Orianna","name":"Orianna","title":"the Lady of Clockwork"},"Pantheon":{"id":80,"key":"Pantheon","name":"Pantheon","title":"the Artisan of War"},"Poppy":{"id":78,"key":"Poppy","name":"Poppy","title":"Keeper of the Hammer"},"Quinn":{"id":133,"key":"Quinn","name":"Quinn","title":"Demacia's Wings"},"Rakan":{"id":497,"key":"Rakan","name":"Rakan","title":"The Charmer"},"Rammus":{"id":33,"key":"Rammus","name":"Rammus","title":"the Armordillo"},"RekSai":{"id":421,"key":"RekSai","name":"Rek'Sai","title":"the Void Burrower"},"Renekton":{"id":58,"key":"Renekton","name":"Renekton","title":"the Butcher of the Sands"},"Rengar":{"id":107,"key":"Rengar","name":"Rengar","title":"the Pridestalker"},"Riven":{"id":92,"key":"Riven","name":"Riven","title":"the Exile"},"Rumble":{"id":68,"key":"Rumble","name":"Rumble","title":"the Mechanized Menace"},"Ryze":{"id":13,"key":"Ryze","name":"Ryze","title":"the Rune Mage"},"Sejuani":{"id":113,"key":"Sejuani","name":"Sejuani","title":"Fury of the North"},"Shaco":{"id":35,"key":"Shaco","name":"Shaco","title":"the Demon Jester"},"Shen":{"id":98,"key":"Shen","name":"Shen","title":"the Eye of Twilight"},"Shyvana":{"id":102,"key":"Shyvana","name":"Shyvana","title":"the Half-Dragon"},"Singed":{"id":27,"key":"Singed","name":"Singed","title":"the Mad Chemist"},"Sion":{"id":14,"key":"Sion","name":"Sion","title":"The Undead Juggernaut"},"Sivir":{"id":15,"key":"Sivir","name":"Sivir","title":"the Battle Mistress"},"Skarner":{"id":72,"key":"Skarner","name":"Skarner","title":"the Crystal Vanguard"},"Sona":{"id":37,"key":"Sona","name":"Sona","title":"Maven of the Strings"},"Soraka":{"id":16,"key":"Soraka","name":"Soraka","title":"the Starchild"},"Swain":{"id":50,"key":"Swain","name":"Swain","title":"the Master Tactician"},"Syndra":{"id":134,"key":"Syndra","name":"Syndra","title":"the Dark Sovereign"},"TahmKench":{"id":223,"key":"TahmKench","name":"Tahm Kench","title":"the River King"},"Taliyah":{"id":163,"key":"Taliyah","name":"Taliyah","title":"the Stoneweaver"},"Talon":{"id":91,"key":"Talon","name":"Talon","title":"the Blade's Shadow"},"Taric":{"id":44,"key":"Taric","name":"Taric","title":"the Shield of Valoran"},"Teemo":{"id":17,"key":"Teemo","name":"Teemo","title":"the Swift Scout"},"Thresh":{"id":412,"key":"Thresh","name":"Thresh","title":"the Chain Warden"},"Tristana":{"id":18,"key":"Tristana","name":"Tristana","title":"the Yordle Gunner"},"Trundle":{"id":48,"key":"Trundle","name":"Trundle","title":"the Troll King"},"Tryndamere":{"id":23,"key":"Tryndamere","name":"Tryndamere","title":"the Barbarian King"},"TwistedFate":{"id":4,"key":"TwistedFate","name":"Twisted Fate","title":"the Card Master"},"Twitch":{"id":29,"key":"Twitch","name":"Twitch","title":"the Plague Rat"},"Udyr":{"id":77,"key":"Udyr","name":"Udyr","title":"the Spirit Walker"},"Urgot":{"id":6,"key":"Urgot","name":"Urgot","title":"the Headsman's Pride"},"Varus":{"id":110,"key":"Varus","name":"Varus","title":"the Arrow of Retribution"},"Vayne":{"id":67,"key":"Vayne","name":"Vayne","title":"the Night Hunter"},"Veigar":{"id":45,"key":"Veigar","name":"Veigar","title":"the Tiny Master of Evil"},"Velkoz":{"id":161,"key":"Velkoz","name":"Vel'Koz","title":"the Eye of the Void"},"Vi":{"id":254,"key":"Vi","name":"Vi","title":"the Piltover Enforcer"},"Viktor":{"id":112,"key":"Viktor","name":"Viktor","title":"the Machine Herald"},"Vladimir":{"id":8,"key":"Vladimir","name":"Vladimir","title":"the Crimson Reaper"},"Volibear":{"id":106,"key":"Volibear","name":"Volibear","title":"the Thunder's Roar"},"Warwick":{"id":19,"key":"Warwick","name":"Warwick","title":"the Uncaged Wrath of Zaun"},"Xayah":{"id":498,"key":"Xayah","name":"Xayah","title":"the Rebel"},"Xerath":{"id":101,"key":"Xerath","name":"Xerath","title":"the Magus Ascendant"},"XinZhao":{"id":5,"key":"XinZhao","name":"Xin Zhao","title":"the Seneschal of Demacia"},"Yasuo":{"id":157,"key":"Yasuo","name":"Yasuo","title":"the Unforgiven"},"Yorick":{"id":83,"key":"Yorick","name":"Yorick","title":"Shepherd of Souls"},"Zac":{"id":154,"key":"Zac","name":"Zac","title":"the Secret Weapon"},"Zed":{"id":238,"key":"Zed","name":"Zed","title":"the Master of Shadows"},"Ziggs":{"id":115,"key":"Ziggs","name":"Ziggs","title":"the Hexplosives Expert"},"Zilean":{"id":26,"key":"Zilean","name":"Zilean","title":"the Chronokeeper"},"Zyra":{"id":143,"key":"Zyra","name":"Zyra","title":"Rise of the Thorns"}} ================================================ FILE: api_data/dd_patch.json ================================================ {"ddPatch":"7.11.1"} ================================================ FILE: api_data/index.js ================================================ var items = require('./items'); var masteries = require('./masteries.json'); var runes = require('./runes'); var skills = require('./skills'); var summoners = require('./summoners'); module.exports = { items: items, masteries: masteries, runes: runes, skills: skills, summoners: summoners }; ================================================ FILE: api_data/items.json ================================================ {"1001":{"name":"Boots of Speed","description":"Limited to 1.

UNIQUE Passive - Enhanced Movement: +25 Movement Speed","colloq":";","plaintext":"Slightly increases Movement Speed","into":["3006","3047","3020","3158","3111","3117","3009"],"image":{"full":"1001.png","sprite":"item0.png","group":"item","x":0,"y":0,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":300,"sell":210},"tags":["Boots"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":25},"id":1001},"1004":{"name":"Faerie Charm","description":"+25% Base Mana Regen ","colloq":";","plaintext":"Slightly increases Mana Regen","into":["3028","3070","3073","3114","3098"],"image":{"full":"1004.png","sprite":"item0.png","group":"item","x":48,"y":0,"w":48,"h":48},"gold":{"base":125,"purchasable":true,"total":125,"sell":88},"tags":["ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":1004},"1006":{"name":"Rejuvenation Bead","description":"+50% Base Health Regen ","colloq":";","plaintext":"Slightly increases Health Regen","into":["3077","3097","2053","3801","3096","3194"],"image":{"full":"1006.png","sprite":"item0.png","group":"item","x":96,"y":0,"w":48,"h":48},"gold":{"base":150,"purchasable":true,"total":150,"sell":105},"tags":["HealthRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":1006},"1011":{"name":"Giant's Belt","description":"+380 Health","colloq":";","plaintext":"Greatly increases Health","from":["1028"],"into":["3083","3143","3084","3022","3742"],"image":{"full":"1011.png","sprite":"item0.png","group":"item","x":144,"y":0,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":1000,"sell":700},"tags":["Health"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":380},"depth":2,"id":1011},"1018":{"name":"Cloak of Agility","description":"+20% Critical Strike Chance","colloq":";","plaintext":"Increases critical strike chance","into":["3031","3185","3508"],"image":{"full":"1018.png","sprite":"item0.png","group":"item","x":192,"y":0,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":800,"sell":560},"tags":["CriticalStrike"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.2},"id":1018},"1026":{"name":"Blasting Wand","description":"+40 Ability Power","colloq":";","plaintext":"Moderately increases Ability Power","into":["3089","3135","3124","3029","3151","3027","3100","3102","3116"],"image":{"full":"1026.png","sprite":"item0.png","group":"item","x":240,"y":0,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":850,"sell":595},"tags":["SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":40},"id":1026},"1027":{"name":"Sapphire Crystal","description":"+250 Mana","colloq":";blue","plaintext":"Increases Mana","into":["3057","3070","3010","3024","3073","3802"],"image":{"full":"1027.png","sprite":"item0.png","group":"item","x":288,"y":0,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":245},"tags":["Mana"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250},"id":1027},"1028":{"name":"Ruby Crystal","description":"+150 Health","colloq":";red","plaintext":"Increases Health","into":["1011","3211","3136","2045","2049","3010","3801","3044","3052","3067","3116","3143","3748","3751"],"image":{"full":"1028.png","sprite":"item0.png","group":"item","x":336,"y":0,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":400,"sell":280},"tags":["Health"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":150},"id":1028},"1029":{"name":"Cloth Armor","description":"+15 Armor","colloq":";","plaintext":"Slightly increases Armor","into":["3047","1031","3191","3024","3082","3075","2053","3105","3026"],"image":{"full":"1029.png","sprite":"item0.png","group":"item","x":384,"y":0,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":300,"sell":210},"tags":["Armor"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":15},"id":1029},"1031":{"name":"Chain Vest","description":"+40 Armor","colloq":";","plaintext":"Greatly increases Armor","from":["1029"],"into":["3075","3068","3109","2053","3193","3742"],"image":{"full":"1031.png","sprite":"item0.png","group":"item","x":432,"y":0,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":800,"sell":560},"tags":["Armor"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":40},"depth":2,"id":1031},"1033":{"name":"Null-Magic Mantle","description":"+25 Magic Resist","colloq":";","plaintext":"Slightly increases Magic Resist","into":["3111","3211","1057","3028","3140","3155","3105","3102","3814","3190","3194"],"image":{"full":"1033.png","sprite":"item0.png","group":"item","x":0,"y":48,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":450,"sell":315},"tags":["SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":25},"id":1033},"1036":{"name":"Long Sword","description":"+10 Attack Damage","colloq":";","plaintext":"Slightly increases Attack Damage","into":["3134","3077","3123","1053","3133","3034","3035","3044","3052","3053","3072","3122","3144","3155","3252"],"image":{"full":"1036.png","sprite":"item0.png","group":"item","x":48,"y":48,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":245},"tags":["Damage","Lane"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":10},"id":1036},"1037":{"name":"Pickaxe","description":"+25 Attack Damage","colloq":";","plaintext":"Moderately increases Attack Damage","into":["3124","3004","3008","3031","3074","3814","3812","3139","3181"],"image":{"full":"1037.png","sprite":"item0.png","group":"item","x":96,"y":48,"w":48,"h":48},"gold":{"base":875,"purchasable":true,"total":875,"sell":613},"tags":["Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25},"id":1037},"1038":{"name":"B. F. Sword","description":"+40 Attack Damage","colloq":";bf","plaintext":"Greatly increases Attack Damage","into":["3026","3031","3072","3147","3508"],"image":{"full":"1038.png","sprite":"item0.png","group":"item","x":144,"y":48,"w":48,"h":48},"gold":{"base":1300,"purchasable":true,"total":1300,"sell":910},"tags":["Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":40},"id":1038},"1039":{"name":"Hunter's Talisman","description":"+150% Base Mana Regen while in Jungle

UNIQUE Passive - Tooth: Damaging a monster with a spell or attack steals 25 Health over 5 seconds. Killing monsters grants special bonus experience.","colloq":";jungle;Jungle","plaintext":"Provides damage against Monsters and Mana Regen in the Jungle","into":["3706","3711","3715"],"image":{"full":"1039.png","sprite":"item0.png","group":"item","x":192,"y":48,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":245},"tags":["LifeSteal","ManaRegen","OnHit","Jungle"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"5","Effect5Amount":"0","Effect6Amount":"1.5","Effect7Amount":"15"},"id":1039},"1041":{"name":"Hunter's Machete","description":"+10% Life Steal vs. Monsters

UNIQUE Passive - Nail: Basic attacks deal 25 bonus damage on hit vs. Monsters. Killing monsters grants special bonus experience.","colloq":";jungle;Jungle","plaintext":"Provides damage and life steal versus Monsters","into":["3706","3711","3715"],"image":{"full":"1041.png","sprite":"item0.png","group":"item","x":240,"y":48,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":245},"tags":["LifeSteal","OnHit","Jungle"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"12","Effect2Amount":"25","Effect3Amount":"0.1","Effect4Amount":"2","Effect5Amount":"0","Effect6Amount":"0.1","Effect7Amount":"15"},"id":1041},"1042":{"name":"Dagger","description":"+12% Attack Speed","colloq":";","plaintext":"Slightly increases Attack Speed","into":["1043","3091","3006","3085","2015","3046","3086","3101"],"image":{"full":"1042.png","sprite":"item0.png","group":"item","x":288,"y":48,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":300,"sell":210},"tags":["AttackSpeed"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.12},"id":1042},"1043":{"name":"Recurve Bow","description":"+25% Attack Speed

UNIQUE Passive: Basic attacks deal an additional 15 physical damage on hit.","colloq":";","plaintext":"Greatly increases Attack Speed","from":["1042","1042"],"into":["3091","3153","3124","3675","1416","1418","1419"],"image":{"full":"1043.png","sprite":"item0.png","group":"item","x":336,"y":48,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":1000,"sell":700},"tags":["AttackSpeed","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.25},"effect":{"Effect1Amount":"15"},"depth":2,"id":1043},"1051":{"name":"Brawler's Gloves","description":"+10% Critical Strike Chance","colloq":";","plaintext":"Slightly increases Critical Strike Chance","into":["3086","3122"],"image":{"full":"1051.png","sprite":"item0.png","group":"item","x":384,"y":48,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":400,"sell":280},"tags":["CriticalStrike"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.1},"id":1051},"1052":{"name":"Amplifying Tome","description":"+20 Ability Power","colloq":";amptome","plaintext":"Slightly increases Ability Power","into":["3108","3191","3136","3135","3145","3113","3090","3116","1402","1410","1414","3050","3089","3165","3673","3802"],"image":{"full":"1052.png","sprite":"item0.png","group":"item","x":432,"y":48,"w":48,"h":48},"gold":{"base":435,"purchasable":true,"total":435,"sell":305},"tags":["SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":20},"id":1052},"1053":{"name":"Vampiric Scepter","description":"+15 Attack Damage
+10% Life Steal
","colloq":";","plaintext":"Basic attacks restore Health","from":["1036"],"into":["3072","3074","3812","3139","3144","3181"],"image":{"full":"1053.png","sprite":"item0.png","group":"item","x":0,"y":96,"w":48,"h":48},"gold":{"base":550,"purchasable":true,"total":900,"sell":630},"tags":["Damage","LifeSteal"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":15,"PercentLifeStealMod":0.1},"depth":2,"id":1053},"1054":{"name":"Doran's Shield","description":"+80 Health

Passive: Restores 6 Health every 5 seconds.
Passive: Basic attacks deal an additional 5 physical damage to minions on hit.
UNIQUE Passive: Regain an additional 20 health over 10 seconds after taking damage from an enemy champion.","colloq":";dshield","plaintext":"Good defensive starting item","image":{"full":"1054.png","sprite":"item0.png","group":"item","x":48,"y":96,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":400,"sell":160},"tags":["Health","HealthRegen","Lane"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":80,"FlatHPRegenMod":1.2},"effect":{"Effect1Amount":"0","Effect2Amount":"10","Effect3Amount":"5","Effect4Amount":"2"},"id":1054},"1055":{"name":"Doran's Blade","description":"+8 Attack Damage
+80 Health
+3% Life Steal
","colloq":";dblade","plaintext":"Good starting item for attackers","image":{"full":"1055.png","sprite":"item0.png","group":"item","x":96,"y":96,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":450,"sell":180},"tags":["Damage","Health","Lane","LifeSteal"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":8,"FlatHPPoolMod":80,"PercentLifeStealMod":0.03},"effect":{"Effect1Amount":"10"},"id":1055},"1056":{"name":"Doran's Ring","description":"+60 Health
+15 Ability Power
+50% Base Mana Regen


Passive: Restores 4 Mana upon killing a unit.","colloq":";dring","plaintext":"Good starting item for casters","image":{"full":"1056.png","sprite":"item0.png","group":"item","x":144,"y":96,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":400,"sell":160},"tags":["Health","Lane","ManaRegen","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":60,"FlatMagicDamageMod":15},"effect":{"Effect1Amount":"4"},"id":1056},"1057":{"name":"Negatron Cloak","description":"+40 Magic Resist","colloq":";","plaintext":"Moderately increases Magic Resist","from":["1033"],"into":["3170","3091","3512","3001","3193"],"image":{"full":"1057.png","sprite":"item0.png","group":"item","x":192,"y":96,"w":48,"h":48},"gold":{"base":270,"purchasable":true,"total":720,"sell":504},"tags":["SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":40},"depth":2,"id":1057},"1058":{"name":"Needlessly Large Rod","description":"+60 Ability Power","colloq":";nlr","plaintext":"Greatly increases Ability Power","into":["3089","3090","3003","3007","3285"],"image":{"full":"1058.png","sprite":"item0.png","group":"item","x":240,"y":96,"w":48,"h":48},"gold":{"base":1250,"purchasable":true,"total":1250,"sell":875},"tags":["SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":60},"id":1058},"1082":{"name":"The Dark Seal","description":"+15 Ability Power
+25% Increased Healing from Potions
+100 Mana


UNIQUE Passive - Dread: Grants +3 Ability Power per Glory.
UNIQUE Passive - Do or Die: Grants 2 Glory for a champion kill or 1 Glory for an assist, up to 10 Glory total. Lose 4 Glory on death.","colloq":";Noxian","plaintext":"Provides Ability Power and Mana. Increases in power as you kill enemies.","into":["3041"],"image":{"full":"1082.png","sprite":"item0.png","group":"item","x":288,"y":96,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":245},"tags":["HealthRegen","SpellDamage","Mana","Lane"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatMPPoolMod":100,"FlatMagicDamageMod":15},"effect":{"Effect1Amount":"0.25","Effect2Amount":"2","Effect3Amount":"1","Effect4Amount":"10","Effect5Amount":"3","Effect6Amount":"4"},"id":1082},"1083":{"name":"Cull","description":"+7 Attack Damage
+3 Life on Hit


UNIQUE Passive: Killing a lane minion grants 1 additional Gold. Killing 100 lane minions grants an additional 350 bonus gold immediately and disables this passive.","colloq":";dblade","plaintext":"Provides damage and Life Steal on hit - Killing minions grant bonus Gold","image":{"full":"1083.png","sprite":"item0.png","group":"item","x":336,"y":96,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":450,"sell":180},"tags":["Damage","LifeSteal","Lane"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":7},"effect":{"Effect1Amount":"3","Effect2Amount":"1","Effect3Amount":"100","Effect4Amount":"350"},"id":1083},"1400":{"name":"Enchantment: Warrior","description":"+60 Attack Damage
+10% Cooldown Reduction
","colloq":"","plaintext":"Grants Attack Damage and Cooldown Reduction","from":["3133","3706"],"hideFromAll":true,"image":{"full":"1400.png","sprite":"item0.png","group":"item","x":384,"y":96,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"-0.2","Effect7Amount":"2","Effect8Amount":"3","Effect9Amount":"0.1"},"depth":3,"id":1400},"1401":{"name":"Enchantment: Cinderhulk","description":"+400 Health
+15% Bonus Health


UNIQUE Passive - Immolate: Deals 11 (+1 per champion level) magic damage a second to nearby enemies while in combat. Deals 200% bonus damage to minions and monsters. ","colloq":"","plaintext":"Grants Health and Immolate Aura","from":["3751","3706"],"hideFromAll":true,"image":{"full":"1401.png","sprite":"item0.png","group":"item","x":432,"y":96,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":400},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"-0.2","Effect7Amount":"2","Effect8Amount":"3","Effect9Amount":"0.1"},"depth":3,"id":1401},"1402":{"name":"Enchantment: Runic Echoes","description":"+60 Ability Power
+7% Movement Speed


UNIQUE Passive - Echo: Gain charges upon moving or casting. At 100 charges, the next damaging spell hit expends all charges to deal 60 (+10% of Ability Power) bonus magic damage to up to 4 targets on hit.

This effect deals 250% damage to Large Monsters. Hitting a Large Monster with this effect will restore 18% of your missing Mana.","colloq":"","plaintext":"Grants Ability Power and periodically empowers your Spells","from":["3113","1052","3706"],"hideFromAll":true,"image":{"full":"1402.png","sprite":"item0.png","group":"item","x":0,"y":144,"w":48,"h":48},"gold":{"base":340,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.07,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"-0.2","Effect7Amount":"2","Effect8Amount":"3","Effect9Amount":"0.1"},"depth":3,"id":1402},"1408":{"name":"Enchantment: Warrior","description":"+60 Attack Damage
+10% Cooldown Reduction
","colloq":"","plaintext":"Grants Attack Damage and Cooldown Reduction","from":["3133","3711"],"hideFromAll":true,"image":{"full":"1408.png","sprite":"item0.png","group":"item","x":48,"y":144,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"30","Effect9Amount":"0.1","Effect10Amount":"150"},"depth":3,"id":1408},"1409":{"name":"Enchantment: Cinderhulk","description":"+400 Health
+15% Bonus Health


UNIQUE Passive - Immolate: Deals 11 (+1 per champion level) magic damage a second to nearby enemies while in combat. Deals 200% bonus damage to minions and monsters. ","colloq":"","plaintext":"Grants Health and Immolate Aura","from":["3751","3711"],"hideFromAll":true,"image":{"full":"1409.png","sprite":"item0.png","group":"item","x":96,"y":144,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":400},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"30","Effect9Amount":"0.1","Effect10Amount":"150"},"depth":3,"id":1409},"1410":{"name":"Enchantment: Runic Echoes","description":"+60 Ability Power
+7% Movement Speed


UNIQUE Passive - Echo: Gain charges upon moving or casting. At 100 charges, the next damaging spell hit expends all charges to deal 60 (+10% of Ability Power) bonus magic damage to up to 4 targets on hit.

This effect deals 250% damage to Large Monsters. Hitting a Large Monster with this effect will restore 18% of your missing Mana.","colloq":"","plaintext":"Grants Ability Power and periodically empowers your Spells","from":["3113","1052","3711"],"hideFromAll":true,"image":{"full":"1410.png","sprite":"item0.png","group":"item","x":144,"y":144,"w":48,"h":48},"gold":{"base":340,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.07,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"30","Effect9Amount":"0.1","Effect10Amount":"150"},"depth":3,"id":1410},"1412":{"name":"Enchantment: Warrior","description":"+60 Attack Damage
+10% Cooldown Reduction
","colloq":"","plaintext":"Grants Attack Damage and Cooldown Reduction","from":["3133","3715"],"hideFromAll":true,"image":{"full":"1412.png","sprite":"item0.png","group":"item","x":192,"y":144,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"18","Effect9Amount":"0.1","Effect10Amount":"4"},"depth":3,"id":1412},"1413":{"name":"Enchantment: Cinderhulk","description":"+400 Health
+15% Bonus Health


UNIQUE Passive - Immolate: Deals 11 (+1 per champion level) magic damage a second to nearby enemies while in combat. Deals 200% bonus damage to minions and monsters. ","colloq":"","plaintext":"Grants Health and Immolate Aura","from":["3751","3715"],"hideFromAll":true,"image":{"full":"1413.png","sprite":"item0.png","group":"item","x":240,"y":144,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":400},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"18","Effect9Amount":"0.1","Effect10Amount":"4"},"depth":3,"id":1413},"1414":{"name":"Enchantment: Runic Echoes","description":"+60 Ability Power
+7% Movement Speed


UNIQUE Passive - Echo: Gain charges upon moving or casting. At 100 charges, the next damaging spell hit expends all charges to deal 60 (+10% of Ability Power) bonus magic damage to up to 4 targets on hit.

This effect deals 250% damage to Large Monsters. Hitting a Large Monster with this effect will restore 18% of your missing Mana.","colloq":"","plaintext":"Grants Ability Power and periodically empowers your Spells","from":["3113","1052","3715"],"hideFromAll":true,"image":{"full":"1414.png","sprite":"item0.png","group":"item","x":288,"y":144,"w":48,"h":48},"gold":{"base":340,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.07,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"18","Effect9Amount":"0.1","Effect10Amount":"4"},"depth":3,"id":1414},"1416":{"name":"Enchantment: Bloodrazor","description":"+50% Attack Speed

UNIQUE Passive: Basic attacks deal 4% of the target's maximum Health in bonus physical damage (max 75 vs. monsters and minions) on hit.","colloq":"","plaintext":"Increases Attack Speed and deals damage based on the target's Health","from":["1043","3706"],"hideFromAll":true,"image":{"full":"1416.png","sprite":"item0.png","group":"item","x":336,"y":144,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.5},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"-0.2","Effect7Amount":"2","Effect8Amount":"3","Effect9Amount":"0.1"},"depth":3,"id":1416},"1418":{"name":"Enchantment: Bloodrazor","description":"+50% Attack Speed

UNIQUE Passive: Basic attacks deal 4% of the target's maximum Health in bonus physical damage (max 75 vs. monsters and minions) on hit.","colloq":"","plaintext":"Increases Attack Speed and deals damage based on the target's Health","from":["1043","3711"],"hideFromAll":true,"image":{"full":"1418.png","sprite":"item0.png","group":"item","x":384,"y":144,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.5},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"30","Effect9Amount":"0.1","Effect10Amount":"150"},"depth":3,"id":1418},"1419":{"name":"Enchantment: Bloodrazor","description":"+50% Attack Speed

UNIQUE Passive: Basic attacks deal 4% of the target's maximum Health in bonus physical damage (max 75 vs. monsters and minions) on hit.","colloq":"","plaintext":"Increases Attack Speed and deals damage based on the target's Health","from":["1043","3715"],"hideFromAll":true,"image":{"full":"1419.png","sprite":"item0.png","group":"item","x":432,"y":144,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":2625,"sell":1838},"tags":[],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.5},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"18","Effect9Amount":"0.1","Effect10Amount":"4"},"depth":3,"id":1419},"2003":{"name":"Health Potion","description":"Limited to 5 at one time. Limited to 1 type of Healing Potion.

Click to Consume: Restores 150 Health over 15 seconds.","colloq":";","plaintext":"Consume to restore Health over time","stacks":5,"consumed":true,"image":{"full":"2003.png","sprite":"item0.png","group":"item","x":0,"y":192,"w":48,"h":48},"gold":{"base":50,"purchasable":true,"total":50,"sell":20},"tags":["Consumable","Jungle","Lane"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"150","Effect2Amount":"15"},"id":2003},"2009":{"name":"Total Biscuit of Rejuvenation","description":"Click to Consume: Restores 80 Health and 50 Mana over 10 seconds.","colloq":";","plaintext":"","consumed":true,"inStore":false,"image":{"full":"2009.png","sprite":"item0.png","group":"item","x":48,"y":192,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":2009},"2010":{"name":"Total Biscuit of Rejuvenation","description":"Click to Consume: Restores 15 Health and 15 Mana immediately and then 150 Health over 15 seconds.","colloq":";","plaintext":"","stacks":5,"consumed":true,"inStore":false,"image":{"full":"2010.png","sprite":"item0.png","group":"item","x":96,"y":192,"w":48,"h":48},"gold":{"base":50,"purchasable":false,"total":50,"sell":20},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"15","Effect2Amount":"15","Effect3Amount":"150","Effect4Amount":"15"},"id":2010},"2011":{"name":"Elixir Of Skill","description":"Click to Consume: Grants +1 Skill Point.","colloq":";","plaintext":"","stacks":5,"consumed":true,"inStore":false,"image":{"full":"2011.png","sprite":"item0.png","group":"item","x":144,"y":192,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Consumable","Lane","Jungle"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":2011},"2015":{"name":"Kircheis Shard","description":"+15% Attack Speed

Passive: Moving and attacking will make an attack Energized.

UNIQUE Passive - Energized Strike: Your Energized attacks deal 50 bonus magic damage on hit.","colloq":";","plaintext":"Attack speed and a chargable magic hit","from":["1042"],"into":["3094","3087"],"image":{"full":"2015.png","sprite":"item0.png","group":"item","x":192,"y":192,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":800,"sell":560},"tags":["AttackSpeed","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.15},"effect":{"Effect1Amount":"50"},"depth":2,"id":2015},"2031":{"name":"Refillable Potion","description":"Limited to 1 type of Healing Potion.

UNIQUE Active: Consumes a charge to restore 125 Health over 12 seconds. Holds up to 2 charges and refills upon visiting the shop.","colloq":";","plaintext":"Restores Health over time. Refills at shop.","into":["2032","2033"],"image":{"full":"2031.png","sprite":"item0.png","group":"item","x":240,"y":192,"w":48,"h":48},"gold":{"base":150,"purchasable":true,"total":150,"sell":60},"tags":["HealthRegen","Consumable","Active","Lane","Jungle"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"125","Effect2Amount":"0","Effect3Amount":"12","Effect4Amount":"2"},"id":2031},"2032":{"name":"Hunter's Potion","description":"Limited to 1 type of Healing Potion.

UNIQUE Active: Consumes a charge to restore 60 Health and 35 Mana over 8 seconds. Holds up to 5 charges and refills upon visiting the shop.

Killing a Large Monster grants 1 charge.

(Killing a Large Monster at full charges will automatically consume the newest charge.)","colloq":";","plaintext":"Restores Health and Mana over time - Refills at shop and has increased capacity","from":["2031"],"image":{"full":"2032.png","sprite":"item0.png","group":"item","x":288,"y":192,"w":48,"h":48},"gold":{"base":250,"purchasable":true,"total":400,"sell":160},"tags":["HealthRegen","ManaRegen","Consumable","Active","Jungle"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"60","Effect2Amount":"35","Effect3Amount":"8","Effect4Amount":"5"},"depth":2,"id":2032},"2033":{"name":"Corrupting Potion","description":"Limited to 1 type of Healing Potion.

UNIQUE Active: Consumes a charge to restore 125 Health and 75 Mana over 12 seconds and grants Touch of Corruption during that time. Holds up to 3 charges that refills upon visiting the shop.

Touch of Corruption: Damaging spells and attacks burn enemy champions for 15 - 30 magic damage over 3 seconds. (Half Damage for Area of Effect or Damage over Time spells. Damage increases with champion level.)

(Corrupting Potion can be used even at full Health and Mana.)","colloq":";","plaintext":"Restores Health and Mana over time and boosts combat power - Refills at Shop","from":["2031"],"image":{"full":"2033.png","sprite":"item0.png","group":"item","x":336,"y":192,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":500,"sell":200},"tags":["Active","Consumable","HealthRegen","Lane","ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"125","Effect2Amount":"75","Effect3Amount":"12","Effect4Amount":"3","Effect5Amount":"10","Effect6Amount":"0.1","Effect7Amount":"15","Effect8Amount":"3","Effect9Amount":"30"},"depth":2,"id":2033},"2045":{"name":"Ruby Sightstone","description":"+500 Health

UNIQUE Passive: Item Active cooldowns are reduced by 20%.
UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 4 charges and refills when visiting the shop.

(A player may only have 3 Stealth Wards on the map at one time. Unique Passives with the same name don't stack.)","colloq":";","plaintext":"Greatly increases Health and provides Stealth Wards over time","from":["2049","1028"],"image":{"full":"2045.png","sprite":"item0.png","group":"item","x":384,"y":192,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":1600,"sell":640},"tags":["Active","Health","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":500},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"4","Effect3Amount":"150"},"depth":3,"id":2045},"2047":{"name":"Oracle's Extract","description":"Click to Consume: Grants detection of nearby invisible or unseen enemy units for 5 minutes.","colloq":";","plaintext":"Allows champion to see invisible or unseen enemy units","consumed":true,"consumeOnFull":true,"image":{"full":"2047.png","sprite":"item0.png","group":"item","x":432,"y":192,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":300,"sell":120},"tags":["Consumable","Stealth","Vision"],"maps":{"8":true,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{},"id":2047},"2049":{"name":"Sightstone","description":"+150 Health

UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 3 charges which refill upon visiting the shop.

(A player may only have 3 Stealth Wards on the map at one time. Unique Passives with the same name don't stack.)","colloq":";","plaintext":"Increases Health and provides Stealth Wards over time","from":["1028"],"into":["2045","2301","2302","2303"],"image":{"full":"2049.png","sprite":"item0.png","group":"item","x":0,"y":240,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":800,"sell":320},"tags":["Active","Health","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":150},"effect":{"Effect1Amount":"3","Effect2Amount":"150"},"depth":2,"id":2049},"2050":{"name":"Explorer's Ward","description":"Click to Consume: Places an invisible ward that reveals the surrounding area for 60 seconds.","colloq":";","plaintext":"","inStore":false,"image":{"full":"2050.png","sprite":"item0.png","group":"item","x":48,"y":240,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Consumable"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":2050},"2051":{"name":"Guardian's Horn","description":"+150 Health

Passive: Restores 20 Health every 5 seconds.
UNIQUE Passive: Blocks 12 damage from attacks and spells from champions (25% effectiveness vs. damage over time abilities).

Limited to 1 Guardian's Item.","colloq":"Golden Arm of Kobe;Golden Bicep of Kobe;Horn; Horn of the ManWolf; ManWolf","plaintext":"Good starting item for tanks","image":{"full":"2051.png","sprite":"item0.png","group":"item","x":96,"y":240,"w":48,"h":48},"gold":{"base":950,"purchasable":true,"total":950,"sell":380},"tags":["Health","HealthRegen","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":150,"FlatHPRegenMod":4},"effect":{"Effect1Amount":"12","Effect2Amount":"0.25"},"id":2051},"2052":{"name":"Poro-Snax","description":"This savory blend of free-range, grass-fed Avarosan game hens and organic, non-ZMO Freljordian herbs contains the essential nutrients necessary to keep your Poro purring with pleasure.

All proceeds will be donated towards fighting Noxian animal cruelty.","colloq":";","plaintext":"","stacks":2,"consumed":true,"inStore":false,"image":{"full":"2052.png","sprite":"item0.png","group":"item","x":144,"y":240,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":2052},"2053":{"name":"Raptor Cloak","description":"+40 Armor
+125% Base Health Regen


UNIQUE Passive - Point Runner: Builds up to +20% Movement Speed over 2 seconds while near turrets, fallen turrets and Void Gates.","colloq":";","plaintext":"Enhances Movement Speed near turrets","from":["1006","1031"],"into":["3512","3056","3069"],"image":{"full":"2053.png","sprite":"item0.png","group":"item","x":192,"y":240,"w":48,"h":48},"gold":{"base":250,"purchasable":true,"total":1200,"sell":840},"tags":["Armor","HealthRegen","NonbootsMovement"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":40},"effect":{"Effect1Amount":"20","Effect2Amount":"2"},"depth":3,"id":2053},"2054":{"name":"Diet Poro-Snax","description":"All the flavor of regular Poro-Snax, without the calories! Keeps your Poro happy AND healthy.

Click to Consume: Gives your Poros a delicious healthy treat.","colloq":"","plaintext":"","consumed":true,"inStore":false,"image":{"full":"2054.png","sprite":"item0.png","group":"item","x":240,"y":240,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":2054},"2055":{"name":"Control Ward","description":"Can only carry 3 Control Wards in inventory.

Click to Consume: Places a ward that grants vision of the surrounding area. This device will also reveal invisible traps and reveal / disable wards. Control Wards do not disable other Control Wards. Camouflaged units will also be revealed.

Limit 1 Control Ward on the map per player.","colloq":"orange;","plaintext":"Used to disable wards and invisible traps in an area.","stacks":3,"consumed":true,"image":{"full":"2055.png","sprite":"item0.png","group":"item","x":288,"y":240,"w":48,"h":48},"gold":{"base":75,"purchasable":true,"total":75,"sell":30},"tags":["Consumable","Lane","Stealth","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1","Effect2Amount":"3"},"id":2055},"2138":{"name":"Elixir of Iron","description":"Level 9 required to purchase.

Click to Consume: Grants +300 Health, 25% Tenacity, increased champion size, and Path of Iron for 3 minutes.

Path of Iron: Moving leaves a path behind that boosts allied champion's Movement Speed by 15%.

(Only one Elixir effect may be active at a time.)","colloq":";white","plaintext":"Temporarily increases defenses. Leaves a trail for allies to follow.","consumed":true,"consumeOnFull":true,"image":{"full":"2138.png","sprite":"item0.png","group":"item","x":336,"y":240,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":500,"sell":200},"tags":["Health","Consumable","NonbootsMovement","Tenacity"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"300","Effect2Amount":"0.25","Effect3Amount":"3","Effect4Amount":"0.15","Effect5Amount":"0.15","Effect6Amount":"0","Effect7Amount":"0","Effect8Amount":"9"},"id":2138},"2139":{"name":"Elixir of Sorcery","description":"Level 9 required to purchase.

Click to Consume: Grants +50 Ability Power, 15 bonus Mana Regen per 5 seconds and Sorcery for 3 minutes.

Sorcery: Damaging a champion or turret deals 25 bonus True Damage. This effect has a 5 second cooldown versus champions but no cooldown versus turrets.

(Only one Elixir effect may be active at a time.)
","colloq":";blue","plaintext":"Temporarily grants Ability Power and Bonus Damage to champions and turrets.","consumed":true,"consumeOnFull":true,"image":{"full":"2139.png","sprite":"item0.png","group":"item","x":384,"y":240,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":500,"sell":200},"tags":["Consumable","ManaRegen","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"50","Effect2Amount":"50","Effect3Amount":"25","Effect4Amount":"3","Effect5Amount":"5","Effect6Amount":"3","Effect7Amount":"0","Effect8Amount":"9"},"id":2139},"2140":{"name":"Elixir of Wrath","description":"Level 9 required to purchase.

Click to Consume: Grants +30 Attack Damage and Bloodlust for 3 minutes.

Bloodlust: Dealing physical damage to champions heals for 15% of the damage dealt.

(Only one Elixir effect may be active at a time.)","colloq":";red","plaintext":"Temporarily grants Attack Damage and heals you when dealing physical damage to champions.","consumed":true,"consumeOnFull":true,"image":{"full":"2140.png","sprite":"item0.png","group":"item","x":432,"y":240,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":500,"sell":200},"tags":["Consumable","Damage","LifeSteal","SpellVamp"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"30","Effect2Amount":"30","Effect3Amount":"0.15","Effect4Amount":"3","Effect5Amount":"0","Effect6Amount":"0","Effect7Amount":"0","Effect8Amount":"9"},"id":2140},"2301":{"name":"Eye of the Watchers","description":"+200 Health
+50% Base Mana Regen
+35 Ability Power
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Tribute: Damaging spells and attacks against champions or buildings deal 15 additional damage and grant 15 Gold. This can occur up to 3 times every 30 seconds.
UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 4 charges which refill upon visiting the shop.
QUEST: Earn 650 gold using this item.
REWARD: Tribute is upgraded into Queen's Tribute.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Provides Ability Power and Stealth Wards over time","from":["2049","3098"],"image":{"full":"2301.png","sprite":"item0.png","group":"item","x":0,"y":288,"w":48,"h":48},"gold":{"base":550,"purchasable":true,"total":2200,"sell":880},"tags":["Health","SpellDamage","ManaRegen","Vision","Active","GoldPer","CooldownReduction"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":200,"FlatMagicDamageMod":35},"effect":{"Effect1Amount":"2","Effect2Amount":"15","Effect3Amount":"15","Effect4Amount":"4","Effect5Amount":"30","Effect6Amount":"12","Effect7Amount":"150","Effect8Amount":"3"},"depth":3,"id":2301},"2302":{"name":"Eye of the Oasis","description":"+200 Health
+125% Base Health Regen
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Favor: Enemy minions killed by your allies sometimes drop coins that give either 30 gold or 8% missing mana (minimum 15). Cannon minions always drop coins.
UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 4 charges which refill upon visiting the shop
QUEST: Earn 650 gold using this item.
REWARD: Favor is upgraded to Emperor's Favor and you receive an Elixir Of Skill.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Provides Gold, Mana, and Stealth Wards over time","from":["2049","3096"],"image":{"full":"2302.png","sprite":"item0.png","group":"item","x":48,"y":288,"w":48,"h":48},"gold":{"base":250,"purchasable":true,"total":1900,"sell":760},"tags":["Health","HealthRegen","ManaRegen","Vision","Active","GoldPer","CooldownReduction"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":200},"effect":{"Effect1Amount":"2","Effect2Amount":"15","Effect3Amount":"25","Effect4Amount":"4","Effect5Amount":"0","Effect6Amount":"0","Effect7Amount":"150"},"depth":3,"id":2302},"2303":{"name":"Eye of the Equinox","description":"+500 Health
+200% Base Health Regen
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Spoils of War: Melee basic attacks execute minions below 320 (+20 per level) Health. Killing a minion heals the owner and the nearest allied champion for 50 Health and grants them kill Gold. These effects require a nearby ally. Recharges every 30 seconds. Max 4 charges.
UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 4 charges which refill upon visiting the shop.
QUEST: Earn 650 gold using this item.
REWARD: Shield Battery, a permanent shield that regenerates slowly outside of combat.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Provides Health and Stealth Wards over time","from":["2049","3097"],"image":{"full":"2303.png","sprite":"item0.png","group":"item","x":96,"y":288,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2300,"sell":920},"tags":["Health","HealthRegen","Vision","Active","GoldPer"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":500},"effect":{"Effect1Amount":"320","Effect2Amount":"50","Effect3Amount":"30","Effect4Amount":"4","Effect5Amount":"0","Effect6Amount":"0","Effect7Amount":"150","Effect8Amount":"0","Effect9Amount":"2","Effect10Amount":"20"},"depth":3,"id":2303},"3001":{"name":"Abyssal Mask","description":"+300 Health
+65 Magic Resist
+100% Base Health Regeneration
+10% Cooldown Reduction


UNIQUE Aura: Nearby enemy champions take 10% more magic damage.","colloq":";","plaintext":"Nearby enemies take more magic damage","from":["3211","1057"],"image":{"full":"3001.png","sprite":"item0.png","group":"item","x":144,"y":288,"w":48,"h":48},"gold":{"base":880,"purchasable":true,"total":2800,"sell":1960},"tags":["Health","SpellBlock","HealthRegen","Aura","CooldownReduction","MagicPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatSpellBlockMod":65},"effect":{"Effect1Amount":"-10","Effect2Amount":"-25","Effect3Amount":"0.1"},"depth":3,"id":3001},"3003":{"name":"Archangel's Staff","description":"+80 Ability Power
+250 Mana


UNIQUE Passive - Awe: Grants Ability Power equal to 3% of maximum Mana. Refunds 25% of Mana spent.
UNIQUE Passive - Mana Charge: Grants +8 maximum Mana (max +750 Mana) for each spell cast or Mana expenditure (occurs up to 2 times every 8 seconds).

Transforms into Seraph's Embrace at +750 Mana.
","colloq":";aa","plaintext":"Increases Ability Power based on maximum Mana","from":["3070","1058"],"image":{"full":"3003.png","sprite":"item0.png","group":"item","x":192,"y":288,"w":48,"h":48},"gold":{"base":1100,"purchasable":true,"total":3100,"sell":2170},"tags":["Mana","ManaRegen","SpellDamage"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.03","Effect2Amount":"8","Effect3Amount":"750","Effect4Amount":"2","Effect5Amount":"8","Effect6Amount":"0","Effect7Amount":"0.25"},"depth":3,"id":3003},"3004":{"name":"Manamune","description":"+25 Attack Damage
+250 Mana


UNIQUE Passive - Awe: Grants bonus Attack Damage equal to 2% of maximum Mana. Refunds 15% of Mana spent.
UNIQUE Passive - Mana Charge: Grants +4 maximum Mana (max +750 Mana) for each basic attack, spell cast or Mana expenditure (occurs up to 2 times every 8 seconds).

Transforms into Muramana at +750 Mana.
","colloq":";","plaintext":"Increases Attack Damage based on maximum Mana","from":["3070","1037"],"image":{"full":"3004.png","sprite":"item0.png","group":"item","x":240,"y":288,"w":48,"h":48},"gold":{"base":775,"purchasable":true,"total":2400,"sell":1680},"tags":["Damage","Mana","ManaRegen","OnHit"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"FlatMPPoolMod":250},"effect":{"Effect1Amount":"0.02","Effect2Amount":"4","Effect3Amount":"750","Effect4Amount":"2","Effect5Amount":"8","Effect6Amount":"1","Effect7Amount":"0.15"},"depth":3,"id":3004},"3006":{"name":"Berserker's Greaves","description":" +35% Attack Speed

UNIQUE Passive - Enhanced Movement: +45 Movement Speed","colloq":";","plaintext":"Enhances Movement Speed and Attack Speed","from":["1001","1042"],"image":{"full":"3006.png","sprite":"item0.png","group":"item","x":288,"y":288,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1100,"sell":770},"tags":["AttackSpeed","Boots"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":45,"PercentAttackSpeedMod":0.35},"depth":2,"id":3006},"3007":{"name":"Archangel's Staff (Quick Charge)","description":"+80 Ability Power
+250 Mana


UNIQUE Passive - Awe: Grants Ability Power equal to 3% of maximum Mana. Refunds 25% of Mana spent.
UNIQUE Passive - Mana Charge: Grants +12 maximum Mana (max +750 Mana) for each spell cast or Mana expenditure (occurs up to 2 times every 8 seconds).

Transforms into Seraph's Embrace at +750 Mana.
","colloq":";aa","plaintext":"Increases Ability Power based on maximum Mana","from":["3073","1058"],"image":{"full":"3007.png","sprite":"item0.png","group":"item","x":336,"y":288,"w":48,"h":48},"gold":{"base":1100,"purchasable":true,"total":3100,"sell":2170},"tags":["Mana","ManaRegen","SpellDamage"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.03","Effect2Amount":"12","Effect3Amount":"750","Effect4Amount":"2","Effect5Amount":"8","Effect6Amount":"0","Effect7Amount":"0.25"},"depth":3,"id":3007},"3008":{"name":"Manamune (Quick Charge)","description":"+25 Attack Damage
+250 Mana


UNIQUE Passive - Awe: Grants bonus Attack Damage equal to 2% of maximum Mana. Refunds 15% of Mana spent.
UNIQUE Passive - Mana Charge: Grants +6 maximum Mana (max +750 Mana) for each basic attack, spell cast or Mana expenditure (occurs up to 2 times every 8 seconds).

Transforms into Muramana at +750 Mana.
","colloq":";","plaintext":"Increases Attack Damage based on maximum Mana","from":["3073","1037"],"image":{"full":"3008.png","sprite":"item0.png","group":"item","x":384,"y":288,"w":48,"h":48},"gold":{"base":775,"purchasable":true,"total":2400,"sell":1680},"tags":["Damage","Mana","ManaRegen","OnHit"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"FlatMPPoolMod":250},"effect":{"Effect1Amount":"0.02","Effect2Amount":"6","Effect3Amount":"750","Effect4Amount":"2","Effect5Amount":"8","Effect6Amount":"1","Effect7Amount":"0.15"},"depth":3,"id":3008},"3009":{"name":"Boots of Swiftness","description":"UNIQUE Passive - Enhanced Movement: +55 Movement Speed
UNIQUE Passive - Slow Resist: Movement slowing effects are reduced by 25%.","colloq":";","plaintext":"Enhances Movement Speed and reduces the effect of slows","from":["1001"],"image":{"full":"3009.png","sprite":"item0.png","group":"item","x":432,"y":288,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":900,"sell":630},"tags":["Boots"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":55},"effect":{"Effect1Amount":"0.25"},"depth":2,"id":3009},"3010":{"name":"Catalyst of Aeons","description":"+225 Health
+300 Mana


UNIQUE Passive - Eternity: 15% of damage taken from champions is gained as Mana.

Spending Mana restores 20% of the cost as Health, up to 15 per spell cast.

(Toggled Spells heal for a maximum of 15 per second.)","colloq":";","plaintext":"Spend Mana to recover Health","from":["1028","1027"],"into":["3027","3029","3030","3800"],"image":{"full":"3010.png","sprite":"item0.png","group":"item","x":0,"y":336,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1100,"sell":770},"tags":["Health","HealthRegen","Mana","ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":225,"FlatMPPoolMod":300},"effect":{"Effect1Amount":"150","Effect2Amount":"200","Effect3Amount":"8","Effect4Amount":"15","Effect5Amount":"0.2","Effect6Amount":"0.15"},"depth":2,"id":3010},"3020":{"name":"Sorcerer's Shoes","description":"+15 Magic Penetration

UNIQUE Passive - Enhanced Movement: +45 Movement Speed","colloq":";","plaintext":"Enhances Movement Speed and magic damage","from":["1001"],"image":{"full":"3020.png","sprite":"item0.png","group":"item","x":48,"y":336,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":1100,"sell":770},"tags":["Boots","MagicPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":45},"effect":{"Effect1Amount":"15"},"depth":2,"id":3020},"3022":{"name":"Frozen Mallet","description":"+700 Health
+30 Attack Damage


UNIQUE Passive - Icy: Basic attacks slow the target's Movement Speed for 1.5 seconds on hit (40% slow for melee attacks, 30% slow for ranged attacks).","colloq":";fm","plaintext":"Basic attacks slow enemies","from":["3052","1011"],"image":{"full":"3022.png","sprite":"item0.png","group":"item","x":96,"y":336,"w":48,"h":48},"gold":{"base":900,"purchasable":true,"total":3100,"sell":2170},"tags":["Damage","Health","OnHit","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":30,"FlatHPPoolMod":700},"effect":{"Effect1Amount":"1.5","Effect2Amount":"0.4","Effect3Amount":"0.3"},"depth":3,"id":3022},"3024":{"name":"Glacial Shroud","description":"+25 Armor
+250 Mana


UNIQUE Passive: +10% Cooldown Reduction","colloq":";","plaintext":"Increases Armor and Cooldown Reduction","from":["1027","1029"],"into":["3110","3025","3050","3060","3187"],"image":{"full":"3024.png","sprite":"item0.png","group":"item","x":144,"y":336,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1000,"sell":700},"tags":["Armor","CooldownReduction","Mana"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250,"FlatArmorMod":25},"effect":{"Effect1Amount":"-0.1"},"depth":2,"id":3024},"3025":{"name":"Iceborn Gauntlet","description":"+65 Armor
+20% Cooldown Reduction
+500 Mana


UNIQUE Passive - Spellblade: After using an ability, the next basic attack deals bonus physical damage equal to 100% of base Attack Damage in an area and creates an icy zone for 2 seconds that slows Movement Speed by 30% (1.5 second cooldown).

Size of zone increases with bonus armor.","colloq":";frozen fist","plaintext":"Basic attacks create a slow field after spell cast","from":["3057","3024"],"image":{"full":"3025.png","sprite":"item0.png","group":"item","x":192,"y":336,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2700,"sell":1890},"tags":["Armor","Mana","CooldownReduction","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":500,"FlatArmorMod":65},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"1","Effect3Amount":"2","Effect4Amount":"-0.3","Effect5Amount":"1.5"},"depth":3,"id":3025},"3026":{"name":"Guardian Angel","description":"+40 Attack Damage
+30 Armor


UNIQUE Passive: Upon taking lethal damage, restores 50% of base Health and 30% of maximum Mana after 4 seconds of stasis (300 second cooldown).","colloq":";ga","plaintext":"Periodically revives champion upon death","from":["1038","1029"],"image":{"full":"3026.png","sprite":"item0.png","group":"item","x":240,"y":336,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":2400,"sell":960},"tags":["Armor","Damage"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":40,"FlatArmorMod":30},"effect":{"Effect1Amount":"0.5","Effect2Amount":"4","Effect3Amount":"300","Effect4Amount":"0.3"},"depth":2,"id":3026},"3027":{"name":"Rod of Ages","description":"+300 Health
+300 Mana
+60 Ability Power


Passive: Grants +20 Health, +10 Mana, and +4 Ability Power per stack (max +200 Health, +100 Mana, and +40 Ability Power). Grants 1 stack per minute (max 10 stacks).
UNIQUE Passive - Eternity: 15% of damage taken from champions is gained as Mana. Spending Mana restores 20% of the cost as Health, up to 25 per spell cast.","colloq":";roa","plaintext":"Greatly increases Health, Mana, and Ability Power","from":["3010","1026"],"image":{"full":"3027.png","sprite":"item0.png","group":"item","x":288,"y":336,"w":48,"h":48},"gold":{"base":750,"purchasable":true,"total":2700,"sell":1890},"tags":["Health","HealthRegen","Mana","ManaRegen","SpellDamage"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMPPoolMod":300,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"20","Effect2Amount":"10","Effect3Amount":"4","Effect4Amount":"1","Effect5Amount":"10","Effect6Amount":"150","Effect7Amount":"200","Effect8Amount":"8","Effect9Amount":"25","Effect10Amount":"0.2","Effect11Amount":"0.15"},"depth":3,"id":3027},"3028":{"name":"Chalice of Harmony","description":"+30 Magic Resist
+50% Base Mana Regen


UNIQUE Passive - Harmony: Grants bonus % Base Health Regen equal to your bonus % Base Mana Regen.","colloq":";","plaintext":"Increases Mana and Health Regeneration","from":["1004","1033","1004"],"into":["3174","3222"],"image":{"full":"3028.png","sprite":"item0.png","group":"item","x":336,"y":336,"w":48,"h":48},"gold":{"base":100,"purchasable":true,"total":800,"sell":560},"tags":["SpellBlock","HealthRegen","ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":30},"effect":{"Effect1Amount":"2","Effect2Amount":"5","Effect3Amount":"600","Effect4Amount":"180","Effect5Amount":"8","Effect6Amount":"1","Effect7Amount":"1"},"depth":2,"id":3028},"3029":{"name":"Rod of Ages (Quick Charge)","description":"+300 Health
+300 Mana
+60 Ability Power


Passive: Grants +20 Health, +10 Mana, and +4 Ability Power per stack (max +200 Health, +100 Mana, and +40 Ability Power). Grants 1 stack per 40 seconds (max 10 stacks).
UNIQUE Passive - Eternity: 15% of damage taken from champions is gained as Mana. Spending Mana restores 20% of the cost as Health, up to 25 per spell cast.","colloq":";roa","plaintext":"Greatly increases Health, Mana, and Ability Power","from":["3010","1026"],"image":{"full":"3029.png","sprite":"item0.png","group":"item","x":384,"y":336,"w":48,"h":48},"gold":{"base":750,"purchasable":true,"total":2700,"sell":1890},"tags":["Health","HealthRegen","Mana","ManaRegen","SpellDamage"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMPPoolMod":300,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"20","Effect2Amount":"10","Effect3Amount":"4","Effect4Amount":"1","Effect5Amount":"10","Effect6Amount":"150","Effect7Amount":"200","Effect8Amount":"8","Effect9Amount":"25","Effect10Amount":"0.2","Effect11Amount":"0.15"},"depth":3,"id":3029},"3030":{"name":"Hextech GLP-800","description":"+300 Health
+400 Mana
+80 Ability Power


UNIQUE Passive - Eternity: 15% of damage taken from champions is gained as Mana. Spending Mana restores 20% of the cost as Health, up to 25 per spell cast.
UNIQUE Active - Frost Bolt: Fires a spray of icy bolts that explode, dealing 100 - 200 (+35% of your Ability Power) magic damage to all enemies hit. (40 second cooldown, shared with other Hextech items).

Enemies hit are slowed by 65% decaying over 0.5 seconds.

(Frost Bolt has a cast time, in contrast to most actives.) ","colloq":"frost cannon;","plaintext":"Activate to fire icy bolts to slow enemies","from":["3010","3145"],"image":{"full":"3030.png","sprite":"item0.png","group":"item","x":432,"y":336,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":3000,"sell":2100},"tags":["Health","HealthRegen","SpellDamage","Mana","ManaRegen","Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMPPoolMod":400,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"0.5","Effect5Amount":"0.65","Effect6Amount":"100","Effect7Amount":"200","Effect8Amount":"0.35","Effect9Amount":"25","Effect10Amount":"0.2","Effect11Amount":"0.15","Effect12Amount":"40"},"depth":3,"id":3030},"3031":{"name":"Infinity Edge","description":"+70 Attack Damage
+20% Critical Strike Chance


UNIQUE Passive: Critical strike bonus damage is increased by 50%.","colloq":";ie","plaintext":"Massively enhances critical strikes","from":["1038","1037","1018"],"image":{"full":"3031.png","sprite":"item0.png","group":"item","x":0,"y":384,"w":48,"h":48},"gold":{"base":425,"purchasable":true,"total":3400,"sell":2380},"tags":["CriticalStrike","Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":70,"FlatCritChanceMod":0.2},"effect":{"Effect1Amount":"0.5"},"depth":2,"id":3031},"3033":{"name":"Mortal Reminder","description":"+50 Attack Damage

UNIQUE Passive - Executioner: Physical damage inflicts Grievous Wounds on enemy champions for 5 seconds.
UNIQUE Passive - Last Whisper: +35% Bonus Armor Penetration.","colloq":";lw;grievous","plaintext":"Overcomes enemies with high Health recovery and Armor","from":["3035","3123"],"image":{"full":"3033.png","sprite":"item0.png","group":"item","x":48,"y":384,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":2600,"sell":1820},"tags":["ArmorPenetration","Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":50},"effect":{"Effect1Amount":"5","Effect2Amount":"0.35"},"depth":3,"id":3033},"3034":{"name":"Giant Slayer","description":"+10 Attack Damage

UNIQUE Passive - Giant Slayer: Grants up to +10% physical damage against enemy champions with greater maximum Health than you (+1% damage per 100 Health difference, maxing at 1000 Health difference).

(Unique Passives with the same name don't stack.)","colloq":";gs","plaintext":"Overcomes enemies with high Health","from":["1036"],"into":["3036"],"image":{"full":"3034.png","sprite":"item0.png","group":"item","x":96,"y":384,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":1000,"sell":700},"tags":["Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":10},"effect":{"Effect1Amount":"0.1","Effect2Amount":"100","Effect3Amount":"0.01","Effect4Amount":"1000"},"depth":2,"id":3034},"3035":{"name":"Last Whisper","description":"+10 Attack Damage

UNIQUE Passive - Last Whisper: +35% Bonus Armor Penetration","colloq":";lw","plaintext":"Overcomes enemies with high Armor","from":["1036"],"into":["3033","3036"],"image":{"full":"3035.png","sprite":"item0.png","group":"item","x":144,"y":384,"w":48,"h":48},"gold":{"base":950,"purchasable":true,"total":1300,"sell":910},"tags":["ArmorPenetration","Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":10},"effect":{"Effect1Amount":"0.35","Effect2Amount":"0.35"},"depth":2,"id":3035},"3036":{"name":"Lord Dominik's Regards","description":"+50 Attack Damage

UNIQUE Passive - Giant Slayer: Grants up to +20% physical damage against enemy champions with greater maximum Health than you (+2% damage per 100 Health difference, maxing at 1000 Health difference).
UNIQUE Passive - Last Whisper: +35% Bonus Armor Penetration","colloq":";lw","plaintext":"Overcomes enemies with high health and armor","from":["3035","3034"],"image":{"full":"3036.png","sprite":"item0.png","group":"item","x":192,"y":384,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":2600,"sell":1820},"tags":["Damage","ArmorPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":50},"effect":{"Effect1Amount":"0.35","Effect2Amount":"0.2","Effect3Amount":"0","Effect4Amount":"100","Effect5Amount":"0.02","Effect6Amount":"1000"},"depth":3,"id":3036},"3040":{"name":"Seraph's Embrace","description":"+80 Ability Power
+1000 Mana


UNIQUE Passive - Awe: Grants Ability Power equal to 3% of maximum Mana. Refunds 25% of Mana spent.
UNIQUE Active - Mana Shield: Consumes 20% of current Mana to grant a shield for 3 seconds that absorbs damage equal to 150 plus the amount of Mana consumed (120 second cooldown).","colloq":";","plaintext":"","specialRecipe":3003,"inStore":false,"image":{"full":"3040.png","sprite":"item0.png","group":"item","x":240,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":1000,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.03","Effect2Amount":"0.2","Effect3Amount":"3","Effect4Amount":"150","Effect5Amount":"120","Effect6Amount":"0","Effect7Amount":"0.25"},"id":3040},"3041":{"name":"Mejai's Soulstealer","description":"+20 Ability Power
+200 Mana


UNIQUE Passive - Dread: Grants +5 Ability Power per Glory. Grants 10% Movement Speed if you have at least 15 Glory.
UNIQUE Passive - Do or Die: Grants 4 Glory for a champion kill or 2 Glory for an assist, up to 25 Glory total. Lose 10 stacks of Glory upon dying.","colloq":";","plaintext":"Grants Ability Power for kills and assists","from":["1082"],"image":{"full":"3041.png","sprite":"item0.png","group":"item","x":288,"y":384,"w":48,"h":48},"gold":{"base":1050,"purchasable":true,"total":1400,"sell":980},"tags":["SpellDamage","Mana"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatMPPoolMod":200,"FlatMagicDamageMod":20},"effect":{"Effect1Amount":"5","Effect2Amount":"4","Effect3Amount":"2","Effect4Amount":"25","Effect5Amount":"0.5","Effect6Amount":"0.1","Effect7Amount":"10","Effect8Amount":"15"},"depth":2,"id":3041},"3042":{"name":"Muramana","description":"+25 Attack Damage
+1000 Mana


UNIQUE Passive - Awe: Grants bonus Attack Damage equal to 2% of maximum Mana. Refunds 15% of Mana spent.
UNIQUE Passive - Shock: Single target spells and attacks (on hit) on Champions consume 3% of current Mana to deal bonus physical damage equal to twice the amount of Mana consumed.

This effect only activates while you have greater than 20% maximum Mana.
","colloq":";","plaintext":"","specialRecipe":3004,"inStore":false,"image":{"full":"3042.png","sprite":"item0.png","group":"item","x":336,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"FlatMPPoolMod":1000},"effect":{"Effect1Amount":"0.15"},"id":3042},"3043":{"name":"Muramana","description":"+25 Attack Damage
+1000 Mana


UNIQUE Passive - Awe: Grants bonus Attack Damage equal to 2% of maximum Mana. Refunds 15% of Mana spent.
UNIQUE Passive - Shock: Single target spells and attacks (on hit) on Champions consume 3% of current Mana to deal bonus physical damage equal to twice the amount of Mana consumed.

This effect only activates while you have greater than 20% maximum Mana.
","colloq":"","plaintext":"","specialRecipe":3008,"inStore":false,"image":{"full":"3043.png","sprite":"item0.png","group":"item","x":384,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"FlatMPPoolMod":1000},"effect":{"Effect1Amount":"0.15"},"id":3043},"3044":{"name":"Phage","description":"+200 Health
+15 Attack Damage


UNIQUE Passive - Rage: Basic attacks grant 20 Movement Speed for 2 seconds. Kills grant 60 Movement Speed instead. This Movement Speed bonus is halved for ranged champions.","colloq":";","plaintext":"Attacks and kills give a small burst of speed","from":["1028","1036"],"into":["3078","3071"],"image":{"full":"3044.png","sprite":"item0.png","group":"item","x":432,"y":384,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1250,"sell":875},"tags":["Damage","Health","NonbootsMovement","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":15,"FlatHPPoolMod":200},"effect":{"Effect1Amount":"20","Effect2Amount":"2","Effect3Amount":"60"},"depth":2,"id":3044},"3046":{"name":"Phantom Dancer","description":"+45% Attack Speed
+30% Critical Strike Chance
+5% Movement Speed


UNIQUE Passive - Spectral Waltz: While within 550 units of an enemy champion you can see, +7% Movement Speed and you can move through units.
UNIQUE Passive - Lament: The last champion hit deals 12% less damage to you (ends after 10 seconds of not hitting).","colloq":";pd","plaintext":"Move faster near enemies and reduce incoming damage","from":["1042","3086","1042"],"image":{"full":"3046.png","sprite":"item0.png","group":"item","x":0,"y":432,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":2600,"sell":1820},"tags":["AttackSpeed","CriticalStrike","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.3,"PercentMovementSpeedMod":0.05,"PercentAttackSpeedMod":0.45},"effect":{"Effect1Amount":"0.12","Effect2Amount":"10","Effect3Amount":"550","Effect4Amount":"0.07"},"depth":3,"id":3046},"3047":{"name":"Ninja Tabi","description":"+30 Armor

UNIQUE Passive: Blocks 10% of the damage from basic attacks.
UNIQUE Passive - Enhanced Movement: +45 Movement Speed","colloq":";","plaintext":"Enhances Movement Speed and reduces incoming basic attack damage","from":["1001","1029"],"image":{"full":"3047.png","sprite":"item0.png","group":"item","x":48,"y":432,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1100,"sell":770},"tags":["Armor","Boots"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":45,"FlatArmorMod":30},"effect":{"Effect1Amount":"0.1"},"depth":2,"id":3047},"3048":{"name":"Seraph's Embrace","description":"+80 Ability Power
+1000 Mana


UNIQUE Passive - Awe: Grants Ability Power equal to 3% of maximum Mana. Refunds 25% of Mana spent.
UNIQUE Active - Mana Shield: Consumes 20% of current Mana to grant a shield for 3 seconds that absorbs damage equal to 150 plus the amount of Mana consumed (120 second cooldown).","colloq":"","plaintext":"","specialRecipe":3007,"inStore":false,"image":{"full":"3048.png","sprite":"item0.png","group":"item","x":96,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":1000,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.03","Effect2Amount":"0.2","Effect3Amount":"3","Effect4Amount":"150","Effect5Amount":"120","Effect6Amount":"0","Effect7Amount":"0.25"},"id":3048},"3050":{"name":"Zeke's Harbinger","description":"+250 Mana
+30 Armor
+50 Ability Power
+10% Cooldown Reduction


UNIQUE Active - Conduit: Bind to target ally (60 second cooldown).
UNIQUE Passive: When within 1000 units of each other, you and your ally generate Charges. Attacking or casting spells generates extra Charges. At 100 Charges, causing damage consumes them, increasing your and your ally's Ability Power by 20% and Critical Strike Chance by 50% for 8 seconds.

(Champions can only be linked by one Zeke's Harbinger at a time.)","colloq":";haroldandkumar","plaintext":"Grants an ally bursts of Critical Strike Chance and Ability Power","from":["1052","3024","1052"],"image":{"full":"3050.png","sprite":"item0.png","group":"item","x":144,"y":432,"w":48,"h":48},"gold":{"base":380,"purchasable":true,"total":2250,"sell":1575},"tags":["Armor","SpellDamage","Mana","Aura","Active","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250,"FlatMagicDamageMod":50,"FlatArmorMod":30},"effect":{"Effect1Amount":"-0.1","Effect2Amount":"1","Effect3Amount":"0.5","Effect4Amount":"0.2","Effect5Amount":"1000","Effect6Amount":"100","Effect7Amount":"8","Effect8Amount":"10","Effect9Amount":"4","Effect10Amount":"2","Effect11Amount":"2","Effect12Amount":"6","Effect13Amount":"60","Effect14Amount":"5"},"depth":3,"id":3050},"3052":{"name":"Jaurim's Fist","description":"+15 Attack Damage
+150 Health


UNIQUE Passive: Killing a unit grants 5 maximum Health. This bonus stacks up to 30 times.","colloq":";enforcer","plaintext":"Attack Damage and stacking Health on Unit Kill","from":["1036","1028"],"into":["3104","3022","3053","3748"],"image":{"full":"3052.png","sprite":"item0.png","group":"item","x":192,"y":432,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":1200,"sell":840},"tags":["Health","Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":15,"FlatHPPoolMod":150},"effect":{"Effect1Amount":"30","Effect2Amount":"5","Effect3Amount":"2","Effect4Amount":"150"},"depth":2,"id":3052},"3053":{"name":"Sterak's Gage","description":"+400 Health
+30% Base Attack Damage


UNIQUE Passive - Lifeline: Upon taking at least 400 to 1800 damage (based on level) within 5 seconds, gain a shield for 75% of your bonus Health that decays over 3 seconds (60 second cooldown).

Sterak's Fury: When Lifeline triggers, grow in size and strength, gaining +30% additional Base Attack Damage for 8 seconds.","colloq":";juggernaut;primal","plaintext":"Shields against large bursts of damage","from":["3052","1036"],"image":{"full":"3053.png","sprite":"item0.png","group":"item","x":240,"y":432,"w":48,"h":48},"gold":{"base":1050,"purchasable":true,"total":2600,"sell":1820},"tags":["Health","Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":400},"effect":{"Effect1Amount":"400","Effect2Amount":"0.3","Effect3Amount":"5","Effect4Amount":"0.75","Effect5Amount":"0","Effect6Amount":"8","Effect7Amount":"60","Effect8Amount":"3","Effect9Amount":"1800"},"depth":3,"id":3053},"3056":{"name":"Ohmwrecker","description":"+300 Health
+50 Armor
+150% Base Health Regen
+10% Cooldown Reduction


UNIQUE Active: Prevents nearby enemy turrets from attacking for 3 seconds (120 second cooldown). This effect cannot be used against the same turret more than once every 8 seconds.

UNIQUE Passive - Point Runner: Builds up to +20% Movement Speed over 2 seconds while near turrets (including fallen turrets) and Void Gates.","colloq":";","plaintext":"Temporarily disables enemy turrets","from":["2053","3067"],"image":{"full":"3056.png","sprite":"item0.png","group":"item","x":288,"y":432,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2650,"sell":1855},"tags":["Active","Armor","CooldownReduction","Health","HealthRegen","NonbootsMovement"],"maps":{"8":false,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatArmorMod":50},"effect":{"Effect1Amount":"3","Effect2Amount":"120","Effect3Amount":"8","Effect4Amount":"0.2","Effect5Amount":"1"},"depth":4,"id":3056},"3057":{"name":"Sheen","description":"+250 Mana
+10% Cooldown Reduction


UNIQUE Passive - Spellblade: After using an ability, the next basic attack deals bonus physical damage equal to 100% base Attack Damage on hit (1.5 second cooldown).","colloq":";","plaintext":"Grants a bonus to next attack after spell cast","from":["1027"],"into":["3078","3100","3025"],"image":{"full":"3057.png","sprite":"item0.png","group":"item","x":336,"y":432,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":1050,"sell":735},"tags":["Mana","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250},"effect":{"Effect1Amount":"1.5","Effect2Amount":"1"},"depth":2,"id":3057},"3060":{"name":"Banner of Command","description":"+60 Armor
+30 Magic Resist
+400 Mana
+10% Cooldown Reduction


UNIQUE Active - Promote: Greatly increases the power of a lane minion and grants it immunity to magic damage (120 second cooldown).","colloq":";flag","plaintext":"Promotes a siege minion to a more powerful unit","from":["3105","3024"],"image":{"full":"3060.png","sprite":"item0.png","group":"item","x":384,"y":432,"w":48,"h":48},"gold":{"base":100,"purchasable":true,"total":2200,"sell":1540},"tags":["SpellBlock","Armor","Mana","Active","CooldownReduction"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":30,"FlatMPPoolMod":400,"FlatArmorMod":60},"effect":{"Effect1Amount":"0.5","Effect2Amount":"15","Effect3Amount":"0.75"},"depth":3,"id":3060},"3065":{"name":"Spirit Visage","description":"+450 Health
+55 Magic Resist
+100% Base Health Regen
+10% Cooldown Reduction


UNIQUE Passive: Increases all healing received by 30%.","colloq":";sv","plaintext":"Increases Health and healing effects","from":["3211","3067"],"image":{"full":"3065.png","sprite":"item0.png","group":"item","x":432,"y":432,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":2800,"sell":1960},"tags":["CooldownReduction","Health","HealthRegen","SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":450,"FlatSpellBlockMod":55},"effect":{"Effect1Amount":"0","Effect2Amount":"0.3","Effect3Amount":"0.5"},"depth":3,"id":3065},"3067":{"name":"Kindlegem","description":"+200 Health

UNIQUE Passive: +10% Cooldown Reduction","colloq":";","plaintext":"Increases Health and Cooldown Reduction","from":["1028"],"into":["3187","3401","3065","3071","3056","3083","3152"],"image":{"full":"3067.png","sprite":"item1.png","group":"item","x":0,"y":0,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":800,"sell":560},"tags":["CooldownReduction","Health"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":200},"effect":{"Effect1Amount":"-0.1"},"depth":2,"id":3067},"3068":{"name":"Sunfire Cape","description":"+425 Health
+60 Armor


UNIQUE Passive - Immolate: Deals 11 (+1 per champion level) magic damage per second to nearby enemies. Deals 200% bonus damage to minions and monsters.","colloq":";","plaintext":"Constantly deals damage to nearby enemies","from":["1031","3751"],"image":{"full":"3068.png","sprite":"item1.png","group":"item","x":48,"y":0,"w":48,"h":48},"gold":{"base":1000,"purchasable":true,"total":2900,"sell":2030},"tags":["Armor","Health"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":425,"FlatArmorMod":60},"effect":{"Effect1Amount":"11","Effect2Amount":"1","Effect3Amount":"2"},"depth":3,"id":3068},"3069":{"name":"Talisman of Ascension","description":"+45 Armor
+175% Base Health Regen
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Point Runner: Builds up to +20% Movement Speed over 2 seconds while near turrets, fallen turrets and Void Gates.
UNIQUE Passive - Favor: Enemy minions killed by your allies sometimes drop coins that give either 30 gold or 8% missing mana (minimum 15). Cannon minions always drop coins.
UNIQUE Active: Grants nearby allies +40% Movement Speed for 3 seconds (60 second cooldown).
QUEST: Earn 650 gold using this item.
REWARD: Favor is upgraded to Emperor's Favor and you receive an Elixir Of Skill.

Limited to 1 Gold Income Item.

''Praise the sun.'' - Historian Shurelya, 22 September, 25 CLE","colloq":";shurelya;reverie","plaintext":"Increases Health / Mana Regeneration and Cooldown Reduction. Activate to speed up nearby allies.","from":["3096","2053"],"image":{"full":"3069.png","sprite":"item1.png","group":"item","x":96,"y":0,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":2400,"sell":960},"tags":["HealthRegen","Armor","ManaRegen","Active","GoldPer","CooldownReduction","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":45},"effect":{"Effect1Amount":"-0.1","Effect2Amount":"2","Effect3Amount":"15","Effect4Amount":"25","Effect5Amount":"0.4","Effect6Amount":"3","Effect7Amount":"60","Effect8Amount":"20","Effect9Amount":"2"},"depth":4,"id":3069},"3070":{"name":"Tear of the Goddess","description":"+250 Mana

UNIQUE Passive - Awe: Refunds 15% of Mana spent.
UNIQUE Passive - Mana Charge: Grants 4 maximum Mana on spell cast or Mana expenditure (up to 2 times per 8 seconds).

Caps at +750 Mana.
","colloq":";","plaintext":"Increases maximum Mana as Mana is spent","from":["1027","1004"],"into":["3003","3004"],"image":{"full":"3070.png","sprite":"item1.png","group":"item","x":144,"y":0,"w":48,"h":48},"gold":{"base":275,"purchasable":true,"total":750,"sell":525},"tags":["Mana","ManaRegen"],"maps":{"8":false,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250},"effect":{"Effect1Amount":"4","Effect2Amount":"8","Effect3Amount":"1","Effect4Amount":"8","Effect5Amount":"750","Effect6Amount":"2","Effect7Amount":"0.15"},"depth":2,"id":3070},"3071":{"name":"The Black Cleaver","description":"+400 Health
+40 Attack Damage
+20% Cooldown Reduction


UNIQUE Passive: Dealing physical damage to an enemy champion Cleaves them, reducing their Armor by 4% for 6 seconds (stacks up to 6 times, up to 24%).
UNIQUE Passive - Rage: Dealing physical damage grants 20 movement speed for 2 seconds. Assists on Cleaved enemy champions or kills on any unit grant 60 movement speed for 2 seconds instead. This Movement Speed is halved for ranged champions.","colloq":";bc","plaintext":"Dealing physical damage to enemy champions reduces their Armor","from":["3044","3067"],"image":{"full":"3071.png","sprite":"item1.png","group":"item","x":192,"y":0,"w":48,"h":48},"gold":{"base":950,"purchasable":true,"total":3000,"sell":2100},"tags":["ArmorPenetration","CooldownReduction","Damage","Health","NonbootsMovement","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":40,"FlatHPPoolMod":400},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"0.04","Effect3Amount":"6","Effect4Amount":"6","Effect5Amount":"0.24","Effect6Amount":"20","Effect7Amount":"2","Effect8Amount":"60","Effect9Amount":"2"},"depth":3,"id":3071},"3072":{"name":"The Bloodthirster","description":"+80 Attack Damage

UNIQUE Passive: +20% Life Steal
UNIQUE Passive: Your basic attacks can now overheal you. Excess life is stored as a shield that can block 50-350 damage, based on champion level.

This shield decays slowly if you haven't dealt or taken damage in the last 25 seconds.","colloq":";bt","plaintext":"Grants Attack Damage, Life Steal and Life Steal now overheals","from":["1038","1036","1053"],"image":{"full":"3072.png","sprite":"item1.png","group":"item","x":240,"y":0,"w":48,"h":48},"gold":{"base":1150,"purchasable":true,"total":3700,"sell":2590},"tags":["Damage","LifeSteal"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":80},"effect":{"Effect1Amount":"50","Effect2Amount":"350","Effect3Amount":"25","Effect4Amount":"0.2"},"depth":3,"id":3072},"3073":{"name":"Tear of the Goddess (Quick Charge)","description":"+250 Mana

UNIQUE Passive - Awe: Refunds 15% of Mana spent.
UNIQUE Passive - Mana Charge: Grants 6 maximum Mana on spell cast or Mana expenditure (up to 2 times per 8 seconds).

Caps at +750 Mana.
","colloq":";","plaintext":"Increases maximum Mana as Mana is spent","from":["1027","1004"],"into":["3007","3008"],"image":{"full":"3073.png","sprite":"item1.png","group":"item","x":288,"y":0,"w":48,"h":48},"gold":{"base":275,"purchasable":true,"total":750,"sell":525},"tags":["Mana","ManaRegen"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":250},"effect":{"Effect1Amount":"6","Effect2Amount":"8","Effect3Amount":"1","Effect4Amount":"8","Effect5Amount":"750","Effect6Amount":"2","Effect7Amount":"0.15"},"depth":2,"id":3073},"3074":{"name":"Ravenous Hydra","description":"+80 Attack Damage
+100% Base Health Regen
+12% Life Steal


Passive: 50% of total Life Steal applies to damage dealt by this item.
UNIQUE Passive - Cleave: Basic attacks deal 20% to 60% of total Attack Damage as bonus physical damage to enemies near the target on hit (enemies closest to the target take the most damage).
UNIQUE Active - Crescent: Deals 60% to 100% of total Attack Damage as physical damage to nearby enemy units (closest enemies take the most damage) (10 second cooldown).","colloq":";","plaintext":"Melee attacks hit nearby enemies, dealing damage and restoring Health","from":["3077","1053","1037"],"image":{"full":"3074.png","sprite":"item1.png","group":"item","x":336,"y":0,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":3500,"sell":2450},"tags":["Active","Damage","HealthRegen","LifeSteal","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":80,"PercentLifeStealMod":0.12},"effect":{"Effect1Amount":"0.2","Effect2Amount":"0.6","Effect3Amount":"0.6","Effect4Amount":"1","Effect5Amount":"10"},"depth":3,"id":3074},"3075":{"name":"Thornmail","description":"+100 Armor

UNIQUE Passive: Upon being hit by a basic attack, reflects magic damage back to the attacker equal to 25% of your bonus Armor plus 15% of the incoming damage.

(Bonus Armor is Armor from items, buffs, runes and masteries.)
(Reflect damage is calculated based on damage taken before being reduced by Armor.)","colloq":";","plaintext":"Returns damage taken from basic attacks as magic damage","from":["1029","1031"],"image":{"full":"3075.png","sprite":"item1.png","group":"item","x":384,"y":0,"w":48,"h":48},"gold":{"base":1250,"purchasable":true,"total":2350,"sell":1645},"tags":["Armor"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":100},"effect":{"Effect1Amount":"0.15","Effect2Amount":"0.25"},"depth":3,"id":3075},"3077":{"name":"Tiamat","description":"+20 Attack Damage
+50% Base Health Regen


UNIQUE Passive - Cleave: Basic attacks deal 20% to 60% of total Attack Damage as bonus physical damage to enemies near the target on hit (enemies closest to the target take the most damage).
UNIQUE Active - Crescent: Deals 60% to 100% of total Attack Damage as physical damage to nearby enemy units (enemies closest to the target take the most damage) (10 second cooldown).","colloq":";","plaintext":"Melee attacks hit nearby enemies","from":["1036","1006","1036"],"into":["3074","3748"],"image":{"full":"3077.png","sprite":"item1.png","group":"item","x":432,"y":0,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1200,"sell":840},"tags":["HealthRegen","Damage","Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":20},"effect":{"Effect1Amount":"0.2","Effect2Amount":"0.6","Effect3Amount":"0.6","Effect4Amount":"1","Effect5Amount":"10"},"depth":2,"id":3077},"3078":{"name":"Trinity Force","description":"+250 Health
+250 Mana
+25 Attack Damage
+40% Attack Speed
+20% Cooldown Reduction
+5% Movement Speed


UNIQUE Passive - Rage: Basic attacks grant 20 Movement Speed for 2 seconds. Kills grant 60 Movement Speed instead. This Movement Speed bonus is halved for ranged champions.
UNIQUE Passive - Spellblade: After using an ability, the next basic attack deals bonus physical damage equal to 200% of base Attack Damage on hit (1.5 second cooldown).","colloq":";triforce;tons of damage","plaintext":"Tons of Damage","from":["3101","3057","3044"],"image":{"full":"3078.png","sprite":"item1.png","group":"item","x":0,"y":48,"w":48,"h":48},"gold":{"base":333,"purchasable":true,"total":3733,"sell":2613},"tags":["Health","Damage","AttackSpeed","Mana","CooldownReduction","OnHit","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"PercentMovementSpeedMod":0.05,"FlatHPPoolMod":250,"FlatMPPoolMod":250,"PercentAttackSpeedMod":0.4},"effect":{"Effect1Amount":"20","Effect2Amount":"60","Effect3Amount":"2","Effect4Amount":"2","Effect5Amount":"1.5"},"depth":3,"id":3078},"3082":{"name":"Warden's Mail","description":"+40 Armor

UNIQUE Passive - Cold Steel: When hit by basic attacks, reduces the attacker's Attack Speed by 15% for 1 seconds.","colloq":";","plaintext":"Slows Attack Speed of enemy champions when receiving basic attacks","from":["1029","1029"],"into":["3110","3143"],"image":{"full":"3082.png","sprite":"item1.png","group":"item","x":48,"y":48,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":1000,"sell":700},"tags":["Armor","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatArmorMod":40},"effect":{"Effect1Amount":"-0.15","Effect2Amount":"1"},"depth":2,"id":3082},"3083":{"name":"Warmog's Armor","description":"+800 Health
+200% Base Health Regen


UNIQUE Passive: +10% Cooldown Reduction
UNIQUE Passive: Grants Warmog's Heart if you have at least 2750 maximum Health.

Warmog's Heart: Restores 25% of maximum Health every 5 seconds if damage hasn't been taken within 6 seconds (3 seconds for damage from minions and monsters).","colloq":";","plaintext":"Grants massive Health and Health Regen","from":["1011","3067","3801"],"image":{"full":"3083.png","sprite":"item1.png","group":"item","x":96,"y":48,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":2850,"sell":1995},"tags":["Health","HealthRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":800},"effect":{"Effect1Amount":"0.015","Effect2Amount":"5","Effect3Amount":"0.25","Effect4Amount":"2750","Effect5Amount":"6","Effect6Amount":"-0.1","Effect7Amount":"6","Effect8Amount":"3"},"depth":3,"id":3083},"3084":{"name":"Overlord's Bloodmail","description":"+800 Health
+100% Base Health Regen


UNIQUE Passive: Upon champion kill or assist, restores 300 Health over 5 seconds.","colloq":";","plaintext":"Restores Health on kill or assist","from":["1011","3801"],"image":{"full":"3084.png","sprite":"item1.png","group":"item","x":144,"y":48,"w":48,"h":48},"gold":{"base":900,"purchasable":true,"total":2550,"sell":1785},"tags":["Health","HealthRegen"],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":800},"effect":{"Effect1Amount":"300","Effect2Amount":"5"},"depth":3,"id":3084},"3085":{"name":"Runaan's Hurricane","description":"+40% Attack Speed
+30% Critical Strike Chance
+7% Movement Speed


UNIQUE Passive - Wind's Fury: When basic attacking, bolts are fired at up to 2 enemies near the target, each dealing (40% of Attack Damage) physical damage. Bolts can critically strike and apply on hit effects.","colloq":";","plaintext":"Ranged attacks fire two bolts at nearby enemies","from":["1042","3086","1042"],"image":{"full":"3085.png","sprite":"item1.png","group":"item","x":192,"y":48,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":2600,"sell":1820},"tags":["CriticalStrike","AttackSpeed","OnHit","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.3,"PercentMovementSpeedMod":0.07,"PercentAttackSpeedMod":0.4},"effect":{"Effect1Amount":"0","Effect2Amount":"40","Effect3Amount":"2","Effect4Amount":"0","Effect5Amount":"40","Effect6Amount":"1"},"depth":3,"id":3085},"3086":{"name":"Zeal","description":"+15% Attack Speed
+20% Critical Strike Chance


UNIQUE Passive: +5% Movement Speed","colloq":";","plaintext":"Slight bonuses to Critical Strike Chance, Movement Speed and Attack Speed","from":["1051","1042"],"into":["3094","3085","3046","3087"],"image":{"full":"3086.png","sprite":"item1.png","group":"item","x":240,"y":48,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1200,"sell":840},"tags":["AttackSpeed","CriticalStrike","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.2,"PercentAttackSpeedMod":0.15},"effect":{"Effect1Amount":"0.05"},"depth":2,"id":3086},"3087":{"name":"Statikk Shiv","description":"+35% Attack Speed
+30% Critical Strike Chance
+5% Movement Speed


Passive: Moving and attacking will make an attack Energized.

UNIQUE Passive - Shiv Lightning: Your Energized attacks deal 60~160 bonus magic damage (based on level) to up to 5 targets on hit (deals +65% bonus damage to minions and can critically strike).","colloq":";","plaintext":"Movement builds charges that release chain lightning on basic attack","from":["3086","2015"],"image":{"full":"3087.png","sprite":"item1.png","group":"item","x":288,"y":48,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":2600,"sell":1820},"tags":["AttackSpeed","CriticalStrike","NonbootsMovement","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.3,"PercentMovementSpeedMod":0.05,"PercentAttackSpeedMod":0.35},"effect":{"Effect1Amount":"100","Effect2Amount":"80","Effect3Amount":"5","Effect4Amount":"750","Effect5Amount":"60","Effect6Amount":"160","Effect7Amount":"5","Effect8Amount":"0.65"},"depth":3,"id":3087},"3089":{"name":"Rabadon's Deathcap","description":"+120 Ability Power

UNIQUE Passive: Increases Ability Power by 35%.","colloq":";dc;banksys;hat","plaintext":"Massively increases Ability Power","from":["1026","1058","1052"],"image":{"full":"3089.png","sprite":"item1.png","group":"item","x":336,"y":48,"w":48,"h":48},"gold":{"base":1265,"purchasable":true,"total":3800,"sell":2660},"tags":["SpellDamage"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":120},"effect":{"Effect1Amount":"35"},"depth":2,"id":3089},"3090":{"name":"Wooglet's Witchcap","description":"+100 Ability Power
+45 Armor


UNIQUE Passive: Increases Ability Power by 25%
UNIQUE Active: Champion becomes invulnerable and untargetable for 2.5 seconds, but is unable to move, attack, cast spells, or use items during this time (120 second cooldown).","colloq":";hat","plaintext":"Massively increases Ability Power and can be activated to enter stasis","from":["3191","1058"],"image":{"full":"3090.png","sprite":"item1.png","group":"item","x":384,"y":48,"w":48,"h":48},"gold":{"base":1050,"purchasable":true,"total":3500,"sell":2450},"tags":["Active","Armor","SpellDamage"],"maps":{"8":true,"10":true,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatMagicDamageMod":100,"FlatArmorMod":45},"effect":{"Effect1Amount":"25","Effect2Amount":"2.5","Effect3Amount":"120"},"depth":3,"id":3090},"3091":{"name":"Wit's End","description":"+40% Attack Speed
+40 Magic Resist


UNIQUE Passive: Basic attacks deal 40 bonus magic damage on hit.
UNIQUE Passive: Basic attacks steal 5 Magic Resist from the target on hit (stacks up to 5 times.)","colloq":";","plaintext":"Deals bonus magic damage on basic attacks","from":["1043","1057","1042"],"image":{"full":"3091.png","sprite":"item1.png","group":"item","x":432,"y":48,"w":48,"h":48},"gold":{"base":480,"purchasable":true,"total":2500,"sell":1750},"tags":["AttackSpeed","OnHit","SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":40,"PercentAttackSpeedMod":0.4},"effect":{"Effect1Amount":"40","Effect2Amount":"5","Effect3Amount":"5"},"depth":3,"id":3091},"3092":{"name":"Frost Queen's Claim","description":"+60 Ability Power
+10% Cooldown Reduction
+2 Gold per 10 seconds
+50% Base Mana Regen


UNIQUE Passive - Tribute: Damaging spells and attacks against champions or buildings deal 15 additional damage and grant 15 Gold. This can occur up to 3 times every 30 seconds.
UNIQUE Active: Summon 2 icy ghosts for 6 seconds that seek out nearby enemy champions. Ghosts reveal enemies on contact and slow them by 40% for between 2 and 5 seconds based on how far the ghosts have traveled (90 second cooldown).
QUEST: Earn 650 gold using this item.
REWARD: Tribute is upgraded into Queen's Tribute.

Limited to 1 Gold Income Item.","colloq":"spooky ghosts;","plaintext":"Sends out seeking wraiths that track hidden enemies and slow them","from":["3098","3108"],"image":{"full":"3092.png","sprite":"item1.png","group":"item","x":0,"y":96,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":2200,"sell":880},"tags":["Active","CooldownReduction","GoldPer","ManaRegen","Slow","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"15","Effect2Amount":"15","Effect3Amount":"12","Effect4Amount":"3","Effect5Amount":"30","Effect6Amount":"2","Effect7Amount":"1","Effect8Amount":"2","Effect9Amount":"2","Effect10Amount":"-0.4","Effect11Amount":"5","Effect12Amount":"6","Effect13Amount":"50","Effect14Amount":"90","Effect15Amount":"0.25","Effect16Amount":"2"},"depth":3,"id":3092},"3094":{"name":"Rapid Firecannon","description":"+30% Attack Speed
+30% Critical Strike Chance
+5% Movement Speed


Passive: Moving and attacking will make an attack Energized.

UNIQUE Passive - Firecannon: Your Energized attacks gain 35% bonus Range (+150 range maximum) and deal 50~120 bonus magic damage (based on level) on hit.

Attacks become Energized 25% faster. Energized attacks function on structures.","colloq":";canon;rapidfire;rfc","plaintext":"Movement builds charges that release a sieging fire attack on release","from":["3086","2015"],"image":{"full":"3094.png","sprite":"item1.png","group":"item","x":48,"y":96,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":2600,"sell":1820},"tags":["AttackSpeed","CriticalStrike","NonbootsMovement","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatCritChanceMod":0.3,"PercentMovementSpeedMod":0.05,"PercentAttackSpeedMod":0.3},"effect":{"Effect1Amount":"0.35","Effect2Amount":"150","Effect3Amount":"50","Effect4Amount":"120","Effect5Amount":"5","Effect6Amount":"0.25"},"depth":3,"id":3094},"3096":{"name":"Nomad's Medallion","description":"+50% Base Health Regen
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Favor: Enemy minions killed by your allies sometimes drop coins that give either 30 gold or 8% missing mana (minimum 15). Cannon minions always drop coins.
QUEST: Earn 650 gold using this item.
REWARD: Favor is upgraded to Emperor's Favor and you receive an Elixir Of Skill.

Limited to 1 Gold Income Item.

''The medallion shines with the glory of a thousand voices when exposed to the sun.'' - Historian Shurelya, 22 June, 24 CLE","colloq":";","plaintext":"Grants gold and mana when nearby minions die that you didn't kill","from":["1006","3301"],"into":["2302","3069"],"image":{"full":"3096.png","sprite":"item1.png","group":"item","x":96,"y":96,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":850,"sell":340},"tags":["HealthRegen","ManaRegen","Active","GoldPer","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"2","Effect2Amount":"15","Effect3Amount":"50","Effect4Amount":"10"},"depth":2,"id":3096},"3097":{"name":"Targon's Brace","description":"+175 Health
+50% Base Health Regen
+2 Gold per 10 seconds


UNIQUE Passive - Spoils of War: Melee basic attacks execute minions below 200 (+10 per level) Health. Killing a minion heals the owner and the nearest allied champion for 40 Health and grants them kill Gold.

These effects require a nearby ally. Recharges every 30 seconds. Max 3 charges.
QUEST: Earn 650 gold using this item.
REWARD: Shield Battery, a permanent shield that regenerates slowly outside of combat.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Periodically kill enemy minions to heal and grant gold to a nearby ally","from":["3302","1006"],"into":["2303","3401"],"image":{"full":"3097.png","sprite":"item1.png","group":"item","x":144,"y":96,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":850,"sell":340},"tags":["Aura","GoldPer","Health","HealthRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":175},"effect":{"Effect1Amount":"200","Effect2Amount":"40","Effect3Amount":"30","Effect4Amount":"3","Effect5Amount":"2","Effect6Amount":"10"},"depth":2,"id":3097},"3098":{"name":"Frostfang","description":"+20 Ability Power
+2 Gold per 10 seconds
+50% Base Mana Regen


UNIQUE Passive - Tribute: Damaging spells and attacks against champions or buildings deal 15 additional damage and grant 15 Gold. This can occur up to 3 times every 30 seconds. Killing minions slows Tribute generation.
QUEST: Earn 650 gold using this item.
REWARD: Tribute is upgraded into Queen's Tribute.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Grants gold when you damage an enemy","from":["3303","1004"],"into":["2301","3092"],"image":{"full":"3098.png","sprite":"item1.png","group":"item","x":192,"y":96,"w":48,"h":48},"gold":{"base":375,"purchasable":true,"total":850,"sell":340},"tags":["Active","GoldPer","ManaRegen","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":20},"effect":{"Effect1Amount":"15","Effect2Amount":"15","Effect3Amount":"12","Effect4Amount":"3","Effect5Amount":"30","Effect6Amount":"2"},"depth":2,"id":3098},"3100":{"name":"Lich Bane","description":"+80 Ability Power
+7% Movement Speed
+10% Cooldown Reduction
+250 Mana


UNIQUE Passive - Spellblade: After using an ability, the next basic attack deals 75% Base Attack Damage (+50% of Ability Power) bonus magic damage on hit (1.5 second cooldown).","colloq":";","plaintext":"Grants a bonus to next attack after spell cast","from":["3057","3113","1026"],"image":{"full":"3100.png","sprite":"item1.png","group":"item","x":240,"y":96,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":3200,"sell":2240},"tags":["SpellDamage","Mana","CooldownReduction","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.07,"FlatMPPoolMod":250,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.75","Effect2Amount":"0.5","Effect3Amount":"1.5"},"depth":3,"id":3100},"3101":{"name":"Stinger","description":"+35% Attack Speed

UNIQUE Passive: +10% Cooldown Reduction","colloq":";","plaintext":"Increased Attack Speed and Cooldown Reduction","from":["1042","1042"],"into":["3115","3137","3078"],"image":{"full":"3101.png","sprite":"item1.png","group":"item","x":288,"y":96,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1100,"sell":770},"tags":["AttackSpeed","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.35},"effect":{"Effect1Amount":"10"},"depth":2,"id":3101},"3102":{"name":"Banshee's Veil","description":"+70 Ability Power
+60 Magic Resist
+10% Cooldown Reduction


UNIQUE Passive: Grants a spell shield that blocks the next enemy ability. This shield refreshes after no damage is taken from enemy champions for 40 seconds.","colloq":";bv","plaintext":"Periodically blocks enemy abilities","from":["3108","1033","1026"],"image":{"full":"3102.png","sprite":"item1.png","group":"item","x":336,"y":96,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":3000,"sell":2100},"tags":["SpellBlock","SpellDamage","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":60,"FlatMagicDamageMod":70},"effect":{"Effect1Amount":"40","Effect2Amount":"45","Effect3Amount":"10","Effect4Amount":"-0.1","Effect5Amount":"8","Effect6Amount":"2"},"depth":3,"id":3102},"3104":{"name":"Lord Van Damm's Pillager","description":"+300 Health
+50 Attack Damage
+10% Cooldown Reduction


UNIQUE Passive - Ashes to Ashes: Controlling the nearest Altar sets you aflame, dealing 25 (+1 per champion level) magic damage per second to nearby enemies (Deals 50% bonus damage to minions and monsters). Controlling the furthest Altar causes your basic attacks to burn targets for up to 114 true damage (based on champion level) over 3 seconds.","colloq":"lvd;","plaintext":"Reduces Armor of nearby enemies","from":["3133","3052"],"image":{"full":"3104.png","sprite":"item1.png","group":"item","x":384,"y":96,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":3000,"sell":2100},"tags":["Health","Damage","Aura","CooldownReduction","OnHit","ArmorPenetration"],"maps":{"8":false,"10":true,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":50,"FlatHPPoolMod":300},"effect":{"Effect1Amount":"25","Effect2Amount":"1","Effect3Amount":"0.5"},"depth":3,"id":3104},"3105":{"name":"Aegis of the Legion","description":"+30 Armor
+30 Magic Resist
","colloq":";","plaintext":"Grants Armor and Magic Resistance","from":["1033","1029"],"into":["3190","3060"],"image":{"full":"3105.png","sprite":"item1.png","group":"item","x":432,"y":96,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1100,"sell":770},"tags":["SpellBlock","Armor"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":30,"FlatArmorMod":30},"depth":2,"id":3105},"3107":{"name":"Redemption","description":"+300 Health
+75% Base Health Regen
+125% Base Mana Regen
+10% Cooldown Reduction


UNIQUE Passive: +10% Heal and Shield Power
UNIQUE Active: Target an area within 5500 range. After 2.5 seconds, call down a beam of light to heal allies for 40 (+25 per level of target) Health, burn enemy champions for 10% of their maximum Health as true damage and deal 250 true damage to enemy minions (120 second cooldown).

Can be used while dead.

(Half effect if the target has been affected by another Redemption recently.)","colloq":";","plaintext":"Further improves defenses for nearby allies","from":["3114","3801"],"image":{"full":"3107.png","sprite":"item1.png","group":"item","x":0,"y":144,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2100,"sell":1470},"tags":["Health","HealthRegen","ManaRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300},"effect":{"Effect1Amount":"0.1","Effect2Amount":"40","Effect3Amount":"25","Effect4Amount":"0.1","Effect5Amount":"250","Effect6Amount":"120","Effect7Amount":"550","Effect8Amount":"5500","Effect9Amount":"0.5","Effect10Amount":"8"},"depth":3,"id":3107},"3108":{"name":"Fiendish Codex","description":"+30 Ability Power

UNIQUE Passive: +10% Cooldown Reduction","colloq":";","plaintext":"Increases Ability Power and Cooldown Reduction","from":["1052"],"into":["3174","3092","3115","3165","3102","3157"],"image":{"full":"3108.png","sprite":"item1.png","group":"item","x":48,"y":144,"w":48,"h":48},"gold":{"base":465,"purchasable":true,"total":900,"sell":630},"tags":["CooldownReduction","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":30},"effect":{"Effect1Amount":"-0.1"},"depth":2,"id":3108},"3109":{"name":"Knight's Vow","description":"+400 Health
+100% Base Health Regen
+40 Armor


UNIQUE Active: Designate an allied champion as your Partner (90 second cooldown).
UNIQUE Passive: If your Partner is nearby, gain +20 additional Armor and +15% Movement Speed towards them.
UNIQUE Passive: If your Partner is nearby, heal for 12% of the damage your Partner deals to champions and redirect 12% of the damage your Partner takes from champions to you as true damage (healing and damage redirection are reduced by 50% if you are ranged).

(Champions can only be linked by one Knight's Vow at a time.)","colloq":";","plaintext":"Partner with an ally to protect each other","from":["3801","1031"],"image":{"full":"3109.png","sprite":"item1.png","group":"item","x":96,"y":144,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":2300,"sell":1610},"tags":["Health","HealthRegen","Armor","Aura","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":400,"FlatArmorMod":40},"effect":{"Effect1Amount":"20","Effect2Amount":"0.15","Effect3Amount":"0.12","Effect4Amount":"0.12","Effect5Amount":"90","Effect6Amount":"0.5","Effect7Amount":"1000"},"depth":3,"id":3109},"3110":{"name":"Frozen Heart","description":"+90 Armor
+20% Cooldown Reduction
+400 Mana


UNIQUE Aura: Reduces the Attack Speed of nearby enemies by 15%.","colloq":";fh","plaintext":"Massively increases Armor and slows enemy basic attacks","from":["3082","3024"],"image":{"full":"3110.png","sprite":"item1.png","group":"item","x":144,"y":144,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":2700,"sell":1890},"tags":["Armor","Aura","CooldownReduction","Mana"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":400,"FlatArmorMod":90},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"-0.15"},"depth":3,"id":3110},"3111":{"name":"Mercury's Treads","description":"+25 Magic Resist

UNIQUE Passive - Enhanced Movement: +45 Movement Speed
UNIQUE Passive - Tenacity: Reduces the duration of stuns, slows, taunts, fears, silences, blinds, polymorphs, and immobilizes by 30%.","colloq":";","plaintext":"Increases Movement Speed and reduces duration of disabling effects","from":["1001","1033"],"image":{"full":"3111.png","sprite":"item1.png","group":"item","x":192,"y":144,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1100,"sell":770},"tags":["Boots","SpellBlock","Tenacity"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":45,"FlatSpellBlockMod":25},"depth":2,"id":3111},"3112":{"name":"Guardian's Orb","description":"+150 Health
+35 Ability Power
+10 Mana regen per 5 seconds


Limited to 1 Guardian's Item.","colloq":";","plaintext":"Good starting item for mages","image":{"full":"3112.png","sprite":"item1.png","group":"item","x":240,"y":144,"w":48,"h":48},"gold":{"base":950,"purchasable":true,"total":950,"sell":380},"tags":["Health","SpellDamage","Mana","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":150,"FlatMagicDamageMod":35},"id":3112},"3113":{"name":"Aether Wisp","description":"+30 Ability Power

UNIQUE Passive: +5% Movement Speed","colloq":";","plaintext":"Increases Ability Power and Movement Speed","from":["1052"],"into":["1402","1410","1414","3100","3285","3504","3673"],"image":{"full":"3113.png","sprite":"item1.png","group":"item","x":288,"y":144,"w":48,"h":48},"gold":{"base":415,"purchasable":true,"total":850,"sell":595},"tags":["NonbootsMovement","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":30},"effect":{"Effect1Amount":"0.05"},"depth":2,"id":3113},"3114":{"name":"Forbidden Idol","description":"+50% Base Mana Regen

UNIQUE Passive: +10% Cooldown Reduction
UNIQUE Passive: +8% Heal and Shield Power","colloq":";","plaintext":"Increases Mana Regeneration and Cooldown Reduction","from":["1004","1004"],"into":["3107","3222","3504"],"image":{"full":"3114.png","sprite":"item1.png","group":"item","x":336,"y":144,"w":48,"h":48},"gold":{"base":550,"purchasable":true,"total":800,"sell":560},"tags":["CooldownReduction","ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"-0.1","Effect2Amount":"0.08"},"depth":2,"id":3114},"3115":{"name":"Nashor's Tooth","description":"+50% Attack Speed
+80 Ability Power


UNIQUE Passive: +20% Cooldown Reduction
UNIQUE Passive: Basic attacks deal 15 (+15% of Ability Power) bonus magic damage on hit.
","colloq":";","plaintext":"Increases Attack Speed, Ability Power, and Cooldown Reduction","from":["3101","3108"],"image":{"full":"3115.png","sprite":"item1.png","group":"item","x":384,"y":144,"w":48,"h":48},"gold":{"base":1000,"purchasable":true,"total":3000,"sell":2100},"tags":["AttackSpeed","CooldownReduction","OnHit","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":80,"PercentAttackSpeedMod":0.5},"depth":3,"id":3115},"3116":{"name":"Rylai's Crystal Scepter","description":"+300 Health
+75 Ability Power


UNIQUE Passive: Damaging spells and abilities reduce enemy movement speed by 20% for 1 second.","colloq":";","plaintext":"Abilities slow enemies","from":["1026","1052","1028"],"image":{"full":"3116.png","sprite":"item1.png","group":"item","x":432,"y":144,"w":48,"h":48},"gold":{"base":915,"purchasable":true,"total":2600,"sell":1820},"tags":["Health","SpellDamage","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMagicDamageMod":75},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"-0.2","Effect3Amount":"-0.2","Effect4Amount":"1","Effect5Amount":"1","Effect6Amount":"1"},"depth":2,"id":3116},"3117":{"name":"Boots of Mobility","description":"UNIQUE Passive - Enhanced Movement: +25 Movement Speed. Increases to +115 Movement Speed when out of combat for 5 seconds.","colloq":";","plaintext":"Greatly enhances Movement Speed when out of combat","from":["1001"],"image":{"full":"3117.png","sprite":"item1.png","group":"item","x":0,"y":192,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":900,"sell":630},"tags":["Boots"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":115},"effect":{"Effect1Amount":"0","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"0","Effect5Amount":"0","Effect6Amount":"0","Effect7Amount":"0","Effect8Amount":"25"},"depth":2,"id":3117},"3122":{"name":"Wicked Hatchet","description":"+20 Attack Damage
+10% Critical Strike Chance


UNIQUE Passive: Critical Strikes cause your target to bleed for an additional 60% of your bonus Attack Damage as magic damage over 3 seconds.","colloq":";ie","plaintext":"Critical Strikes cause your target to bleed","from":["1051","1036"],"into":["3104","3185"],"image":{"full":"3122.png","sprite":"item1.png","group":"item","x":48,"y":192,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":1200,"sell":840},"tags":["CriticalStrike","Damage","OnHit"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":20,"FlatCritChanceMod":0.1},"effect":{"Effect1Amount":"0.6","Effect2Amount":"3"},"depth":2,"id":3122},"3123":{"name":"Executioner's Calling","description":"+15 Attack Damage

UNIQUE Passive - Executioner: Physical damage inflicts Grievous Wounds on enemy champions for 3 seconds.","colloq":";grievous","plaintext":"Overcomes enemies with high health gain","from":["1036"],"into":["3033"],"image":{"full":"3123.png","sprite":"item1.png","group":"item","x":96,"y":192,"w":48,"h":48},"gold":{"base":450,"purchasable":true,"total":800,"sell":560},"tags":["Damage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":15},"effect":{"Effect1Amount":"3"},"depth":2,"id":3123},"3124":{"name":"Guinsoo's Rageblade","description":"+35 Attack Damage
+50 Ability Power
+25% Attack Speed


Passive: Basic attacks deal an additional 15 magic damage on hit.
UNIQUE Passive: Basic attacks grant +8% Attack Speed, +3 Attack Damage, and +4 Ability Power for 5 seconds (stacks up to 6 times). While you have 6 stacks, gain Guinsoo's Rage.

Guinsoo's Rage: Every other basic attack will trigger on hit effects an additional time.","colloq":";","plaintext":"Increases Ability Power and Attack Damage","from":["1026","1043","1037"],"image":{"full":"3124.png","sprite":"item1.png","group":"item","x":144,"y":192,"w":48,"h":48},"gold":{"base":875,"purchasable":true,"total":3600,"sell":2520},"tags":["Damage","AttackSpeed","SpellDamage","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":35,"FlatMagicDamageMod":50,"PercentAttackSpeedMod":0.25},"effect":{"Effect1Amount":"0.08","Effect2Amount":"4","Effect3Amount":"3","Effect4Amount":"5","Effect5Amount":"6","Effect6Amount":"0","Effect7Amount":"0","Effect8Amount":"15","Effect9Amount":"1"},"depth":3,"id":3124},"3133":{"name":"Caulfield's Warhammer","description":"+25 Attack Damage

UNIQUE Passive: +10% Cooldown Reduction","colloq":";","plaintext":"Attack Damage and Cooldown Reduction","stacks":0,"from":["1036","1036"],"into":["3142","1400","3104","1408","1412","3812","3156","3508","3671"],"image":{"full":"3133.png","sprite":"item1.png","group":"item","x":192,"y":192,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":1100,"sell":770},"tags":["Damage","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25},"effect":{"Effect1Amount":"-0.1"},"depth":2,"id":3133},"3134":{"name":"Serrated Dirk","description":"+25 Attack Damage

UNIQUE Passive: +10 Lethality
UNIQUE Passive: +20 Movement Speed out of Combat.","colloq":";lethality","plaintext":"Increases Attack Damage and Lethality","stacks":0,"from":["1036","1036"],"into":["3142","3814","3147"],"image":{"full":"3134.png","sprite":"item1.png","group":"item","x":240,"y":192,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":1100,"sell":770},"tags":["Damage","NonbootsMovement","ArmorPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25},"effect":{"Effect1Amount":"0","Effect2Amount":"10","Effect3Amount":"20"},"depth":2,"id":3134},"3135":{"name":"Void Staff","description":"+80 Ability Power

UNIQUE Passive: +35% Magic Penetration.","colloq":";","plaintext":"Increases magic damage","from":["1026","1052"],"image":{"full":"3135.png","sprite":"item1.png","group":"item","x":288,"y":192,"w":48,"h":48},"gold":{"base":1365,"purchasable":true,"total":2650,"sell":1855},"tags":["MagicPenetration","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.35"},"depth":2,"id":3135},"3136":{"name":"Haunting Guise","description":"+25 Ability Power
+200 Health


UNIQUE Passive - Eyes of Pain: +15 Magic Penetration","colloq":";mask","plaintext":"Increases magic damage","stacks":0,"from":["1028","1052"],"into":["3151"],"image":{"full":"3136.png","sprite":"item1.png","group":"item","x":336,"y":192,"w":48,"h":48},"gold":{"base":665,"purchasable":true,"total":1500,"sell":1050},"tags":["Health","MagicPenetration","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":200,"FlatMagicDamageMod":25},"effect":{"Effect1Amount":"15"},"depth":2,"id":3136},"3137":{"name":"Dervish Blade","description":"+50% Attack Speed
+45 Magic Resist
+10% Cooldown Reduction


UNIQUE Active - Quicksilver: Removes all debuffs, and if the champion is melee, also grants +50% bonus Movement Speed for 1 second (90 second cooldown).","colloq":";","plaintext":"Activate to remove all debuffs and grant massive Movement Speed","from":["3140","3101"],"image":{"full":"3137.png","sprite":"item1.png","group":"item","x":384,"y":192,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":2700,"sell":1890},"tags":["Active","AttackSpeed","CooldownReduction","NonbootsMovement","SpellBlock"],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatSpellBlockMod":45,"PercentAttackSpeedMod":0.5},"effect":{"Effect1Amount":"0.5","Effect2Amount":"1","Effect3Amount":"90"},"depth":3,"id":3137},"3139":{"name":"Mercurial Scimitar","description":"+65 Attack Damage
+35 Magic Resist
+10% Life Steal


UNIQUE Active - Quicksilver: Removes all crowd control debuffs and also grants +50% bonus Movement Speed for 1 second (90 second cooldown).","colloq":";","plaintext":"Activate to remove all crowd control debuffs and grant massive Movement Speed","from":["3140","1037","1053"],"image":{"full":"3139.png","sprite":"item1.png","group":"item","x":432,"y":192,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":3600,"sell":2520},"tags":["SpellBlock","Damage","LifeSteal","Active","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":65,"FlatSpellBlockMod":35,"PercentLifeStealMod":0.1},"effect":{"Effect1Amount":"0.5","Effect2Amount":"1","Effect3Amount":"90"},"depth":3,"id":3139},"3140":{"name":"Quicksilver Sash","description":"+30 Magic Resist

UNIQUE Active - Quicksilver: Removes all crowd control debuffs (90 second cooldown).","colloq":";qss","plaintext":"Activate to remove all crowd control debuffs","from":["1033"],"into":["3139","3137"],"image":{"full":"3140.png","sprite":"item1.png","group":"item","x":0,"y":240,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":1300,"sell":910},"tags":["Active","SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":30},"effect":{"Effect1Amount":"90"},"depth":2,"id":3140},"3142":{"name":"Youmuu's Ghostblade","description":"+60 Attack Damage
+10% Cooldown Reduction


UNIQUE Passive: +15 Lethality
UNIQUE Passive: +20 Movement Speed out of Combat
UNIQUE Active: Grants +20% Movement Speed for 6 seconds (45 second cooldown).","colloq":";lethality","plaintext":"Activate to greatly increase Movement Speed","from":["3133","3134"],"image":{"full":"3142.png","sprite":"item1.png","group":"item","x":48,"y":240,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":2900,"sell":2030},"tags":["Damage","Active","CooldownReduction","NonbootsMovement","ArmorPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":60},"effect":{"Effect1Amount":"45","Effect2Amount":"15","Effect3Amount":"0.2","Effect4Amount":"0","Effect5Amount":"6","Effect6Amount":"20"},"depth":3,"id":3142},"3143":{"name":"Randuin's Omen","description":"+350 Health
+60 Armor


UNIQUE Passive: -20% damage taken from basic attack critical strikes.
UNIQUE Passive - Cold Steel: When hit by basic attacks, reduces the attacker's Attack Speed by 15% for 1 second.
UNIQUE Active: Slows the Movement Speed of nearby enemy units by 55% for 2 seconds (60 second cooldown).","colloq":";","plaintext":"Greatly increases defenses, activate to slow nearby enemies","from":["1028","3082","1028"],"image":{"full":"3143.png","sprite":"item1.png","group":"item","x":96,"y":240,"w":48,"h":48},"gold":{"base":1100,"purchasable":true,"total":2900,"sell":2030},"tags":["Active","Armor","Health","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":350,"FlatArmorMod":60},"effect":{"Effect1Amount":"2","Effect2Amount":"0.2","Effect3Amount":"-0.55","Effect4Amount":"1","Effect5Amount":"-0.15","Effect6Amount":"0.15","Effect7Amount":"0.3"},"depth":3,"id":3143},"3144":{"name":"Bilgewater Cutlass","description":"+25 Attack Damage
+10% Life Steal


UNIQUE Active: Deals 100 magic damage and slows the target champion's Movement Speed by 25% for 2 seconds (90 second cooldown).","colloq":";","plaintext":"Activate to deal magic damage and slow target champion","from":["1053","1036"],"into":["3146","3153"],"image":{"full":"3144.png","sprite":"item1.png","group":"item","x":144,"y":240,"w":48,"h":48},"gold":{"base":250,"purchasable":true,"total":1500,"sell":1050},"tags":["Active","Damage","LifeSteal","Slow"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":25,"PercentLifeStealMod":0.1},"effect":{"Effect1Amount":"-0.25","Effect2Amount":"2","Effect3Amount":"90","Effect4Amount":"100"},"depth":3,"id":3144},"3145":{"name":"Hextech Revolver","description":"+40 Ability Power

UNIQUE Passive - Magic Bolt: Damaging an enemy champion with a basic attack shocks them for 50 - 125 bonus magic damage (40 second cooldown, shared with other Hextech items).

Magic Bolt's cooldown is reduced by Active Item cooldown reduction.

(Damage scales based on level. Hextech effects can trigger other item spell effects.)","colloq":";","plaintext":"Increases Ability Power. Deal bonus magic damage on attack periodically.","from":["1052","1052"],"into":["3146","3152","3030"],"image":{"full":"3145.png","sprite":"item1.png","group":"item","x":192,"y":240,"w":48,"h":48},"gold":{"base":180,"purchasable":true,"total":1050,"sell":735},"tags":["SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":40},"effect":{"Effect1Amount":"0","Effect2Amount":"50","Effect3Amount":"0","Effect4Amount":"125","Effect5Amount":"40"},"depth":2,"id":3145},"3146":{"name":"Hextech Gunblade","description":"+40 Attack Damage
+80 Ability Power


UNIQUE Passive: Heal for 15% of damage dealt. This is 33% as effective for Area of Effect damage.
UNIQUE Active - Lightning Bolt: Deals 175 - 250 (+30% of Ability Power) magic damage and slows the target champion's Movement Speed by 40% for 2 seconds (40 second cooldown, shared with other Hextech items).","colloq":";","plaintext":"Increases Attack Damage and Ability Power, activate to slow a target","from":["3144","3145"],"image":{"full":"3146.png","sprite":"item1.png","group":"item","x":240,"y":240,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":3400,"sell":2380},"tags":["Active","Damage","LifeSteal","Slow","SpellDamage","SpellVamp"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":40,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"0.15","Effect2Amount":"40","Effect3Amount":"175","Effect4Amount":"250"},"depth":4,"id":3146},"3147":{"name":"Duskblade of Draktharr","description":"+65 Attack Damage

UNIQUE Passive: +15 Lethality
UNIQUE Passive: +20 Movement Speed out of Combat.
UNIQUE Passive - Nightstalker: After being unseen for at least 1 second, your next Basic Attack against an enemy champion will deal 75 (+200% Lethality) true damage on-hit (lasts for 4 seconds after being seen by an enemy champion).
UNIQUE Passive - Blackout: When spotted by an enemy ward, causes a blackout for 8 seconds, revealing invisible traps and revealing / disabling wards (90 second cooldown).","colloq":";lethality","plaintext":"Deals additional true damage on-hit and provides true sight periodically","from":["3134","1038"],"image":{"full":"3147.png","sprite":"item1.png","group":"item","x":288,"y":240,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":3250,"sell":2275},"tags":["Damage","OnHit","NonbootsMovement","ArmorPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":65},"effect":{"Effect1Amount":"15","Effect2Amount":"20","Effect3Amount":"0","Effect4Amount":"90","Effect5Amount":"75","Effect6Amount":"2"},"depth":3,"id":3147},"3151":{"name":"Liandry's Torment","description":"+80 Ability Power
+300 Health


UNIQUE Passive - Eyes of Pain: +15 Magic Penetration
UNIQUE Passive: Spells burn enemies for 3 seconds, dealing bonus magic damage equal to 2% of their current Health per second. Burn damage is doubled against movement-impaired units.","colloq":";mask","plaintext":"Spell damage burns enemies for a portion of their Health","stacks":0,"from":["3136","1026"],"image":{"full":"3151.png","sprite":"item1.png","group":"item","x":336,"y":240,"w":48,"h":48},"gold":{"base":750,"purchasable":true,"total":3100,"sell":2170},"tags":["Health","MagicPenetration","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMagicDamageMod":80},"effect":{"Effect1Amount":"15","Effect2Amount":"0.02","Effect3Amount":"3","Effect4Amount":"100","Effect5Amount":"2"},"depth":3,"id":3151},"3152":{"name":"Hextech Protobelt-01","description":"+300 Health
+60 Ability Power
+10% Cooldown Reduction


UNIQUE Active - Fire Bolt: Dash forward and unleash a nova of fire bolts that deal 75 - 150 (+25% of your Ability Power) as magic damage (40 second cooldown, shared with other Hextech items).

Champions and Monsters hit by multiple fire bolts take 10% damage per additional bolt.

(This dash cannot pass through terrain.)","colloq":"rocket belt;","plaintext":"Activate to dash forward and unleash a fiery explosion","from":["3145","3067"],"image":{"full":"3152.png","sprite":"item1.png","group":"item","x":384,"y":240,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2500,"sell":1750},"tags":["Health","SpellDamage","Active","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"0.12","Effect2Amount":"0.04","Effect3Amount":"40","Effect4Amount":"75","Effect5Amount":"150","Effect6Amount":"0.1","Effect7Amount":"0.25","Effect8Amount":"40","Effect9Amount":"0.5"},"depth":3,"id":3152},"3153":{"name":"Blade of the Ruined King","description":"+40 Attack Damage
+25% Attack Speed
+12% Life Steal


UNIQUE Passive: Basic attacks deal 8% of the target's current Health as bonus physical damage on hit.
UNIQUE Active: Deals 100 magic damage to target champion and steals 25% of their Movement Speed for 3 seconds (90 second cooldown).

Minimum bonus physical damage dealt is 15.
Maximum bonus physical damage dealt to monsters and minions is 60.
User's Life Steal is applied to bonus physical damage dealt.
","colloq":";brk;bork;bork;bork;botrk","plaintext":"Deals damage based on target's Health, can steal Movement Speed","from":["3144","1043"],"image":{"full":"3153.png","sprite":"item1.png","group":"item","x":432,"y":240,"w":48,"h":48},"gold":{"base":900,"purchasable":true,"total":3400,"sell":2380},"tags":["Active","AttackSpeed","Damage","LifeSteal","NonbootsMovement","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":40,"PercentAttackSpeedMod":0.25,"PercentLifeStealMod":0.12},"effect":{"Effect1Amount":"0.08"},"depth":4,"id":3153},"3155":{"name":"Hexdrinker","description":"+20 Attack Damage
+35 Magic Resist


UNIQUE Passive - Lifeline: Upon taking magic damage that would reduce Health below 30%, grants a shield that absorbs 110 to 280 (based on level) magic damage for 5 seconds (90 second cooldown).","colloq":";","plaintext":"Increases Attack Damage and Magic Resist","stacks":0,"from":["1036","1033"],"into":["3156"],"image":{"full":"3155.png","sprite":"item1.png","group":"item","x":0,"y":288,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":1300,"sell":910},"tags":["Damage","SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":20,"FlatSpellBlockMod":35},"effect":{"Effect1Amount":"0.3","Effect2Amount":"110","Effect3Amount":"5","Effect4Amount":"90","Effect5Amount":"280","Effect6Amount":"100","Effect7Amount":"10"},"depth":2,"id":3155},"3156":{"name":"Maw of Malmortius","description":"+50 Attack Damage
+45 Magic Resist
+10% Cooldown Reduction


UNIQUE Passive - Lifeline: Upon taking magic damage that would reduce Health below 30%, grants a shield that absorbs magic damage equal to 300 + 1 per bonus Magic Resistance for 5 seconds (90 second cooldown).
Lifegrip: When Lifeline triggers, gain +20 Attack Damage, +10% Spell Vamp and +10% Life Steal until out of combat.","colloq":";","plaintext":"Grants bonus Attack Damage when Health is low","stacks":0,"from":["3155","3133"],"image":{"full":"3156.png","sprite":"item1.png","group":"item","x":48,"y":288,"w":48,"h":48},"gold":{"base":850,"purchasable":true,"total":3250,"sell":2275},"tags":["SpellBlock","Damage","LifeSteal","CooldownReduction","SpellVamp"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":50,"FlatSpellBlockMod":45},"effect":{"Effect1Amount":"1","Effect2Amount":"35","Effect3Amount":"0.3","Effect4Amount":"300","Effect5Amount":"5","Effect6Amount":"90","Effect7Amount":"20","Effect8Amount":"0","Effect9Amount":"0.1","Effect10Amount":"0.1"},"depth":3,"id":3156},"3157":{"name":"Zhonya's Hourglass","description":"+70 Ability Power
+45 Armor
+10% Cooldown Reduction


UNIQUE Active - Stasis: Champion becomes invulnerable and untargetable for 2.5 seconds, but is unable to move, attack, cast spells, or use items during this time (120 second cooldown).","colloq":";zhg;zonyas","plaintext":"Activate to become invincible but unable to take actions","from":["3191","3108"],"image":{"full":"3157.png","sprite":"item1.png","group":"item","x":96,"y":288,"w":48,"h":48},"gold":{"base":800,"purchasable":true,"total":2900,"sell":2030},"tags":["Armor","SpellDamage","Active","CooldownReduction"],"maps":{"8":true,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":70,"FlatArmorMod":45},"effect":{"Effect1Amount":"0","Effect2Amount":"2.5","Effect3Amount":"120"},"depth":3,"id":3157},"3158":{"name":"Ionian Boots of Lucidity","description":"UNIQUE Passive: +10% Cooldown Reduction
UNIQUE Passive - Enhanced Movement: +45 Movement Speed
UNIQUE Passive: Reduces Summoner Spell cooldowns by 10%


''This item is dedicated in honor of Ionia's victory over Noxus in the Rematch for the Southern Provinces on 10 December, 20 CLE.''","colloq":";","plaintext":"Increases Movement Speed and Cooldown Reduction","from":["1001"],"image":{"full":"3158.png","sprite":"item1.png","group":"item","x":144,"y":288,"w":48,"h":48},"gold":{"base":600,"purchasable":true,"total":900,"sell":630},"tags":["Boots","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":45},"effect":{"Effect1Amount":"-0.1","Effect2Amount":"0.1"},"depth":2,"id":3158},"3165":{"name":"Morellonomicon","description":"+100 Ability Power
+400 Mana


UNIQUE Passive: +20% Cooldown Reduction
UNIQUE Passive: Dealing magic damage to champions below 35% Health inflicts Grievous Wounds for 8 seconds.
UNIQUE Passive: Kills and Assists restore 20% of your maximum Mana.","colloq":";nmst;grievous","plaintext":"Greatly increases Ability Power and Cooldown Reduction","from":["3108","1052","3802"],"image":{"full":"3165.png","sprite":"item1.png","group":"item","x":192,"y":288,"w":48,"h":48},"gold":{"base":665,"purchasable":true,"total":2900,"sell":2030},"tags":["SpellDamage","Mana","ManaRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":400,"FlatMagicDamageMod":100},"effect":{"Effect1Amount":"20","Effect2Amount":"35","Effect3Amount":"8","Effect4Amount":"0.2"},"depth":3,"id":3165},"3170":{"name":"Moonflair Spellblade","description":"+50 Ability Power
+50 Armor
+50 Magic Resist


UNIQUE Passive - Tenacity: Reduces the duration of stuns, slows, taunts, fears, silences, blinds, polymorphs, and immobilizes by 35%.","colloq":";","plaintext":"Improves defense and reduces duration of disabling effects","from":["3191","1057"],"image":{"full":"3170.png","sprite":"item1.png","group":"item","x":240,"y":288,"w":48,"h":48},"gold":{"base":580,"purchasable":true,"total":2500,"sell":1750},"tags":["Armor","SpellBlock","SpellDamage","Tenacity"],"maps":{"8":true,"10":true,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatSpellBlockMod":50,"FlatMagicDamageMod":50,"FlatArmorMod":50},"depth":3,"id":3170},"3174":{"name":"Athene's Unholy Grail","description":"+40 Ability Power
+30 Magic Resist
+20% Cooldown Reduction
+75% Base Mana Regen


UNIQUE Passive: Gain 20% of the premitigation damage dealt to champions as Blood Charges, up to 100 - 250 max. Healing or shielding another ally consumes charges to heal them, up to the original effect amount.
UNIQUE Passive - Harmony: Grants bonus % Base Health Regen equal to your bonus % Base Mana Regen.

(Maximum amount of Blood Charges stored is based on level. Healing amplification is applied to the total heal value.)","colloq":";","plaintext":"Deal damage to empower your heals and shields","from":["3108","3028"],"image":{"full":"3174.png","sprite":"item1.png","group":"item","x":288,"y":288,"w":48,"h":48},"gold":{"base":400,"purchasable":true,"total":2100,"sell":1470},"tags":["SpellBlock","HealthRegen","SpellDamage","ManaRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":30,"FlatMagicDamageMod":40},"effect":{"Effect1Amount":"20","Effect2Amount":"30","Effect3Amount":"2","Effect4Amount":"5","Effect5Amount":"100","Effect6Amount":"0.2","Effect7Amount":"100","Effect8Amount":"250","Effect9Amount":"1","Effect10Amount":"1","Effect11Amount":"0.25","Effect12Amount":"600","Effect13Amount":"8","Effect14Amount":"90"},"depth":3,"id":3174},"3175":{"name":"Head of Kha'Zix","description":"UNIQUE Active - Bonetooth Totem: Places a Stealth Ward that lasts 180 seconds (90 Second cooldown). Limit 3 Stealth Wards on the map per player.

UNIQUE Passive - Mementos of the Hunt: Rengar collects trophies when killing Champions and gains bonus effects based on how many trophies he has. Kills and assists grant 1 trophy.

3 Trophies: Rengar gains 25 Movement Speed whilst out of combat or in brush.
6 Trophies: Increases the range of Rengar's Leap by 125.
12 Trophies: Thrill of the Hunt's duration is increased by 5 seconds.
20 Trophies: Rengar gains the movement speed bonus of Thrill of the Hunt while he is stealthed.","colloq":"","plaintext":"","specialRecipe":3169,"inStore":false,"requiredChampion":"Rengar","hideFromAll":true,"image":{"full":"3175.png","sprite":"item1.png","group":"item","x":336,"y":288,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"125","Effect3Amount":"5","Effect4Amount":"180","Effect5Amount":"90"},"id":3175},"3181":{"name":"Sanguine Blade","description":"+45 Attack Damage
+10% Life Steal


UNIQUE Passive: Basic attacks grant +6 Attack Damage and +1% Life Steal for 8 seconds on hit (effect stacks up to 5 times).","colloq":";","plaintext":"Greatly increases Attack Damage and Life Steal","from":["1037","1053"],"image":{"full":"3181.png","sprite":"item1.png","group":"item","x":384,"y":288,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":2400,"sell":1680},"tags":["Damage","LifeSteal"],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":45,"PercentLifeStealMod":0.1},"effect":{"Effect1Amount":"6","Effect2Amount":"0.01","Effect3Amount":"8","Effect4Amount":"5"},"depth":3,"id":3181},"3184":{"name":"Guardian's Hammer","description":"+150 Health
+20 Attack Damage
+10% Life Steal


Limited to 1 Guardian's Item.","colloq":";dblade","plaintext":"Good starting item for attackers","image":{"full":"3184.png","sprite":"item1.png","group":"item","x":432,"y":288,"w":48,"h":48},"gold":{"base":950,"purchasable":true,"total":950,"sell":380},"tags":["Health","Damage","LifeSteal","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":20,"FlatHPPoolMod":150,"PercentLifeStealMod":0.1},"id":3184},"3185":{"name":"The Lightbringer","description":"+30 Attack Damage
+30% Critical Strike Chance


UNIQUE Passive: Critical Strikes cause enemies to bleed for an additional 90% of bonus Attack Damage as magic damage over 3 seconds and reveal them for the duration.
UNIQUE Passive - Trap Detection: Nearby stealthed enemy traps are revealed.
UNIQUE Active - Hunter's Sight: A stealth-detecting mist grants vision in the target area for 5 seconds, revealing enemy champions that enter for 3 seconds (60 second cooldown).","colloq":";lb","plaintext":"Critical Strikes cause your target to bleed and be revealed","from":["3122","1018"],"image":{"full":"3185.png","sprite":"item1.png","group":"item","x":0,"y":336,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":2350,"sell":1645},"tags":["Active","CriticalStrike","Damage","OnHit","Stealth","Vision"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":30,"FlatCritChanceMod":0.3},"effect":{"Effect1Amount":"0.9","Effect2Amount":"3","Effect3Amount":"0","Effect4Amount":"5","Effect5Amount":"3","Effect6Amount":"60"},"depth":3,"id":3185},"3187":{"name":"Arcane Sweeper","description":"+225 Health
+250 Mana
+25 Armor
+20% Cooldown Reduction


UNIQUE Passive - Trap Detection: Grants True Sight of nearby enemy traps.
UNIQUE Active - Hunter's Sight: An arcane mist grants vision in the target area for 5 seconds, revealing enemy champions in the area for 3 seconds (60 second cooldown).","colloq":";","plaintext":"Activate to reveal a nearby area of the map","from":["3024","3067"],"image":{"full":"3187.png","sprite":"item1.png","group":"item","x":48,"y":336,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":2150,"sell":1505},"tags":["Active","Armor","CooldownReduction","Health","Mana","Stealth","Vision"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatHPPoolMod":225,"FlatMPPoolMod":250,"FlatArmorMod":25},"effect":{"Effect1Amount":"-0.2","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"5","Effect5Amount":"3","Effect6Amount":"60"},"depth":3,"id":3187},"3190":{"name":"Locket of the Iron Solari","description":"+30 Armor
+60 Magic Resist


UNIQUE Active: Grants a decaying shield to nearby allies for 2.5 seconds that absorbs up to 35 (+35 per level) damage (90 second cooldown).

(Half effect if the target has been affected by another Locket of the Iron Solari recently.)","colloq":";","plaintext":"Activate to shield nearby allies from damage","from":["3105","1033"],"image":{"full":"3190.png","sprite":"item1.png","group":"item","x":96,"y":336,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2200,"sell":1540},"tags":["SpellBlock","Armor","Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":60,"FlatArmorMod":30},"effect":{"Effect1Amount":"0","Effect2Amount":"0.75","Effect3Amount":"2.5","Effect4Amount":"35","Effect5Amount":"35","Effect6Amount":"90"},"depth":3,"id":3190},"3191":{"name":"Seeker's Armguard","description":"+30 Armor
+20 Ability Power


UNIQUE Passive: Killing a unit grants 0.5 bonus Armor and Ability Power. This bonus stacks up to 30 times.","colloq":";","plaintext":"Increases Armor and Ability Power","from":["1029","1052","1029"],"into":["3090","3170","3157"],"image":{"full":"3191.png","sprite":"item1.png","group":"item","x":144,"y":336,"w":48,"h":48},"gold":{"base":165,"purchasable":true,"total":1200,"sell":840},"tags":["Armor","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":20,"FlatArmorMod":30},"effect":{"Effect1Amount":"0.5","Effect2Amount":"30"},"depth":2,"id":3191},"3193":{"name":"Gargoyle Stoneplate","description":"+40 Armor
+40 Magic Resist


UNIQUE Passive - Stone Skin: If 3+ enemy champions are nearby, grants 40 bonus Armor and Magic Resist.
UNIQUE Active - Metallicize: Increases Health by 40% and increases champion size, but reduces damage dealt by 60% for 4 seconds (90 second cooldown). If Stone Skin is active, the Health increase becomes 100% instead.","colloq":";","plaintext":"Greatly increases defense near multiple enemies.","from":["1031","1057"],"image":{"full":"3193.png","sprite":"item1.png","group":"item","x":192,"y":336,"w":48,"h":48},"gold":{"base":980,"purchasable":true,"total":2500,"sell":1750},"tags":["Health","SpellBlock","Armor"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":40,"FlatArmorMod":40},"effect":{"Effect1Amount":"40","Effect2Amount":"850","Effect3Amount":"3","Effect4Amount":"4","Effect5Amount":"0.6","Effect6Amount":"0.4","Effect7Amount":"0.07","Effect8Amount":"1","Effect9Amount":"0.25","Effect10Amount":"90","Effect11Amount":"1"},"depth":3,"id":3193},"3194":{"name":"Adaptive Helm","description":"+300 Health
+55 Magic Resist
+100% Base Health Regeneration
+10% Cooldown Reduction


UNIQUE Passive: Taking magic damage from a spell or effect reduces all subsequent magic damage from that same spell or effect by 15% for 4 seconds.","colloq":";","plaintext":"Reduces damage from repeated spells and effects.","from":["1033","3211","1006"],"image":{"full":"3194.png","sprite":"item1.png","group":"item","x":240,"y":336,"w":48,"h":48},"gold":{"base":1000,"purchasable":true,"total":2800,"sell":1960},"tags":["Health","SpellBlock","HealthRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":300,"FlatSpellBlockMod":55},"effect":{"Effect1Amount":"15","Effect2Amount":"4","Effect3Amount":"25"},"depth":3,"id":3194},"3196":{"name":"The Hex Core mk-1","description":"+3 Ability Power per level
+15 Mana per level


UNIQUE Passive - Progress: Viktor can upgrade one of his basic spells.","colloq":";viktor","plaintext":"Allows Viktor to improve an ability of his choice","from":["3200"],"requiredChampion":"Viktor","into":["3197"],"image":{"full":"3196.png","sprite":"item1.png","group":"item","x":288,"y":336,"w":48,"h":48},"gold":{"base":1250,"purchasable":true,"total":1250,"sell":875},"tags":["Mana","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"3","Effect2Amount":"15"},"depth":2,"id":3196},"3197":{"name":"The Hex Core mk-2","description":"+6 Ability Power per level
+20 Mana per level


UNIQUE Passive - Progress: Viktor can upgrade one of his basic spells.","colloq":";viktor","plaintext":"Allows Viktor to improve an ability of his choice","from":["3196"],"requiredChampion":"Viktor","into":["3198"],"image":{"full":"3197.png","sprite":"item1.png","group":"item","x":336,"y":336,"w":48,"h":48},"gold":{"base":1000,"purchasable":true,"total":2250,"sell":1575},"tags":["Mana","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"6","Effect2Amount":"20"},"depth":3,"id":3197},"3198":{"name":"Perfect Hex Core","description":"+10 Ability Power per level
+25 Mana per level


UNIQUE Passive - Glorious Evolution: Viktor has reached the pinnacle of his power, upgrading Chaos Storm in addition to his basic spells.","colloq":";viktor","plaintext":"Allows Viktor to improve an ability of his choice","from":["3197"],"requiredChampion":"Viktor","image":{"full":"3198.png","sprite":"item1.png","group":"item","x":384,"y":336,"w":48,"h":48},"gold":{"base":750,"purchasable":true,"total":3000,"sell":2100},"tags":["Mana","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"10","Effect2Amount":"25"},"depth":4,"id":3198},"3200":{"name":"Prototype Hex Core","description":"+1 Ability Power per level
+10 Mana per level


UNIQUE Passive - Progress: This item can be upgraded three times to enhance Viktor's basic abilities.","colloq":";viktor","plaintext":"Increases Ability Power and can be upgraded to improve Viktor's abilities","inStore":false,"requiredChampion":"Viktor","into":["3196"],"image":{"full":"3200.png","sprite":"item1.png","group":"item","x":432,"y":336,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1","Effect2Amount":"10"},"id":3200},"3211":{"name":"Spectre's Cowl","description":"+250 Health
+25 Magic Resist


UNIQUE Passive: Grants 150% Base Health Regen for up to 10 seconds after taking damage from an enemy champion.","colloq":";hat","plaintext":"Improves defense and grants regeneration upon being damaged","from":["1028","1033"],"into":["3065","3001","3194"],"image":{"full":"3211.png","sprite":"item1.png","group":"item","x":0,"y":384,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":1200,"sell":840},"tags":["Health","HealthRegen","SpellBlock"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":250,"FlatSpellBlockMod":25},"effect":{"Effect1Amount":"1.5","Effect2Amount":"10"},"depth":2,"id":3211},"3222":{"name":"Mikael's Crucible","description":"+40 Magic Resist
+10% Cooldown Reduction
+100% Base Mana Regen


UNIQUE Passive: +20% Heal and Shield Power
UNIQUE Passive - Harmony: Grants bonus % Base Health Regen equal to your bonus % Base Mana Regen.
UNIQUE Active: Cleanses all stuns, roots, taunts, fears, silences, and slows on an allied champion and grants them slow immunity for 2 seconds (120 second cooldown).

Cleansing an effect grants the ally 40% movement speed for 2 seconds.","colloq":";","plaintext":"Activate to remove all disabling effects from an allied champion","from":["3028","3114"],"image":{"full":"3222.png","sprite":"item1.png","group":"item","x":48,"y":384,"w":48,"h":48},"gold":{"base":500,"purchasable":true,"total":2100,"sell":1470},"tags":["SpellBlock","HealthRegen","ManaRegen","Active","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatSpellBlockMod":40},"effect":{"Effect1Amount":"2","Effect2Amount":"5","Effect3Amount":"0.5","Effect4Amount":"0.25","Effect5Amount":"600","Effect6Amount":"8","Effect7Amount":"90","Effect8Amount":"1","Effect9Amount":"1","Effect10Amount":"0.4","Effect11Amount":"0.2","Effect12Amount":"2","Effect13Amount":"120"},"depth":3,"id":3222},"3252":{"name":"Poacher's Dirk","description":"+10 Attack Damage

UNIQUE Passive: +20 Movement Speed out of Combat
UNIQUE Passive: After poaching 3 large monsters from the enemy jungle (50 second cooldown), transforms into a Serrated Dirk.","colloq":";serrated dirk;lethality","plaintext":"Transforms into a Serrated Dirk after poaching in the enemy jungle.","stacks":0,"from":["1036"],"image":{"full":"3252.png","sprite":"item1.png","group":"item","x":96,"y":384,"w":48,"h":48},"gold":{"base":250,"purchasable":true,"total":600,"sell":420},"tags":["Damage","NonbootsMovement","ArmorPenetration"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":10},"effect":{"Effect1Amount":"100","Effect2Amount":"15","Effect3Amount":"20","Effect4Amount":"50"},"depth":2,"id":3252},"3285":{"name":"Luden's Echo","description":"+100 Ability Power
+10% Movement Speed


UNIQUE Passive - Echo: Gain charges upon moving or casting. At 100 charges, the next damaging spell hit expends all charges to deal 100 (+10% of Ability Power) bonus magic damage to up to 4 targets on hit.","colloq":";","plaintext":"Movement and casting builds charges that release chain lightning on next spell hit","from":["1058","3113"],"image":{"full":"3285.png","sprite":"item1.png","group":"item","x":144,"y":384,"w":48,"h":48},"gold":{"base":1100,"purchasable":true,"total":3200,"sell":2240},"tags":["NonbootsMovement","OnHit","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.1,"FlatMagicDamageMod":100},"effect":{"Effect1Amount":"100","Effect2Amount":"100","Effect3Amount":"4","Effect4Amount":"0.1","Effect5Amount":"35","Effect6Amount":"10"},"depth":3,"id":3285},"3301":{"name":"Ancient Coin","description":"+5% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Favor: Enemy minions killed by your allies sometimes drop coins that give either 20 gold or 8% missing mana (minimum 15). Cannon minions always drop coins.
QUEST: Earn 650 gold using this item and upgrade to Nomad's Medallion.
REWARD: Favor is upgraded to Emperor's Favor and you receive an Elixir Of Skill.

Limited to 1 Gold Income Item.

''Gold dust rises from the desert and clings to the coin.'' - Historian Shurelya, 11 November, 23 CLE","colloq":";","plaintext":"Grants gold and mana when nearby minions die that you didn't kill","into":["3096"],"image":{"full":"3301.png","sprite":"item1.png","group":"item","x":192,"y":384,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":140},"tags":["ManaRegen","GoldPer","CooldownReduction","Lane"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"10","Effect2Amount":"13","Effect3Amount":"2"},"id":3301},"3302":{"name":"Relic Shield","description":"+75 Health
+2 Gold per 10 seconds


UNIQUE Passive - Spoils of War: Melee basic attacks execute minions below 195 (+5 per level) Health. Killing a minion heals the owner and the nearest allied champion for 15 Health and grants them kill Gold. These effects require a nearby ally. Recharges every 40 seconds. Max 2 charges.
QUEST: Earn 650 gold using this item and upgrade to Targon's Brace.
REWARD: Shield Battery, a permanent shield that regenerates slowly outside of combat.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Kill minions periodically to heal and grant gold to a nearby ally","into":["3097"],"image":{"full":"3302.png","sprite":"item1.png","group":"item","x":240,"y":384,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":140},"tags":["Aura","GoldPer","Health","Lane"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":75},"effect":{"Effect1Amount":"195","Effect2Amount":"15","Effect3Amount":"2","Effect4Amount":"5"},"id":3302},"3303":{"name":"Spellthief's Edge","description":"+5 Ability Power
+2 Gold per 10 seconds
+25% Base Mana Regen


UNIQUE Passive - Tribute: Damaging spells and attacks against champions or buildings deal 10 additional damage and grant 8 Gold. This can occur up to 3 times every 30 seconds. Killing minions slows Tribute generation.
QUEST: Earn 650 gold using this item and upgrade to Frostfang.
REWARD: Tribute is upgraded into Queen's Tribute.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Grants gold when you damage enemies","into":["3098"],"image":{"full":"3303.png","sprite":"item1.png","group":"item","x":288,"y":384,"w":48,"h":48},"gold":{"base":350,"purchasable":true,"total":350,"sell":140},"tags":["GoldPer","Lane","ManaRegen","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":5},"effect":{"Effect1Amount":"10","Effect2Amount":"8","Effect3Amount":"12","Effect4Amount":"3","Effect5Amount":"30","Effect6Amount":"2"},"id":3303},"3340":{"name":"Warding Totem (Trinket)","description":"Limited to 1 Trinket.

Active: Consume a charge to place an invisible Stealth Ward which reveals the surrounding area for 60 - 120 seconds.

Stores one charge every 180 - 90 seconds, up to 2 maximum charges.

Ward duration and recharge time gradually improve with level.

(Limit 3 Stealth Wards on the map per player. Switching to a Lens type trinket will disable Trinket use for 120 seconds.)","colloq":"yellow;","plaintext":"Periodically place a Stealth Ward","image":{"full":"3340.png","sprite":"item1.png","group":"item","x":336,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Active","Jungle","Lane","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"60","Effect2Amount":"180","Effect3Amount":"120","Effect4Amount":"90","Effect5Amount":"2","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120"},"id":3340},"3341":{"name":"Sweeping Lens (Trinket)","description":"Limited to 1 Trinket.

Active: Scans an area for 6 seconds, warning against hidden hostile units and revealing invisible traps and revealing / disabling wards (90 to 60 second cooldown).

Cast range and sweep radius gradually improve with level.

(Switching to a Totem-type trinket will disable Trinket use for 120 seconds.)","colloq":"red;","plaintext":"Detects and disables nearby invisible wards and traps","image":{"full":"3341.png","sprite":"item1.png","group":"item","x":384,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Active","Jungle","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"6","Effect2Amount":"90","Effect3Amount":"400","Effect4Amount":"60","Effect5Amount":"1500","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120","Effect9Amount":"60","Effect10Amount":"450","Effect11Amount":"575"},"id":3341},"3345":{"name":"Soul Anchor (Trinket)","description":"Limited to 1 Trinket.

Active: Consumes a charge to instantly revive at your Summoner Platform and grants 125% Movement Speed that decays over 12 seconds.

Additional charges are gained at levels 9 and 14.

(Max: 2 charges)

","colloq":"","plaintext":"Consumes charge to revive champion.","inStore":false,"image":{"full":"3345.png","sprite":"item1.png","group":"item","x":432,"y":384,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3345},"3348":{"name":"Arcane Sweeper","description":"UNIQUE Active - Hunter's Sight: An arcane mist grants vision in the target area for 5 seconds, revealing enemy champions and granting True Sight of traps in the area for 3 seconds (90 second cooldown).","colloq":";","plaintext":"Activate to reveal a nearby area of the map","inStore":false,"image":{"full":"3348.png","sprite":"item1.png","group":"item","x":0,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Vision","Trinket","Stealth","Active"],"maps":{"8":true,"10":true,"11":false,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"0","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"5","Effect5Amount":"3","Effect6Amount":"90"},"id":3348},"3361":{"name":"Greater Stealth Totem (Trinket)","description":"Limited to 1 Trinket. *Level 9+ required to upgrade.

UNIQUE Active: Consume a charge to place an invisible ward that reveals the surrounding area for 180 seconds. Stores a charge every 60 seconds, up to 2 total. Limit 3 Stealth Wards on the map per player.

(Trinkets cannot be used in the first 30 seconds of a game. Selling a Trinket will disable Trinket use for 120 seconds).","colloq":"yellow;","plaintext":"Periodically place a Stealth Ward","inStore":false,"image":{"full":"3361.png","sprite":"item1.png","group":"item","x":48,"y":432,"w":48,"h":48},"gold":{"base":250,"purchasable":false,"total":250,"sell":175},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"180","Effect2Amount":"60","Effect3Amount":"0","Effect4Amount":"0","Effect5Amount":"0","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120"},"id":3361},"3362":{"name":"Greater Vision Totem (Trinket)","description":"Limited to 1 Trinket. *Level 9+ required to upgrade.

UNIQUE Active: Places a visible ward that reveals the surrounding area and invisible units in the area until killed (120 second cooldown). Limit 1 Vision Ward on the map per player.

(Trinkets cannot be used in the first 30 seconds of a game. Selling a Trinket will disable Trinket use for 120 seconds).","colloq":"yellow;","plaintext":"Periodically place a Vision Ward","inStore":false,"image":{"full":"3362.png","sprite":"item1.png","group":"item","x":96,"y":432,"w":48,"h":48},"gold":{"base":250,"purchasable":false,"total":250,"sell":175},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"120","Effect2Amount":"0","Effect3Amount":"0","Effect4Amount":"0","Effect5Amount":"0","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120"},"id":3362},"3363":{"name":"Farsight Alteration","description":"* Level 9+ required to upgrade.

Alters the Warding Totem Trinket:

+ Massively increased cast range (+650%)
+ Infinite duration and does not count towards ward limit
- 10% increased cooldown
- Ward is visible, fragile, untargetable by allies
- 45% reduced ward vision radius
- Cannot store charges
","colloq":"blue; totem;","plaintext":"Grants increased range and reveals the targetted area","image":{"full":"3363.png","sprite":"item1.png","group":"item","x":144,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"4000","Effect2Amount":"2","Effect3Amount":"5","Effect4Amount":"198","Effect5Amount":"60","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120","Effect9Amount":"6.5","Effect10Amount":"198","Effect11Amount":"99","Effect12Amount":"60","Effect13Amount":"180","Effect14Amount":"10","Effect15Amount":"45"},"id":3363},"3364":{"name":"Oracle Alteration","description":"* Level 9+ required to upgrade.

Alters the Sweeping Lens Trinket:

+ Increased detection radius
+ Sweeping effect follows you for 10 seconds
- Cast range reduced to zero
","colloq":"red; lens;","plaintext":"Disables nearby invisible wards and traps for a duration","image":{"full":"3364.png","sprite":"item1.png","group":"item","x":192,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"6","Effect2Amount":"10","Effect3Amount":"90","Effect4Amount":"60","Effect5Amount":"0","Effect6Amount":"9","Effect7Amount":"30","Effect8Amount":"120","Effect9Amount":"60"},"id":3364},"3401":{"name":"Face of the Mountain","description":"+450 Health
+100% Base Health Regen
+10% Cooldown Reduction
+2 Gold per 10 seconds


UNIQUE Passive - Spoils of War: Melee basic attacks execute minions below 320 (+20 per level) Health. Killing a minion heals the owner and the nearest allied champion for 50 Health and grants them kill Gold. These effects require a nearby ally. Recharges every 30 seconds. Max 4 charges.
UNIQUE Active: Grant a shield to you and an ally equal to 10% of your maximum Health for 4 seconds. After 4 seconds, the shields explode to slow nearby enemies by 40% for 2 seconds (60 second cooldown). Automatically targets the most wounded ally if cast upon self.
QUEST: Earn 650 gold using this item.
REWARD: Shield Battery, a permanent shield that regenerates slowly outside of combat.

Limited to 1 Gold Income Item.","colloq":";","plaintext":"Shield an ally from damage based on your Health","from":["3097","3067"],"image":{"full":"3401.png","sprite":"item1.png","group":"item","x":240,"y":432,"w":48,"h":48},"gold":{"base":550,"purchasable":true,"total":2200,"sell":880},"tags":["Health","HealthRegen","Active","GoldPer","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":450},"effect":{"Effect1Amount":"320","Effect2Amount":"50","Effect3Amount":"0.1","Effect4Amount":"4","Effect5Amount":"-0.4","Effect6Amount":"2","Effect7Amount":"60","Effect8Amount":"120","Effect9Amount":"2","Effect10Amount":"20"},"depth":3,"id":3401},"3410":{"name":"Head of Kha'Zix","description":"UNIQUE Active - Sweeping Lens: Reveals and disables nearby invisible traps and invisible wards for 6 seconds in a medium radius and grants detection of invisible units for 10 seconds (60 second cooldown).

UNIQUE Passive - Mementos of the Hunt: Rengar collects trophies when killing Champions and gains bonus effects based on how many trophies he has. Kills and assists grant 1 trophy.

3 Trophies: Rengar gains 25 Movement Speed whilst out of combat or in brush.
6 Trophies: Increases the range of Rengar's Leap by 125.
12 Trophies: Thrill of the Hunt's duration is increased by 5 seconds.
20 Trophies: Thrill of the Hunt's Movement Speed while stealthed is doubled.","colloq":"","plaintext":"","specialRecipe":3169,"inStore":false,"requiredChampion":"Rengar","hideFromAll":true,"image":{"full":"3410.png","sprite":"item1.png","group":"item","x":288,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"125","Effect3Amount":"5","Effect4Amount":"6","Effect5Amount":"10","Effect6Amount":"60"},"id":3410},"3416":{"name":"Head of Kha'Zix","description":"UNIQUE Active - Scrying: Reveals a small location within 4000 range for 2 seconds. Enemy champions found will be revealed for 5 seconds (90 second cooldown).

UNIQUE Passive - Mementos of the Hunt: Rengar collects trophies when killing Champions and gains bonus effects based on how many trophies he has. Kills and assists grant 1 trophy.

3 Trophies: Rengar gains 25 Movement Speed whilst out of combat or in brush.
6 Trophies: Increases the range of Rengar's Leap by 125.
12 Trophies: Thrill of the Hunt's duration is increased by 5 seconds.
20 Trophies: Thrill of the Hunt's Movement Speed while stealthed is doubled.","colloq":"","plaintext":"","specialRecipe":3169,"inStore":false,"requiredChampion":"Rengar","hideFromAll":true,"image":{"full":"3416.png","sprite":"item1.png","group":"item","x":336,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"125","Effect3Amount":"5","Effect4Amount":"4000","Effect5Amount":"2","Effect6Amount":"5","Effect7Amount":"90"},"id":3416},"3422":{"name":"Head of Kha'Zix","description":"UNIQUE Passive - Mementos of the Hunt: Rengar collects trophies when killing Champions and gains bonus effects based on how many trophies he has. Kills and assists grant 1 trophy.

3 Trophies: Rengar gains 25 Movement Speed whilst out of combat or in brush.
6 Trophies: Increases the range of Rengar's Leap by 125.
12 Trophies: Thrill of the Hunt's duration is increased by 5 seconds.
20 Trophies: Thrill of the Hunt's Movement Speed while stealthed is doubled.","colloq":"","plaintext":"","specialRecipe":3169,"inStore":false,"requiredChampion":"Rengar","hideFromAll":true,"image":{"full":"3422.png","sprite":"item1.png","group":"item","x":384,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":true,"10":true,"11":false,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"125","Effect3Amount":"5"},"id":3422},"3455":{"name":"Head of Kha'Zix","description":"UNIQUE Passive - Mementos of the Hunt: Rengar collects trophies when killing Champions and gains bonus effects based on how many trophies he has. Kills and assists grant 1 trophy.

3 Trophies: Rengar gains 25 Movement Speed whilst out of combat or in brush.
6 Trophies: Increases the range of Rengar's Leap by 125.
12 Trophies: Thrill of the Hunt's duration is increased by 5 seconds.
20 Trophies: Thrill of the Hunt's Movement Speed while stealthed is doubled.","colloq":"","plaintext":"","specialRecipe":3169,"inStore":false,"requiredChampion":"Rengar","hideFromAll":true,"image":{"full":"3455.png","sprite":"item1.png","group":"item","x":432,"y":432,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"25","Effect2Amount":"125","Effect3Amount":"5"},"id":3455},"3460":{"name":"Golden Transcendence","description":"Active: Use this trinket to teleport to one of the battle platforms. Can only be used from the summoning platform.

''It is at this magical precipice where a champion is dismantled, reforged, and empowered.''","colloq":"","plaintext":"","inStore":false,"image":{"full":"3460.png","sprite":"item2.png","group":"item","x":0,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3460},"3461":{"name":"Golden Transcendence (Disabled)","description":"Active: Use this trinket to teleport to one of the battle platforms. Can only be used from the summoning platform.

''It is at this magical precipice where a champion is dismantled, reforged, and empowered.''","colloq":"","plaintext":"","inStore":false,"image":{"full":"3461.png","sprite":"item2.png","group":"item","x":48,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3461},"3462":{"name":"Seer Stone (Trinket)","description":"Limited to 1 Trinket.

Active: Reveals a small area within 2500 range for 3 seconds. Enemy champions will be revealed for 5 seconds (60 second cooldown)","colloq":"blue;","plaintext":"Briefly reveals a nearby targeted area","inStore":false,"image":{"full":"3462.png","sprite":"item2.png","group":"item","x":96,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":true,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"2500","Effect2Amount":"60","Effect3Amount":"3","Effect4Amount":"5","Effect5Amount":"550"},"id":3462},"3504":{"name":"Ardent Censer","description":"+60 Ability Power
+10% Cooldown Reduction
+50% Base Mana Regen


UNIQUE Passive: +10% Heal and Shield Power
UNIQUE Passive: +8% Movement Speed
UNIQUE Passive: Your heals and shields on another allied champion grant them 20% - 35% Attack Speed and their attacks drain 20 - 35 health on-hit for 6 seconds.

(This does not include regeneration effects or effects on yourself. Bonus effects are based on target's level.)","colloq":"","plaintext":"Shield and heal effects on other units grant them Attack Speed and their attacks drain life","from":["3114","3113"],"image":{"full":"3504.png","sprite":"item2.png","group":"item","x":144,"y":0,"w":48,"h":48},"gold":{"base":650,"purchasable":true,"total":2300,"sell":1610},"tags":["CooldownReduction","ManaRegen","NonbootsMovement","SpellDamage"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMagicDamageMod":60},"effect":{"Effect1Amount":"0.08","Effect2Amount":"0.2","Effect3Amount":"6","Effect4Amount":"20","Effect5Amount":"0.1","Effect6Amount":"0.35","Effect7Amount":"35"},"depth":3,"id":3504},"3508":{"name":"Essence Reaver","description":"+70 Attack Damage
+20% Critical Strike Chance


UNIQUE Passive: +10% Cooldown Reduction
UNIQUE Passive: Gain increasingly more Cooldown Reduction from Critical Strike Chance provided by other sources (maximum +20% additional Cooldown Reduction at 30% Critical Strike Chance).
UNIQUE Passive: Critical strikes restore 3% of your maximum Mana pool.","colloq":";","plaintext":"Critical Strike provides Cooldown Reduction and Mana","from":["1038","3133","1018"],"image":{"full":"3508.png","sprite":"item2.png","group":"item","x":192,"y":0,"w":48,"h":48},"gold":{"base":200,"purchasable":true,"total":3400,"sell":2380},"tags":["Damage","CriticalStrike","ManaRegen","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":70,"FlatCritChanceMod":0.2},"effect":{"Effect1Amount":"0.1","Effect2Amount":"0.03","Effect3Amount":"0.2","Effect4Amount":"0.3","Effect5Amount":"0.0167","Effect6Amount":"0.1667"},"depth":3,"id":3508},"3512":{"name":"Zz'Rot Portal","description":"+55 Armor
+55 Magic Resist
+125% Base Health Regen

UNIQUE Passive - Point Runner: Builds up to +20% Movement Speed over 2 seconds while near turrets, fallen turrets and Void Gates.
UNIQUE Active: Spawns a Void Gate for 120 seconds (120 second cooldown).

Every 4 seconds the gate makes a Voidspawn. The first and every fourth Voidspawn gains 15% of maximum Health as damage.","colloq":";Void Gate","plaintext":"Makes a Voidspawn generating Void Gate to push a lane with.","from":["2053","1057"],"image":{"full":"3512.png","sprite":"item2.png","group":"item","x":240,"y":0,"w":48,"h":48},"gold":{"base":780,"purchasable":true,"total":2700,"sell":1890},"tags":["SpellBlock","HealthRegen","Armor","Active","NonbootsMovement"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatSpellBlockMod":55,"FlatArmorMod":55},"effect":{"Effect1Amount":"20","Effect2Amount":"4","Effect3Amount":"50","Effect4Amount":"0","Effect5Amount":"120","Effect6Amount":"120","Effect7Amount":"0.5","Effect8Amount":"0.15","Effect9Amount":"3","Effect10Amount":"20","Effect11Amount":"2","Effect12Amount":"100","Effect13Amount":"20","Effect14Amount":"50","Effect15Amount":"2"},"depth":4,"id":3512},"3513":{"name":"Eye of the Herald","description":"
UNIQUE Passive - Glimpse of the Void: The holder of the Eye of the Herald has Empowered Recall.

UNIQUE Active: Channel for 3.5 seconds to crush the Eye of the Herald, summoning the Rift Herald to siege enemy turrets.

The Eye of the Herald will be lost to the Void if not used within 240 seconds.","colloq":";Herald's Eye","plaintext":"Eye of the Herald - a Gift of the Void.","consumed":true,"inStore":false,"image":{"full":"3513.png","sprite":"item2.png","group":"item","x":288,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Trinket","Active"],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"240","Effect2Amount":"3.5","Effect3Amount":"40"},"id":3513},"3599":{"name":"The Black Spear","description":"
Active: Offer to bind with an ally for the remainder of the game, becoming Oathsworn Allies. Oathsworn empowers you both while near one another.","colloq":";spear","plaintext":"Kalista's spear that binds an Oathsworn Ally.","requiredChampion":"Kalista","image":{"full":"3599.png","sprite":"item2.png","group":"item","x":336,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Active"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":3599},"3630":{"name":"Siege Teleport","description":"Active: Use this trinket to teleport to one of your team's port pads. Can only be used from the summoning platform.","colloq":"","plaintext":"","inStore":false,"image":{"full":"3630.png","sprite":"item2.png","group":"item","x":384,"y":0,"w":48,"h":48},"gold":{"base":10,"purchasable":false,"total":10,"sell":7},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3630},"3631":{"name":"Siege Ballista","description":"
Deploys a ballista that shoots the closest turret.

Places a long range ballista if within 2200 range of an enemy turret. After a 5 second delay, it will begin firing at the nearest enemy turret, dealing heavy damage. If the targeted turret expires, the ballista will as well.","colloq":"","plaintext":"Place a long range anti-turret ballista","consumed":true,"consumeOnFull":true,"image":{"full":"3631.png","sprite":"item2.png","group":"item","x":432,"y":0,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"5","Effect2Amount":"5","Effect3Amount":"20","Effect4Amount":"3","Effect5Amount":"0","Effect6Amount":"2200","Effect7Amount":"10","Effect8Amount":"0","Effect9Amount":"0.5"},"id":3631},"3632":{"name":"","description":"","colloq":"","plaintext":"","inStore":false,"image":{"full":"3632.png","sprite":"item2.png","group":"item","x":0,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"15","Effect2Amount":"1","Effect3Amount":"310","Effect4Amount":"5000","Effect5Amount":"20","Effect6Amount":"0","Effect7Amount":"3","Effect8Amount":"45","Effect9Amount":"10","Effect10Amount":"1","Effect11Amount":"15","Effect12Amount":"3000"},"id":3632},"3633":{"name":"Siege Teleport","description":"Active: Use this trinket to teleport to one of your team's port pads. Can only be used from the summoning platform.","colloq":"","plaintext":"","inStore":false,"image":{"full":"3633.png","sprite":"item2.png","group":"item","x":48,"y":48,"w":48,"h":48},"gold":{"base":10,"purchasable":false,"total":10,"sell":7},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3633},"3634":{"name":"Tower: Beam of Ruination","description":"
Attach, then recast to fire a damaging beam from a turret to your cursor.

First Cast: Attach a Slayer Beam to the target turret that can be fired 3 times.

Next Three Casts: Fires the attached beam towards your cursor, dealing 30/level + 30% of the hit target's maximum health (20% damage to minions) in magic damage to all targets in a line.



Beam will last 15 seconds, or until it has been fired 3 times.","colloq":"","plaintext":"Attaches a three shot beam to a turret which can then be aimed and fired","consumed":true,"consumeOnFull":true,"image":{"full":"3634.png","sprite":"item2.png","group":"item","x":96,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"3","Effect2Amount":"1.5","Effect3Amount":"3000","Effect4Amount":"0","Effect5Amount":"30","Effect6Amount":"0.2","Effect7Amount":"15","Effect8Amount":"0.3","Effect9Amount":"0","Effect10Amount":"76"},"id":3634},"3635":{"name":"Port Pad","description":"
Deploy an additional teleport target.

Places a Port Pad at target location. After a 4 second delay, it activates, allowing you or your allies to teleport to it from base.","colloq":"","plaintext":"Creates another point for your team to Teleport to","consumed":true,"consumeOnFull":true,"image":{"full":"3635.png","sprite":"item2.png","group":"item","x":144,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"4","Effect2Amount":"1000","Effect3Amount":"3","Effect4Amount":"10"},"id":3635},"3636":{"name":"Tower: Storm Bulwark","description":"
Makes a turret go invulnerable, then rain fire.

Makes the target turret invulnerable for 6 seconds. Two seconds before expiry, it unleashes a missile volley, dealing 2600 true damage over the remaining time to all nearby enemies.

Cannot be used on the same turret more than once in 15 seconds.","colloq":"","plaintext":"Make a turret go invulnerable while charging a powerful barrage","consumed":true,"consumeOnFull":true,"image":{"full":"3636.png","sprite":"item2.png","group":"item","x":192,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{"FlatCritChanceMod":3},"effect":{"Effect1Amount":"6","Effect2Amount":"15","Effect3Amount":"650","Effect4Amount":"825","Effect5Amount":"1.2"},"id":3636},"3637":{"name":"Nexus Siege: Siege Weapon Slot","description":"In Nexus Siege, Summoner Spells are replaced with Siege Weapon Slots. Spend Crystal Shards to buy single-use Siege Weapons from the item shop, then use your Summoner Spell keys to activate them!","colloq":"","plaintext":"","inStore":false,"image":{"full":"3637.png","sprite":"item2.png","group":"item","x":240,"y":48,"w":48,"h":48},"gold":{"base":10,"purchasable":false,"total":10,"sell":7},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3637},"3640":{"name":"Flash Zone","description":"
Allows team to cast Flash repeatedly in a limited zone.

Creates a magic zone for your team for 5 seconds. While in this zone, you and your allies have your summoner spells replaced by an instant cast blink that moves you to any location in the zone (1 second cooldown).","colloq":"","plaintext":"Allows you and allies to repeatedly flash while in a zone","consumed":true,"consumeOnFull":true,"image":{"full":"3640.png","sprite":"item2.png","group":"item","x":288,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1","Effect2Amount":"5"},"id":3640},"3641":{"name":"Vanguard Banner","description":"
Place a banner that buffs minions.

Place a Vanguard Banner at target location. After a 2 second delay, any nearby minions will be granted a buff, increasing their damage by 50%, and granting them 50 Armor and 100 Magic Resistance while within range.","colloq":"","plaintext":"Strengthens nearby minions","consumed":true,"image":{"full":"3641.png","sprite":"item2.png","group":"item","x":336,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"0","Effect2Amount":"0.5","Effect3Amount":"50","Effect4Amount":"100","Effect5Amount":"5","Effect6Amount":"0.3","Effect7Amount":"2","Effect8Amount":"10","Effect9Amount":"1400"},"id":3641},"3642":{"name":"Siege Refund","description":"Refunds all purchased Siege Weapons for their full price.","colloq":"","plaintext":"Refunds all current Siege Weapons","consumed":true,"consumeOnFull":true,"image":{"full":"3642.png","sprite":"item2.png","group":"item","x":384,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1"},"id":3642},"3643":{"name":"Entropy Field","description":"
Stun minions and slow champions in an area.

Places an Entropy Field at target location for 5 seconds. Enemy minions and Siege Ballistas trapped in the field are unable to move or attack while in the field. Enemy champions in the field have their Movement Speed reduced by 25%.","colloq":"","plaintext":"Places a field that stuns enemy minions and slows champions","consumed":true,"consumeOnFull":true,"image":{"full":"3643.png","sprite":"item2.png","group":"item","x":432,"y":48,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"5","Effect2Amount":"600","Effect3Amount":"25"},"id":3643},"3645":{"name":"Seer Stone (Trinket)","description":"Limited to 1 Trinket.

Active: Reveals a small area within 1400 range for 3 seconds. Enemy champions will be revealed for 5 seconds (60 second cooldown)","colloq":"blue;","plaintext":"Briefly reveals a nearby targeted area","inStore":false,"image":{"full":"3645.png","sprite":"item2.png","group":"item","x":0,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":["Active","Trinket","Vision"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1400","Effect2Amount":"60","Effect3Amount":"3","Effect4Amount":"5","Effect5Amount":"550"},"id":3645},"3647":{"name":"Shield Totem","description":"
Place a totem that shields nearby deployables.

Places a Shield Totem at target location. After a 2 second delay, the totem will activate, granting a 2 (+1 per additional Shield Totem) strength shield to all nearby deployables.","colloq":"","plaintext":"Grants bonus health to nearby Siege Weapons","consumed":true,"image":{"full":"3647.png","sprite":"item2.png","group":"item","x":48,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"2","Effect2Amount":"3","Effect3Amount":"2","Effect4Amount":"1","Effect5Amount":"1000","Effect6Amount":"0.25","Effect7Amount":"10","Effect8Amount":"1","Effect9Amount":"4"},"id":3647},"3648":{"name":"Siege Teleport (Inactive)","description":"","colloq":"","plaintext":"","inStore":false,"image":{"full":"3648.png","sprite":"item2.png","group":"item","x":96,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"id":3648},"3649":{"name":"Siege Sight Warder","description":"Limited to 1 Trinket.

Active: Places a Stealth Ward that lasts 30 seconds (30 second cooldown).","colloq":"","plaintext":"","inStore":false,"image":{"full":"3649.png","sprite":"item2.png","group":"item","x":144,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":false,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"30"},"id":3649},"3671":{"name":"Enchantment: Warrior","description":"+60 Attack Damage
+10% Cooldown Reduction
","colloq":";","plaintext":"","from":["3133"],"hideFromAll":true,"image":{"full":"3671.png","sprite":"item2.png","group":"item","x":192,"y":96,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":1625,"sell":1138},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":60},"depth":3,"id":3671},"3672":{"name":"Enchantment: Cinderhulk","description":"+400 Health
+15% Bonus Health


UNIQUE Passive - Immolate: Deals 7 (+2 per champion level) magic damage a second to nearby enemies while in combat. Deals 100% bonus damage to monsters. ","colloq":";","plaintext":"","from":["3751"],"hideFromAll":true,"image":{"full":"3672.png","sprite":"item2.png","group":"item","x":240,"y":96,"w":48,"h":48},"gold":{"base":525,"purchasable":true,"total":1625,"sell":1138},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":400},"depth":3,"id":3672},"3673":{"name":"Enchantment: Runic Echoes","description":"+60 Ability Power
+7% Movement Speed


UNIQUE Passive - Echo: Gain charges upon moving or casting. At 100 charges, the next damaging spell hit expends all charges to deal 60 (+10% of Ability Power) bonus magic damage to up to 4 targets on hit.

This effect deals 250% damage to Large Monsters. Hitting a Large Monster with this effect will restore 18% of your missing Mana.","colloq":";","plaintext":"","from":["3113","1052"],"hideFromAll":true,"image":{"full":"3673.png","sprite":"item2.png","group":"item","x":288,"y":96,"w":48,"h":48},"gold":{"base":340,"purchasable":true,"total":1625,"sell":1138},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentMovementSpeedMod":0.07,"FlatMagicDamageMod":60},"depth":3,"id":3673},"3675":{"name":"Enchantment: Bloodrazor","description":"+50% Attack Speed

UNIQUE Passive: Basic attacks deal 4% of the target's maximum Health in bonus physical damage (max 75 vs. monsters and minions) on hit.","colloq":";","plaintext":"","from":["1043"],"hideFromAll":true,"image":{"full":"3675.png","sprite":"item2.png","group":"item","x":336,"y":96,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":1625,"sell":1138},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"PercentAttackSpeedMod":0.5},"depth":3,"id":3675},"3680":{"name":"Frosted Snax","description":"Active - Feed The King: The King lobs many projectiles at far-away enemies, each dealing 213-775 magic damage to targets in the center of the impact, scaling down to 85-310 on the edge. (120s cooldown)","colloq":"","plaintext":"King: Fires a barrage of icy artillery","image":{"full":"3680.png","sprite":"item2.png","group":"item","x":384,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Trinket","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"5","Effect2Amount":"120","Effect3Amount":"40","Effect4Amount":"15","Effect5Amount":"250","Effect6Amount":"213","Effect7Amount":"775","Effect8Amount":"85","Effect9Amount":"310"},"id":3680},"3681":{"name":"Super Spicy Snax","description":"Active - Feed The King: The King breathes fire for 4 seconds, dealing 705-1479 true damage over the duration to enemies caught in the cone. Deals up to 560 true damage to Turrets. (120s cooldown)","colloq":"","plaintext":"King: Shoots flames that burn units and Turrets","image":{"full":"3681.png","sprite":"item2.png","group":"item","x":432,"y":96,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Trinket","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"4","Effect2Amount":"120","Effect3Amount":"700","Effect4Amount":"2.35","Effect5Amount":"560","Effect6Amount":"705","Effect7Amount":"1479"},"id":3681},"3682":{"name":"Espresso Snax","description":"Active - Feed The King: The King leaps into the air and crashes down twice, knocking enemies away and dealing 40-190 physical damage. He also gains a decaying shield for 20% of his maximum health, lasting 4 seconds. (30s cooldown)","colloq":"","plaintext":"King: Knocks back and grants a large shield","image":{"full":"3682.png","sprite":"item2.png","group":"item","x":0,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Trinket","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"1.8","Effect2Amount":"30","Effect3Amount":"10","Effect4Amount":"10","Effect5Amount":"20","Effect6Amount":"4","Effect7Amount":"600","Effect8Amount":"40","Effect9Amount":"190","Effect10Amount":"500"},"id":3682},"3683":{"name":"Rainbow Snax Party Pack!","description":"Active - Feed The King: The King tosses many Snax behind the enemy, attracting Poros which dash back towards him. Enemy champions hit will be knocked forwards and dealt 230-680 physical damage. (120s cooldown)","colloq":"","plaintext":"King: Poros knock enemies towards him","image":{"full":"3683.png","sprite":"item2.png","group":"item","x":48,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":["Trinket","Lane"],"maps":{"8":false,"10":false,"11":false,"12":true,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"6","Effect2Amount":"120","Effect3Amount":"140","Effect4Amount":"30","Effect5Amount":"1","Effect6Amount":"1500","Effect7Amount":"1650","Effect8Amount":"1000","Effect9Amount":"230","Effect10Amount":"680","Effect11Amount":"2"},"id":3683},"3690":{"name":"Cosmic Shackle","description":"Passive - Cosmic Shackle: Death Sentence pulls much farther (based on the target's Missing Health), and can be ignited by the Dark Star to do more damage.

''A still more glorious dawn awaits.''","colloq":";","plaintext":"","image":{"full":"3690.png","sprite":"item2.png","group":"item","x":96,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3690},"3691":{"name":"Singularity Lantern","description":"Passive - Singularity Lantern: Dark Passage automatically saves disabled allies. However, it no longer provides a shield.

''The stars call to us.''","colloq":";","plaintext":"","image":{"full":"3691.png","sprite":"item2.png","group":"item","x":144,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3691},"3692":{"name":"Dark Matter Scythe","description":"Passive - Dark Matter Scythe: Flay's on-hit passive charges damage very quickly. Flay will throw enemies much farther (based on their Missing Health).

''If you want to make a Singularity from scratch, you must first destroy the universe.''","colloq":";","plaintext":"","image":{"full":"3692.png","sprite":"item2.png","group":"item","x":192,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{},"id":3692},"3693":{"name":"Gravity Boots","description":"Passive - Mass Conversion: Thresh's Health represents how far enemy pulls and pushes will send him. At lower Health, he will be thrown farther.

Passive - Terminus Dwellers: Abyss Scuttlers emerge periodically, and will scurry towards the Dark Star when attacked. Gravitational disturbances will temporarily attract many of them.","colloq":";","plaintext":"","image":{"full":"3693.png","sprite":"item2.png","group":"item","x":240,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":50},"id":3693},"3694":{"name":"Cloak of Stars","description":"Passive - Stellar Spirit: Upon spawning, Thresh is invulnerable, untargetable, cannot cast, and is able to travel in open space. This is lost when stepping foot on stable ground.

Being saved by Dark Passage or using Death Sentence on one of the three Gravity Anchors will briefly put you into this invulnerable state and break enemy chains on you.","colloq":";","plaintext":"","image":{"full":"3694.png","sprite":"item2.png","group":"item","x":288,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":50},"id":3694},"3695":{"name":"Dark Star Sigil","description":"Passive - Stellar Fealty: Thresh cannot kill units directly - their souls, experience, and gold belong to the Dark Star.

Pulling or pushing an enemy into the Dark Star will destroy them instantly, scoring points for your team (+5, or +1 for Abyss Scuttlers).

Winning a round requires 100 points, and the final points must be from a champion kill.","colloq":";","plaintext":"","image":{"full":"3695.png","sprite":"item2.png","group":"item","x":336,"y":144,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":false,"10":false,"11":false,"12":false,"14":false,"16":false},"stats":{"FlatMovementSpeedMod":50},"id":3695},"3706":{"name":"Stalker's Blade","description":"Limited to 1 Jungle item

+10% Life Steal vs. Monsters
+180% Base Mana Regen while in Jungle


UNIQUE Passive - Chilling Smite: Smite can be cast on enemy champions, dealing reduced true damage and stealing 20% Movement Speed for 2 seconds.
UNIQUE Passive - Tooth / Nail: Basic attacks deal 25 bonus damage vs. monsters. Damaging a monster with a spell or attack steals 30 Health over 5 seconds. Killing monsters grants special bonus experience.","colloq":";jungle;Jungle;jangle","plaintext":"Lets your Smite slow Champions","from":["1039","1041"],"into":["1400","1401","1402","1416"],"image":{"full":"3706.png","sprite":"item2.png","group":"item","x":384,"y":144,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":1000,"sell":700},"tags":["LifeSteal","ManaRegen","Slow","OnHit","NonbootsMovement","Jungle"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"-0.2","Effect7Amount":"2","Effect8Amount":"3","Effect9Amount":"0.1"},"depth":2,"id":3706},"3711":{"name":"Tracker's Knife","description":"Limited to 1 Jungle item

+10% Life Steal vs. Monsters
+180% Base Mana Regen while in Jungle


UNIQUE Passive - Tooth / Nail: Basic attacks deal 25 bonus damage vs. monsters. Damaging a monster with a spell or attack steals 30 Health over 5 seconds. Killing monsters grants special bonus experience.
UNIQUE Active - Warding: Consumes a charge to place a Stealth Ward that reveals the surrounding area for 150 seconds. Holds up to 2 charges which refill upon visiting the shop.

(A player may only have 3 Stealth Wards on the map at one time. Unique Passives with the same name don't stack.)","colloq":";jungle;Jungle","plaintext":"Provides Stealth Wards over time","from":["1039","1041"],"into":["1408","1409","1410","1418"],"image":{"full":"3711.png","sprite":"item2.png","group":"item","x":432,"y":144,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":1000,"sell":700},"tags":["LifeSteal","ManaRegen","Vision","Active","OnHit","Jungle"],"maps":{"8":false,"10":false,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"30","Effect9Amount":"0.1","Effect10Amount":"150"},"depth":2,"id":3711},"3715":{"name":"Skirmisher's Sabre","description":"Limited to 1 Jungle item

+10% Life Steal vs. Monsters
+180% Base Mana Regen while in Jungle


Passive - Challenging Smite: Smite can be cast on enemy champions, marking them for 4 seconds. While marked, the target is revealed, your basic attacks deal bonus true damage over 3 seconds, and their damage to you is reduced by 20%.
UNIQUE Passive - Tooth / Nail: Basic attacks deal 25 bonus damage vs. monsters. Damaging a monster with a spell or attack steals 30 Health over 5 seconds. Killing monsters grants special bonus experience.","colloq":";jungle;Jungle","plaintext":"Lets your Smite mark Champions, giving you combat power against them.","from":["1039","1041"],"into":["1412","1413","1414","1419"],"image":{"full":"3715.png","sprite":"item2.png","group":"item","x":0,"y":192,"w":48,"h":48},"gold":{"base":300,"purchasable":true,"total":1000,"sell":700},"tags":["LifeSteal","ManaRegen","OnHit","Jungle"],"maps":{"8":false,"10":true,"11":true,"12":false,"14":false,"16":false},"stats":{},"effect":{"Effect1Amount":"30","Effect2Amount":"25","Effect3Amount":"1.8","Effect4Amount":"5","Effect5Amount":"30","Effect6Amount":"3","Effect7Amount":"20","Effect8Amount":"18","Effect9Amount":"0.1","Effect10Amount":"4"},"depth":2,"id":3715},"3742":{"name":"Dead Man's Plate","description":"+425 Health
+60 Armor


UNIQUE Passive - Dreadnought: While moving, build stacks of Momentum, increasing movement speed by up to 60 at 100 stacks. Momentum decays while under the effect of a slow, stun, taunt, fear, polymorph, or immobilize effect, as well as when basic attacking.
UNIQUE Passive - Crushing Blow: Basic attacks at 100 stacks deal 100 bonus damage and discharge the stacks. If the attacker is melee, they also slow the target by 50% for 1 second.

''There's only one way you'll get this armor from me...'' - forgotten namesake","colloq":";juggernaut;dreadnought","plaintext":"Build momentum as you move around then smash into enemies.","from":["1031","1011"],"image":{"full":"3742.png","sprite":"item2.png","group":"item","x":48,"y":192,"w":48,"h":48},"gold":{"base":1100,"purchasable":true,"total":2900,"sell":2030},"tags":["Health","Armor","OnHit","NonbootsMovement","Bilgewater"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":425,"FlatArmorMod":60},"effect":{"Effect1Amount":"60","Effect2Amount":"100","Effect3Amount":"2","Effect4Amount":"-0.5","Effect5Amount":"1"},"depth":3,"id":3742},"3748":{"name":"Titanic Hydra","description":"+450 Health
+35 Attack Damage
+100% Base Health Regen


UNIQUE Passive - Cleave: Basic attacks deal 5 + 1% of your maximum health as bonus physical damage to your target and 40 + 2.5% of your maximum health as physical damage to other enemies in a cone on hit.
UNIQUE Active - Crescent: Cleave damage to all targets is increased to 40 + 10% of your maximum health as bonus physical damage in a larger cone for your next basic attack (20 second cooldown).

(Unique Passives with the same name don't stack.)","colloq":";juggernaut","plaintext":"Deals area of effect damage based on owner's health","from":["3077","1028","3052"],"image":{"full":"3748.png","sprite":"item2.png","group":"item","x":96,"y":192,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":3500,"sell":2450},"tags":["Health","HealthRegen","Damage","Active","OnHit"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":35,"FlatHPPoolMod":450},"effect":{"Effect1Amount":"0.025","Effect2Amount":"40","Effect3Amount":"0","Effect4Amount":"0","Effect5Amount":"0.1","Effect6Amount":"0","Effect7Amount":"20","Effect8Amount":"40","Effect9Amount":"0.01","Effect10Amount":"5"},"depth":3,"id":3748},"3751":{"name":"Bami's Cinder","description":"+280 Health

UNIQUE Passive - Immolate: Deals 5 (+1 per champion level) magic damage per second to nearby enemies. Deals 50% bonus damage to minions and monsters.","colloq":";","plaintext":"Grants Health and Immolate Aura","from":["1028"],"into":["3068","1401","1409","1413","3672"],"image":{"full":"3751.png","sprite":"item2.png","group":"item","x":144,"y":192,"w":48,"h":48},"gold":{"base":700,"purchasable":true,"total":1100,"sell":770},"tags":["Health"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":280},"effect":{"Effect1Amount":"5","Effect2Amount":"1","Effect3Amount":"50"},"depth":2,"id":3751},"3800":{"name":"Righteous Glory","description":"+500 Health
+300 Mana
+100% Base Health Regen


UNIQUE Passive - Eternity: 15% of damage taken from champions is gained as Mana. Spending Mana restores 20% of the cost as Health, up to 25 per spell cast.
UNIQUE Active: Grants +75% Movement Speed when moving towards enemies or enemy turrets for 4 seconds. After 3 seconds, a shockwave is emitted, slowing nearby enemy champion Movement Speed by 75% for 2 second(s) (90 second cooldown).

This effect may be reactivated early to instantly release the shockwave.","colloq":";","plaintext":"Grants Health, Mana. Activate to speed towards enemies and slow them.","from":["3010","3801"],"image":{"full":"3800.png","sprite":"item2.png","group":"item","x":192,"y":192,"w":48,"h":48},"gold":{"base":750,"purchasable":true,"total":2500,"sell":1750},"tags":["Health","HealthRegen","Mana","ManaRegen","Active","Slow","NonbootsMovement"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":500,"FlatMPPoolMod":300},"effect":{"Effect1Amount":"0.75","Effect2Amount":"4","Effect3Amount":"-0.75","Effect4Amount":"2","Effect5Amount":"90","Effect6Amount":"0.2","Effect7Amount":"25","Effect8Amount":"0.15","Effect9Amount":"3"},"depth":3,"id":3800},"3801":{"name":"Crystalline Bracer","description":"+200 Health
+50% Base Health Regen
","colloq":";","plaintext":"Grants Health and Health Regen","from":["1028","1006"],"into":["3109","3800","3083","3084","3107"],"image":{"full":"3801.png","sprite":"item2.png","group":"item","x":240,"y":192,"w":48,"h":48},"gold":{"base":100,"purchasable":true,"total":650,"sell":455},"tags":["Health","HealthRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatHPPoolMod":200},"depth":2,"id":3801},"3802":{"name":"Lost Chapter","description":"+25 Ability Power
+300 Mana


UNIQUE Passive: Upon levelling up, restores 20% of your maximum Mana over 3 seconds.","colloq":";","plaintext":"Restores Mana upon levelling up.","from":["1052","1027"],"into":["3165"],"image":{"full":"3802.png","sprite":"item2.png","group":"item","x":288,"y":192,"w":48,"h":48},"gold":{"base":115,"purchasable":true,"total":900,"sell":630},"tags":["SpellDamage","Mana","ManaRegen"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatMPPoolMod":300,"FlatMagicDamageMod":25},"effect":{"Effect1Amount":"25","Effect2Amount":"-0.1","Effect3Amount":"15","Effect4Amount":"10","Effect5Amount":"20","Effect6Amount":"5","Effect7Amount":"0.2","Effect8Amount":"3"},"depth":2,"id":3802},"3812":{"name":"Death's Dance","description":"+80 Attack Damage
+10% Cooldown Reduction


UNIQUE Passive: Dealing physical damage heals for 15% of the damage dealt. This is 33% as effective for Area of Effect damage.
UNIQUE Passive: 30% of damage taken is dealt as a Bleed effect over 3 seconds instead.","colloq":";Bloodbag","plaintext":"Trades incoming damage now for incoming damage later","stacks":0,"from":["1053","1037","3133"],"image":{"full":"3812.png","sprite":"item2.png","group":"item","x":336,"y":192,"w":48,"h":48},"gold":{"base":625,"purchasable":true,"total":3500,"sell":2450},"tags":["Damage","LifeSteal","CooldownReduction"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":80},"effect":{"Effect1Amount":"0.15","Effect2Amount":"0.3","Effect3Amount":"3"},"depth":3,"id":3812},"3814":{"name":"Edge of Night","description":"+55 Attack Damage
+35 Magic Resist


UNIQUE Passive: +15 Lethality
UNIQUE Passive: +20 Movement Speed out of Combat
UNIQUE Active - Night's Veil: Channel for 1.5 second to grant a spell shield that blocks the next enemy ability. Lasts for 5 seconds (45 second cooldown).

(Can move while channeling, but taking damage breaks the channel.)","colloq":";lethality","plaintext":"Blocks an incoming enemy spell.","stacks":0,"from":["1037","3134","1033"],"image":{"full":"3814.png","sprite":"item2.png","group":"item","x":384,"y":192,"w":48,"h":48},"gold":{"base":675,"purchasable":true,"total":3100,"sell":2170},"tags":["SpellBlock","Damage","NonbootsMovement","ArmorPenetration"],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{"FlatPhysicalDamageMod":55,"FlatSpellBlockMod":35},"effect":{"Effect1Amount":"15","Effect2Amount":"5","Effect3Amount":"20","Effect4Amount":"45","Effect5Amount":"5"},"depth":3,"id":3814},"3901":{"name":"Fire at Will","description":"Requires 500 Silver Serpents.

UNIQUE Passive: Cannon Barrage fires at an increasing rate over time (additional 6 waves over the duration).","colloq":"","plaintext":"Cannon Barrage gains extra waves","consumed":true,"consumeOnFull":true,"requiredChampion":"Gangplank","image":{"full":"3901.png","sprite":"item2.png","group":"item","x":432,"y":192,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":3901},"3902":{"name":"Death's Daughter","description":"Requires 500 Silver Serpents.

UNIQUE Passive: Cannon Barrage additionally fires a mega-cannonball at center of the Barrage, dealing 300% true damage and slowing them by 60% for 1.5 seconds. ","colloq":"","plaintext":"Cannon Barrage fires a mega-cannonball","consumed":true,"consumeOnFull":true,"requiredChampion":"Gangplank","image":{"full":"3902.png","sprite":"item2.png","group":"item","x":0,"y":240,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":3902},"3903":{"name":"Raise Morale","description":"Requires 500 Silver Serpents.

UNIQUE Passive: Allies in the Cannon Barrage gain 30% Movement Speed for 2 seconds.","colloq":"","plaintext":"Cannon Barrage hastes allies","consumed":true,"consumeOnFull":true,"requiredChampion":"Gangplank","image":{"full":"3903.png","sprite":"item2.png","group":"item","x":48,"y":240,"w":48,"h":48},"gold":{"base":0,"purchasable":true,"total":0,"sell":0},"tags":[],"maps":{"8":true,"10":true,"11":true,"12":true,"14":false,"16":false},"stats":{},"id":3903}} ================================================ FILE: api_data/masteries.json ================================================ {"6111":{"id":6111,"name":"Fury","description":["+0.8% Attack Speed","+1.6% Attack Speed","+2.4% Attack Speed","+3.2% Attack Speed","+4% Attack Speed"],"image":{"full":"6111.png","sprite":"mastery0.png","group":"mastery","x":0,"y":0,"w":48,"h":48},"ranks":5,"prereq":"0"},"6114":{"id":6114,"name":"Sorcery","description":["+0.4% increased Ability damage","+0.8% increased Ability damage","+1.2% increased Ability damage","+1.6% increased Ability damage","+2.0% increased Ability damage"],"image":{"full":"6114.png","sprite":"mastery0.png","group":"mastery","x":48,"y":0,"w":48,"h":48},"ranks":5,"prereq":"0"},"6121":{"id":6121,"name":"Fresh Blood","description":["Your first basic attack against a champion deals an additional 10 +1 per level damage (6 second cooldown)"],"image":{"full":"6121.png","sprite":"mastery0.png","group":"mastery","x":96,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6122":{"id":6122,"name":"Feast","description":["Killing a unit restores 20 Health (30 second cooldown)"],"image":{"full":"6122.png","sprite":"mastery0.png","group":"mastery","x":144,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6123":{"id":6123,"name":"Expose Weakness","description":["Damaging enemy champions causes them to take 3% more damage from your allies"],"image":{"full":"6123.png","sprite":"mastery0.png","group":"mastery","x":192,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6131":{"id":6131,"name":"Vampirism","description":["+0.4% Lifesteal and Spell Vamp","+0.8% Lifesteal and Spell Vamp","+1.2% Lifesteal and Spell Vamp","+1.6% Lifesteal and Spell Vamp","+2.0% Lifesteal and Spell Vamp"],"image":{"full":"6131.png","sprite":"mastery0.png","group":"mastery","x":240,"y":0,"w":48,"h":48},"ranks":5,"prereq":"0"},"6134":{"id":6134,"name":"Natural Talent","description":["Gain 0.4 + 0.09 per level Attack Damage, and 0.6 + 0.13 per level Ability Power (+2 Attack Damage and 3 Ability Power at level 18)","Gain 0.8 + 0.18 per level Attack Damage, and 1.2 + 0.27 per level Ability Power (+4 Attack Damage and 6 Ability Power at level 18)","Gain 1.2 + 0.27 per level Attack Damage, and 1.8 + 0.4 per level Ability Power (+6 Attack Damage and 9 Ability Power at level 18)","Gain 1.6 + 0.36 per level Attack Damage, and 2.4 + 0.53 per level Ability Power (+8 Attack Damage and 12 Ability Power at level 18)","Gain 2 + 0.44 per level Attack Damage, and 3 + 0.67 per level Ability Power (+10 Attack Damage and 15 Ability Power at level 18)"],"image":{"full":"6134.png","sprite":"mastery0.png","group":"mastery","x":288,"y":0,"w":48,"h":48},"ranks":5,"prereq":"0"},"6141":{"id":6141,"name":"Bounty Hunter","description":["Deal 1% increased damage for each unique enemy champion you have killed"],"image":{"full":"6141.png","sprite":"mastery0.png","group":"mastery","x":336,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6142":{"id":6142,"name":"Double Edged Sword","description":["Deal 3% additional damage, take 1.5% additional damage."],"image":{"full":"6142.png","sprite":"mastery0.png","group":"mastery","x":384,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6143":{"id":6143,"name":"Battle Trance","description":["Gain up to 3% increased damage over 3 seconds when in combat with enemy Champions"],"image":{"full":"6143.png","sprite":"mastery0.png","group":"mastery","x":432,"y":0,"w":48,"h":48},"ranks":1,"prereq":"0"},"6151":{"id":6151,"name":"Battering Blows","description":["+1.4% Armor Penetration","+2.8% Armor Penetration","+4.2% Armor Penetration","+5.6% Armor Penetration","+7% Armor Penetration"],"image":{"full":"6151.png","sprite":"mastery0.png","group":"mastery","x":0,"y":48,"w":48,"h":48},"ranks":5,"prereq":"0"},"6154":{"id":6154,"name":"Piercing Thoughts","description":["+1.4% Magic Penetration","+2.8% Magic Penetration","+4.2% Magic Penetration","+5.6% Magic Penetration","+7% Magic Penetration"],"image":{"full":"6154.png","sprite":"mastery0.png","group":"mastery","x":48,"y":48,"w":48,"h":48},"ranks":5,"prereq":"0"},"6161":{"id":6161,"name":"Warlord's Bloodlust","description":["Moving or attacking will charge an Energized attack. Energized attacks heal for 5-40% of your total Attack Damage (amplified by Critical Strikes) and grant 30% Movement Speed for 0.75 seconds."],"image":{"full":"6161.png","sprite":"mastery0.png","group":"mastery","x":96,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6162":{"id":6162,"name":"Fervor of Battle","description":["Hitting champions with basic attacks generates a Fervor stack (2 for melee attacks). Stacks of Fervor last 8 seconds (max 8 stacks)and increase your AD by 1-8 for each stack."],"image":{"full":"6162.png","sprite":"mastery0.png","group":"mastery","x":144,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6164":{"id":6164,"name":"Deathfire Touch","description":["Your damaging abilities cause enemy champions to take magic damage over 4 seconds.

Damage: 8 + 45% Bonus Attack Damage and 25% Ability Power

Deathfire Touch's duration is reduced for:
- Area of Effect: 2 second duration.
- Damage over Time: 1 second duration."],"image":{"full":"6164.png","sprite":"mastery0.png","group":"mastery","x":192,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6211":{"id":6211,"name":"Recovery","description":["+0.4 Health per 5 seconds","+0.8 Health per 5 seconds","+1.2 Health per 5 seconds","+1.6 Health per 5 seconds","+2.0 Health per 5 seconds"],"image":{"full":"6211.png","sprite":"mastery0.png","group":"mastery","x":0,"y":144,"w":48,"h":48},"ranks":5,"prereq":"0"},"6212":{"id":6212,"name":"Unyielding","description":["+1% Bonus Armor and Magic Resist","+2% Bonus Armor and Magic Resist","+3% Bonus Armor and Magic Resist","+4% Bonus Armor and Magic Resist","+5% Bonus Armor and Magic Resist"],"image":{"full":"6212.png","sprite":"mastery0.png","group":"mastery","x":48,"y":144,"w":48,"h":48},"ranks":5,"prereq":"0"},"6221":{"id":6221,"name":"Explorer","description":["+15 Movement Speed in Brush and River"],"image":{"full":"6221.png","sprite":"mastery0.png","group":"mastery","x":96,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6222":{"id":6222,"name":"Siegemaster","description":["Gain 8 Armor and Magic Resistance when near an allied tower"],"image":{"full":"6222.png","sprite":"mastery0.png","group":"mastery","x":192,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6223":{"id":6223,"name":"Tough Skin","description":["You take 2 less damage from champion and neutral monster basic attacks"],"image":{"full":"6223.png","sprite":"mastery0.png","group":"mastery","x":144,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6231":{"id":6231,"name":"Runic Armor","description":["Shields, healing, regeneration, and lifesteal on you are 1.6% stronger","Shields, healing, regeneration, and lifesteal on you are 3.2% stronger","Shields, healing, regeneration, and lifesteal on you are 4.8% stronger","Shields, healing, regeneration, and lifesteal on you are 6.4% stronger","Shields, healing, regeneration, and lifesteal on you are 8% stronger"],"image":{"full":"6231.png","sprite":"mastery0.png","group":"mastery","x":240,"y":144,"w":48,"h":48},"ranks":5,"prereq":"0"},"6232":{"id":6232,"name":"Veteran's Scars","description":["+10 Health","+20 Health","+30 Health","+40 Health","+50 Health"],"image":{"full":"6232.png","sprite":"mastery0.png","group":"mastery","x":288,"y":144,"w":48,"h":48},"ranks":5,"prereq":"0"},"6241":{"id":6241,"name":"Insight","description":["Reduces the cooldown of Summoner Spells by 15%"],"image":{"full":"6241.png","sprite":"mastery0.png","group":"mastery","x":336,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6242":{"id":6242,"name":"Perseverance","description":["+50% Base Health Regen, increased to +200% when below 25% Health"],"image":{"full":"6242.png","sprite":"mastery0.png","group":"mastery","x":384,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6243":{"id":6243,"name":"Fearless","description":["Gain 10% +1.5 per level bonus Armor and Magic Resist when damaged by an enemy champion for 2 seconds (9s Cooldown)"],"image":{"full":"6243.png","sprite":"mastery0.png","group":"mastery","x":432,"y":144,"w":48,"h":48},"ranks":1,"prereq":"0"},"6251":{"id":6251,"name":"Swiftness","description":["+3% Tenacity and Slow Resist","+6% Tenacity and Slow Resist","+9% Tenacity and Slow Resist","+12% Tenacity and Slow Resist","+15% Tenacity and Slow Resist"],"image":{"full":"6251.png","sprite":"mastery0.png","group":"mastery","x":0,"y":192,"w":48,"h":48},"ranks":5,"prereq":"0"},"6252":{"id":6252,"name":"Legendary Guardian","description":["+0.6 Armor and Magic Resist for each nearby enemy champion","+1.2 Armor and Magic Resist for each nearby enemy champion","+1.8 Armor and Magic Resist for each nearby enemy champion","+2.4 Armor and Magic Resist for each nearby enemy champion","+3 Armor and Magic Resist for each nearby enemy champion"],"image":{"full":"6252.png","sprite":"mastery0.png","group":"mastery","x":48,"y":192,"w":48,"h":48},"ranks":5,"prereq":"0"},"6261":{"id":6261,"name":"Grasp of the Undying","description":["Every 4 seconds in combat, your next attack against an enemy champion deals damage equal to 3% of your max Health and heals you for 1.5% of your max Health (halved for ranged champions, deals magic damage)"],"image":{"full":"6261.png","sprite":"mastery0.png","group":"mastery","x":96,"y":192,"w":48,"h":48},"ranks":1,"prereq":"0"},"6262":{"id":6262,"name":"Courage of the Colossus","description":["Gain a shield for 3-54 (+5% of your maximum health) for each nearby enemy champion for 3 seconds after hitting an enemy champion with a stun, taunt, snare, or knock up (45-30 second cooldown, based on level)."],"image":{"full":"6262.png","sprite":"mastery0.png","group":"mastery","x":144,"y":192,"w":48,"h":48},"ranks":1,"prereq":"0"},"6263":{"id":6263,"name":"Stoneborn Pact","description":["Gain 5% total health.
Your movement impairing effects brand enemy champions with an earthen rune for 4 seconds. Other allied champions who attack branded enemies heal for 5 + 2.5% of your maximum health over 2 seconds (halved if you are ranged)."],"image":{"full":"6263.png","sprite":"mastery0.png","group":"mastery","x":192,"y":192,"w":48,"h":48},"ranks":1,"prereq":"0"},"6311":{"id":6311,"name":"Wanderer","description":["+0.6% Movement Speed out of combat","+1.2% Movement Speed out of combat","+1.8% Movement Speed out of combat","+2.4% Movement Speed out of combat","+3% Movement Speed out of combat"],"image":{"full":"6311.png","sprite":"mastery0.png","group":"mastery","x":240,"y":48,"w":48,"h":48},"ranks":5,"prereq":"0"},"6312":{"id":6312,"name":"Savagery","description":["Single target attacks and spells deal 1 bonus damage to minions and monsters","Single target attacks and spells deal 2 bonus damage to minions and monsters","Single target attacks and spells deal 3 bonus damage to minions and monsters","Single target attacks and spells deal 4 bonus damage to minions and monsters","Single target attacks and spells deal 5 bonus damage to minions and monsters"],"image":{"full":"6312.png","sprite":"mastery0.png","group":"mastery","x":288,"y":48,"w":48,"h":48},"ranks":5,"prereq":"0"},"6321":{"id":6321,"name":"Runic Affinity","description":["Buffs from neutral monsters last 15% longer"],"image":{"full":"6321.png","sprite":"mastery0.png","group":"mastery","x":336,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6322":{"id":6322,"name":"Secret Stash","description":["Your Potions and Elixirs last 10% longer.

Your Health Potions are replaced with Biscuits that restore 15 Health and Mana instantly on use"],"image":{"full":"6322.png","sprite":"mastery0.png","group":"mastery","x":384,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6323":{"id":6323,"name":"Assassin","description":["Deal 2% increased damage to champions when no allied champions are nearby"],"image":{"full":"6323.png","sprite":"mastery0.png","group":"mastery","x":432,"y":48,"w":48,"h":48},"ranks":1,"prereq":"0"},"6331":{"id":6331,"name":"Merciless","description":["Deal 0.6% increased damage to champions below 40% Health","Deal 1.2% increased damage to champions below 40% Health","Deal 1.8% increased damage to champions below 40% Health","Deal 2.4% increased damage to champions below 40% Health","Deal 3% increased damage to champions below 40% Health"],"image":{"full":"6331.png","sprite":"mastery0.png","group":"mastery","x":0,"y":96,"w":48,"h":48},"ranks":5,"prereq":"0"},"6332":{"id":6332,"name":"Meditation","description":["Regenerate 0.25% of your missing Mana every 5 seconds","Regenerate 0.5% of your missing Mana every 5 seconds","Regenerate 0.75% of your missing Mana every 5 seconds","Regenerate 1.0% of your missing Mana every 5 seconds","Regenerate 1.25% of your missing Mana every 5 seconds"],"image":{"full":"6332.png","sprite":"mastery0.png","group":"mastery","x":48,"y":96,"w":48,"h":48},"ranks":5,"prereq":"0"},"6341":{"id":6341,"name":"Greenfather's Gift","description":["Stepping into brush causes your next damaging attack or ability to deal 3% of your target's current health as bonus magic damage (9s Cooldown)"],"image":{"full":"6341.png","sprite":"mastery0.png","group":"mastery","x":96,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"},"6342":{"id":6342,"name":"Bandit","description":["Gain 1 gold for each nearby minion killed by an ally.

Gain 3 gold (10 if melee) when hitting an enemy champion with a basic attack (5 second cooldown)"],"image":{"full":"6342.png","sprite":"mastery0.png","group":"mastery","x":144,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"},"6343":{"id":6343,"name":"Dangerous Game","description":["Champion kills and assists restore 5% of your missing Health and Mana"],"image":{"full":"6343.png","sprite":"mastery0.png","group":"mastery","x":192,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"},"6351":{"id":6351,"name":"Precision","description":["Gain 1.2 Lethality and 0.3 + 0.05 per level Magic Penetration","Gain 2.4 Lethality and 0.6 + 0.10 per level Magic Penetration","Gain 3.6 Lethality and 0.9 + 0.15 per level Magic Penetration","Gain 4.8 Lethality and 1.2 + 0.20 per level Magic Penetration","Gain 6 Lethality and 1.5 + 0.25 per level Magic Penetration"],"image":{"full":"6351.png","sprite":"mastery0.png","group":"mastery","x":240,"y":96,"w":48,"h":48},"ranks":5,"prereq":"0"},"6352":{"id":6352,"name":"Intelligence","description":["Your Cooldown Reduction cap is increased to 41% and you gain 1% Cooldown Reduction","Your Cooldown Reduction cap is increased to 42% and you gain 2% Cooldown Reduction","Your Cooldown Reduction cap is increased to 43% and you gain 3% Cooldown Reduction","Your Cooldown Reduction cap is increased to 44% and you gain 4% Cooldown Reduction","Your Cooldown Reduction cap is increased to 45% and you gain 5% Cooldown Reduction"],"image":{"full":"6352.png","sprite":"mastery0.png","group":"mastery","x":288,"y":96,"w":48,"h":48},"ranks":5,"prereq":"0"},"6361":{"id":6361,"name":"Stormraider's Surge","description":["Dealing 30% of a champion's max Health within 2.5 seconds grants you 40% Movement Speed and 75% Slow Resistance for 3 seconds (10 second cooldown)."],"image":{"full":"6361.png","sprite":"mastery0.png","group":"mastery","x":336,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"},"6362":{"id":6362,"name":"Thunderlord's Decree","description":["Your 3rd attack or damaging spell against the same enemy champion calls down a lightning strike, dealing magic damage in the area.

Damage: 10 per level, plus 30% of your Bonus Attack Damage, and 10% of your Ability Power (25-15 second cooldown, based on level)."],"image":{"full":"6362.png","sprite":"mastery0.png","group":"mastery","x":384,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"},"6363":{"id":6363,"name":"Windspeaker's Blessing","description":["Your heals and shields are 10% stronger. Additionally, your shields and heals on other allies increase their armor by 5-22 (based on level) and their magic resistance by half that amount for 3 seconds."],"image":{"full":"6363.png","sprite":"mastery0.png","group":"mastery","x":432,"y":96,"w":48,"h":48},"ranks":1,"prereq":"0"}} ================================================ FILE: api_data/runes.js ================================================ module.exports = ''; ================================================ FILE: api_data/skills.json ================================================ {"Aatrox":{"id":266,"key":"Aatrox","name":"Aatrox","title":"the Darkin Blade","spells":[{"id":"AatroxQ","name":"Dark Flight","description":"Aatrox takes flight and slams down at a targeted location, dealing damage and knocking up enemies at the center of impact.","tooltip":"Aatrox takes flight and slams down at target location, dealing {{ e1 }} (+{{ a1 }}) physical damage and knocking up enemies at the center of impact for {{ e4 }} second.

Aatrox fills up 20% of the Blood Well upon cast.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12.5,12,11.5,11],"cooldownBurn":"13/12.5/12/11.5/11","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,35,60,95,120],[310,310,310,310,310],[190,190,190,190,190],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/35/60/95/120","310","190","1","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[25000,25000,25000,25000,25000],"rangeBurn":"25000","image":{"full":"AatroxQ.png","sprite":"spell0.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"AatroxW","name":"Blood Thirst / Blood Price","description":"While toggled on Aatrox deals bonus damage and fills a portion of his Blood Well every third subsequent attack. While toggled off Aatrox restores Health every third subsequent attack.","tooltip":"Toggle Off: Blood Thirst: Every third attack, Aatrox restores {{ e3 }} (+{{ f1*100 }}% Missing Health) Health.

Toggle On: Blood Price: Every third attack, Aatrox deals {{ e2 }} (+{{ a2 }}) bonus physical damage and fills up 20% of the Blood Well.","leveltip":{"label":["Heal","Damage"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[200,200,200,200,200],[45,80,115,150,185],[30,45,60,75,90],[50,50,50,50,50],[50,50,50,50,50],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"200","45/80/115/150/185","30/45/60/75/90","50","50","0.5","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.75,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"AatroxW.png","sprite":"spell0.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"AatroxE","name":"Blades of Torment","description":"Aatrox unleashes the power of his blade, dealing damage to enemies hit and slowing them.","tooltip":"Aatrox unleashes the power of his blade, dealing {{ e1 }} (+{{ a2 }}) physical damage to enemies hit and slowing them by {{ e2 }}% for {{ e3 }} seconds.

Aatrox fills up 20% of the Blood Well upon cast.","leveltip":{"label":["Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[70,110,150,190,230],[30,35,40,45,50],[2,2,2,2,2],[30,30,30,30,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","30/35/40/45/50","2","30","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.7,"key":"a2"}],"costType":" Health","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"AatroxE.png","sprite":"spell0.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ e4 }} Health"},{"id":"AatroxR","name":"Massacre","description":"Aatrox draws in the blood of his foes, damaging all nearby enemy champions around him and gaining increased Attack Speed and bonus Attack Range for a short duration.","tooltip":"Aatrox draws in the blood of his foes, dealing {{ e2 }} (+{{ a1 }}) magic damage to nearby enemy champions, filling up 20% of the Blood Well for each hit.

Aatrox gains {{ e3 }}% attack speed and {{ e5 }} attack range for {{ e8 }} seconds upon cast.","leveltip":{"label":["Damage","Attack Speed","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[0,0,0],"costBurn":"0","effect":[null,[12,12,12],[200,300,400],[40,50,60],[10,10,10],[175,175,175],[50,65,80],[20,20,20],[12,12,12],[0,0,0],[0,0,0]],"effectBurn":[null,"12","200/300/400","40/50/60","10","175","50/65/80","20","12","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"AatroxR.png","sprite":"spell0.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Ahri":{"id":103,"key":"Ahri","name":"Ahri","title":"the Nine-Tailed Fox","spells":[{"id":"AhriOrbofDeception","name":"Orb of Deception","description":"Ahri sends out and pulls back her orb, dealing magic damage on the way out and true damage on the way back. Ahri gains movement speed that decays while her orb is traveling.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage on the way out, and {{ e1 }} (+{{ a1 }}) true damage on the way back.

Ahri gains movement speed that decays while her orb is traveling.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[65,70,75,80,85],"costBurn":"65/70/75/80/85","effect":[null,[40,65,90,115,140],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/65/90/115/140","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"},{"link":"spelldamage","coeff":0.35,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[880,880,880,880,880],"rangeBurn":"880","image":{"full":"AhriOrbofDeception.png","sprite":"spell0.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AhriFoxFire","name":"Fox-Fire","description":"Ahri releases three fox-fires, that lock onto and attack nearby enemies.","tooltip":"Releases three fox-fires that lock on to nearby enemies (prioritizes Champions) dealing {{ e1 }} (+{{ a1 }}) magic damage.

Enemies hit with multiple fox-fires take 30% damage from each additional fox-fire beyond the first, for a maximum of {{ f1 }} damage to a single enemy.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[40,65,90,115,140],[12,19.5,27,34.5,42],[64,104,144,184,224],[30,30,30,30,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/65/90/115/140","12/19.5/27/34.5/42","64/104/144/184/224","30","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"AhriFoxFire.png","sprite":"spell0.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AhriSeduce","name":"Charm","description":"Ahri blows a kiss that damages and charms an enemy it encounters, causing them to walk harmlessly towards her.","tooltip":"Blows a kiss dealing {{ e1 }} (+{{ a1 }}) magic damage and charms an enemy causing them to walk harmlessly towards Ahri for {{ e2 }} second(s).","leveltip":{"label":["Damage","Duration"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[85,85,85,85,85],"costBurn":"85","effect":[null,[60,95,130,165,200],[1,1.25,1.5,1.75,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","1/1.25/1.5/1.75/2","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[975,975,975,975,975],"rangeBurn":"975","image":{"full":"AhriSeduce.png","sprite":"spell0.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AhriTumble","name":"Spirit Rush","description":"Ahri dashes forward and fires essence bolts, damaging 3 nearby enemies (prioritizes Champions). Spirit Rush can be cast up to three times before going on cooldown.","tooltip":"Nimbly dashes forward firing 3 essence bolts at nearby enemies (prioritizes Champions) dealing {{ e1 }} (+{{ a1 }}) magic damage. Can be cast up to three times within 10 seconds before going on cooldown.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,95,80],"cooldownBurn":"110/95/80","cost":[100,100,100],"costBurn":"100","effect":[null,[70,110,150],[10,20,30],[28,23,18],[2,2,2],[1,1,1],[10,10,10],[10,10,10],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"70/110/150","10/20/30","28/23/18","2","1","10","10","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450],"rangeBurn":"450","image":{"full":"AhriTumble.png","sprite":"spell0.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Akali":{"id":84,"key":"Akali","name":"Akali","title":"the Fist of Shadow","spells":[{"id":"AkaliMota","name":"Mark of the Assassin","description":"Akali spins her kama at a target enemy to deal Magic Damage and mark the target for 6 seconds. Akali's melee attacks against a marked target will trigger and consume the mark to cause additional damage and restore Energy.","tooltip":"Akali throws her kama at a target enemy to deal {{ e1 }} (+{{ a1 }}) magic damage and mark the target for {{ e4 }} seconds.

Akali's melee attacks against a marked target will consume the mark to deal {{ e2 }} (+{{ a2 }}) magic damage and restore {{ e3 }} energy.","leveltip":{"label":["Damage (initial)","Damage (secondary)","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[6,5.5,5,4.5,4],"cooldownBurn":"6/5.5/5/4.5/4","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[35,55,75,95,115],[45,70,95,120,145],[40,40,40,40,40],[6,6,6,6,6],[0,0,0,0,0],[0.4,0.4,0.4,0.4,0.4],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/55/75/95/115","45/70/95/120/145","40","6","0","0.4","4","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.5,"key":"a2"}],"costType":" Energy","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"AkaliMota.png","sprite":"spell0.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"AkaliSmokeBomb","name":"Twilight Shroud","description":"Akali teleports to a nearby location, leaving a cover of smoke at her previous location. While inside the shroud, Akali becomes Invisible and gains Movement Speed. Attacking or using abilities will briefly reveal her. Enemies inside the smoke have their Movement Speed reduced.","tooltip":"Akali teleports to a nearby location, leaving a cover of smoke at her previous location that lasts for {{ e2 }} seconds and slows enemies within its area by {{ e3 }}%.

While inside the shroud Akali gains Invisibility and {{ e6 }}% movement speed. Attacking or using abilities briefly removes Invisibility.

16","leveltip":{"label":["Movement Speed","Slow","Cost"],"effect":["{{ e6 }}% -> {{ e6NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[20,20,20,20,20],"cooldownBurn":"20","cost":[60,55,50,45,40],"costBurn":"60/55/50/45/40","effect":[null,[425,425,425,425,425],[8,8,8,8,8],[14,18,22,26,30],[250,250,250,250,250],[0,0,0,0,0],[20,40,60,80,100],[1,1,1,1,1],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"425","8","14/18/22/26/30","250","0","20/40/60/80/100","1","0.5","0","0"],"vars":[],"costType":" Energy","maxammo":"-1","range":[270,270,270,270,270],"rangeBurn":"270","image":{"full":"AkaliSmokeBomb.png","sprite":"spell0.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"AkaliShadowSwipe","name":"Crescent Slash","description":"Akali flourishes her kamas, dealing damage based on her bonus Attack Damage and Ability Power. When Crescent Slash kills a unit, it has a shorter cooldown.","tooltip":"Akali flourishes her kamas, slicing enemies for {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) physical damage.

If Crescent Slash kills an enemy, its cooldown is refunded by {{ e3 }}%.","leveltip":{"label":["Damage","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[5,4.5,4,3.5,3],"cooldownBurn":"5/4.5/4/3.5/3","cost":[60,55,50,45,40],"costBurn":"60/55/50/45/40","effect":[null,[70,100,130,160,190],[1,1,1,1,1],[60,60,60,60,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","1","60","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a2"},{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Energy","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"AkaliShadowSwipe.png","sprite":"spell0.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"AkaliShadowDance","name":"Shadow Dance","description":"Akali moves through shadows to quickly strike through her target, dealing damage and consuming an Essence of Shadow charge. Akali recharges Essence of Shadow charges periodically, max 3 stacks.","tooltip":"Akali quickly strikes through her target, dealing {{ e1 }} (+{{ a1 }}) magic damage.

Akali stores an Essence of Shadow on kills and assists as well as every {{ f1 }} seconds up to {{ e4 }} total.","leveltip":{"label":["Damage","Essence Refresh Timer"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f1 }} -> {{ f2 }}"]},"maxrank":3,"cooldown":[2,2,2],"cooldownBurn":"2","cost":[0,0,0],"costBurn":"0","effect":[null,[50,100,150],[2000,2000,2000],[1,1,1],[3,3,3],[1,1,1],[100,100,100],[200,200,200],[600,600,600],[225,225,225],[1,1,1]],"effectBurn":[null,"50/100/150","2000","1","3","1","100","200","600","225","1"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"},{"link":"@cooldownchampion","coeff":[30,22.5,15],"key":"f1"},{"link":"@cooldownchampion","coeff":[30,22.5,15],"key":"f1"},{"link":"@cooldownchampion","coeff":[22.5,15],"key":"f2"}],"costType":" Essence of Shadow","maxammo":"3","range":[700,700,700],"rangeBurn":"700","image":{"full":"AkaliShadowDance.png","sprite":"spell0.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ e5 }} Essence of Shadow"}]},"Alistar":{"id":12,"key":"Alistar","name":"Alistar","title":"the Minotaur","spells":[{"id":"Pulverize","name":"Pulverize","description":"Alistar smashes the ground, dealing damage to nearby enemies and tossing them into the air.","tooltip":"Alistar smashes the ground, dealing {{ e2 }} (+{{ a1 }}) magic damage and tossing nearby enemy units into the air for {{ e3 }} second.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[17,16,15,14,13],"cooldownBurn":"17/16/15/14/13","cost":[65,70,75,80,85],"costBurn":"65/70/75/80/85","effect":[null,[375,375,375,375,375],[60,105,150,195,240],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"375","60/105/150/195/240","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[365,365,365,365,365],"rangeBurn":"365","image":{"full":"Pulverize.png","sprite":"spell0.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Headbutt","name":"Headbutt","description":"Alistar rams a target with his head, dealing damage and knocking the target back.","tooltip":"Alistar rams into an enemy, dealing {{ e2 }} (+{{ a1 }}) magic damage and knocking them back.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[65,70,75,80,85],"costBurn":"65/70/75/80/85","effect":[null,[0,0,0,0,0],[55,110,165,220,275],[700,700,700,700,700],[0.75,0.75,0.75,0.75,0.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","55/110/165/220/275","700","0.75","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"Headbutt.png","sprite":"spell0.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AlistarE","name":"Trample","description":"Alistar tramples nearby enemy units, ignoring unit colision and gaining stacks if he damages an enemy champion. At full stacks Alistar's next basic attack against an enemy champion deals additional magic damage and stuns them.","tooltip":"Alistar tramples the ground, ignoring unit collision and dealing {{ f1 }} (+{{ f2 }}) magic damage over {{ e3 }} seconds to nearby enemies. Each pulse that damages at least one enemy champion grants Alistar a Trample stack.

At {{ e5 }} Trample stacks Alistar empowers his next basic attack against an enemy champion to deal an additional {{ f3 }} magic damage and stun for {{ e6 }} second.","leveltip":{"label":["Cooldown","Mana Cost","Trample Damage"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[12,11.5,11,10.5,10],"cooldownBurn":"12/11.5/11/10.5/10","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[100,125,150,175,200],[50,50,50,50,50],[5,5,5,5,5],[350,350,350,350,350],[5,5,5,5,5],[1,1,1,1,1],[5,5,5,5,5],[40,40,40,40,40],[15,15,15,15,15],[0,0,0,0,0]],"effectBurn":[null,"100/125/150/175/200","50","5","350","5","1","5","40","15","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"AlistarE.png","sprite":"spell0.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FerociousHowl","name":"Unbreakable Will","description":"Alistar lets out a wild roar, removing all crowd control effects on himself, and reducing incoming physical and magical damage for the duration.","tooltip":"Removes all disables from Alistar. For {{ e1 }} seconds Alistar takes {{ e2 }}% reduced physical and magical damage.","leveltip":{"label":["Damage Reduction","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[7,7,7],[50,60,70],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"7","50/60/70","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1,1,1],"rangeBurn":"1","image":{"full":"FerociousHowl.png","sprite":"spell0.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Amumu":{"id":32,"key":"Amumu","name":"Amumu","title":"the Sad Mummy","spells":[{"id":"BandageToss","name":"Bandage Toss","description":"Amumu tosses a sticky bandage at a target, stunning and damaging the target while he pulls himself to them.","tooltip":"Launches a bandage in a direction. If it hits an enemy unit, Amumu pulls himself to them, dealing {{ e1 }} (+{{ a1 }}) magic damage and stunning for {{ e2 }} second. ","leveltip":{"label":["Damage","Cooldown","Mana"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,14,12,10,8],"cooldownBurn":"16/14/12/10/8","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[80,130,180,230,280],[1,1,1,1,1],[1350,1350,1350,1350,1350],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/130/180/230/280","1","1350","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"BandageToss.png","sprite":"spell0.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AuraofDespair","name":"Despair","description":"Overcome by anguish, nearby enemies lose a percentage of their maximum Health each second and have their Curses refreshed.","tooltip":"Toggle: Amumu cries, refreshing Curses on nearby enemies and dealing magic damage equal to {{ e2 }} plus {{ e1 }} (+{{ a1 }})% of their maximum health each second.","leveltip":{"label":["Percent Health Damaged","Base Damage"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[1,1,1,1,1],"cooldownBurn":"1","cost":[8,8,8,8,8],"costBurn":"8","effect":[null,[1,1.25,1.5,1.75,2],[10,15,20,25,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1/1.25/1.5/1.75/2","10/15/20/25/30","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.01,"key":"a1"}],"costType":" Mana per Second","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"AuraofDespair.png","sprite":"spell0.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana per Second"},{"id":"Tantrum","name":"Tantrum","description":"Permanently reduces the physical damage Amumu would take. Amumu can unleash his rage, dealing damage to surrounding enemies. Each time Amumu is hit, the cooldown on Tantrum is reduced by 0.5 seconds.","tooltip":"Passive: Amumu takes {{ e1 }} reduced damage from physical attacks.

Active: Amumu tantrums, dealing {{ e2 }} (+{{ a1 }}) magic damage to surrounding units.

16","leveltip":{"label":["Damage Reduced","Cooldown","Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[35,35,35,35,35],"costBurn":"35","effect":[null,[2,4,6,8,10],[75,100,125,150,175],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2/4/6/8/10","75/100/125/150/175","0.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"Tantrum.png","sprite":"spell0.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CurseoftheSadMummy","name":"Curse of the Sad Mummy","description":"Amumu entangles surrounding enemy units in bandages, applying his Curse, damaging them and rendering them unable to attack or move.","tooltip":"Amumu entangles nearby enemy units, dealing {{ e1 }} (+{{ a1 }}) magic damage and applying Curse. Entangled enemies are unable to attack or move for 2 seconds.","leveltip":{"label":["Damage Dealt","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,130,110],"cooldownBurn":"150/130/110","cost":[100,150,200],"costBurn":"100/150/200","effect":[null,[150,250,350],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","2","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"CurseoftheSadMummy.png","sprite":"spell0.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Anivia":{"id":34,"key":"Anivia","name":"Anivia","title":"the Cryophoenix","spells":[{"id":"FlashFrost","name":"Flash Frost","description":"Anivia brings her wings together and summons a sphere of ice that flies towards her opponents, chilling and damaging anyone in its path. When the sphere explodes it does moderate damage in a radius, stunning anyone in the area.","tooltip":"A massive chunk of ice flies toward target location, dealing {{ e1 }} (+{{ a1 }}) magic damage.

At the end of its range or if Anivia activates the spell again, the missile detonates, doing {{ e1 }} (+{{ a1 }}) magic damage in a small area and stunning units for {{ e4 }} seconds.

Enemies damaged by Flash Frost are also slowed by {{ f1 }}% for {{ e5 }} seconds.","leveltip":{"label":["Damage","Stun Duration","Cooldown","Mana Cost "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[60,85,110,135,160],[13,12,11,10,9],[0,0,0,0,0],[1.1,1.2,1.3,1.4,1.5],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/85/110/135/160","13/12/11/10/9","0","1.1/1.2/1.3/1.4/1.5","3","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"FlashFrost.png","sprite":"spell0.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Crystallize","name":"Crystallize","description":"Anivia condenses the moisture in the air into an impassable wall of ice to block all movement. The wall only lasts a short duration before it melts.","tooltip":"Anivia summons an impassable wall of ice {{ e2 }} units wide, blocking all movement. The wall lasts for {{ e1 }} seconds before it melts.","leveltip":{"label":["Width"],"effect":["{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[17,17,17,17,17],"cooldownBurn":"17","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[5,5,5,5,5],[400,500,600,700,800],[4,5,6,7,8],[120,120,120,120,120],[250,250,250,250,250],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5","400/500/600/700/800","4/5/6/7/8","120","250","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"Crystallize.png","sprite":"spell0.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Frostbite","name":"Frostbite","description":"With a flap of her wings, Anivia blasts a freezing gust of wind at her target, dealing a low amount of damage. If the target was recently stunned by Flash Frost or damaged by a fully formed Glacial Storm, the damage they take is doubled.","tooltip":"Anivia blasts her target with a freezing wind, dealing {{ e1 }} (+{{ a1 }}) magic damage.

If a target was recently stunned by Anivia or damaged by a fully formed Glacial Storm, they take double damage.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[50,75,100,125,150],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"Frostbite.png","sprite":"spell0.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GlacialStorm","name":"Glacial Storm","description":"Anivia summons a driving rain of ice and hail to damage her enemies and slow their advance.","tooltip":"Toggle: Drains {{ e2 }} Mana per second.

Anivia calls forth a driving rain of ice and hail that increases in size over {{ e7 }} seconds, dealing {{ e1 }} (+{{ a1 }}) magic damage per second to targets and slowing their Movement Speed by {{ e4 }}%.

When the Glacial Storm is fully formed, it slows targets' Movement Speed by {{ f1 }}% and does {{ e3 }}% damage instead.","leveltip":{"label":["Damage Per Second","Chilled Slow Amount","Mana Cost Per Second"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":3,"cooldown":[6,6,6],"cooldownBurn":"6","cost":[75,75,75],"costBurn":"75","effect":[null,[40,60,80],[40,50,60],[300,300,300],[20,30,40],[0,0,0],[1,1,1],[1.5,1.5,1.5],[50,50,50],[800,800,800],[1000,1000,1000]],"effectBurn":[null,"40/60/80","40/50/60","300","20/30/40","0","1","1.5","50","800","1000"],"vars":[{"link":"spelldamage","coeff":0.125,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[750,750,750],"rangeBurn":"750","image":{"full":"GlacialStorm.png","sprite":"spell1.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Annie":{"id":1,"key":"Annie","name":"Annie","title":"the Dark Child","spells":[{"id":"Disintegrate","name":"Disintegrate","description":"Annie hurls a Mana infused fireball, dealing damage and refunding the Mana cost if it destroys the target.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage. Mana cost and half the cooldown are refunded if Disintegrate kills the target.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[80,115,150,185,220],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/115/150/185/220","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"Disintegrate.png","sprite":"spell1.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Incinerate","name":"Incinerate","description":"Annie casts a blazing cone of fire, dealing damage to all enemies in the area.","tooltip":"Casts a cone of fire dealing {{ e1 }} (+{{ a1 }}) magic damage to all enemies in the area.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[70,115,160,205,250],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.85,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"Incinerate.png","sprite":"spell1.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MoltenShield","name":"Molten Shield","description":"Grants Annie and Tibbers increased percentage Damage Resist and damages enemies who attack with basic attacks.","tooltip":"Annie grants herself and Tibbers {{ e1 }}% damage reduction for {{ e3 }} seconds.

While the shield is active, enemies who basic attack it take {{ e2 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage Reduction","Damage Return"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[16,22,28,34,40],[20,30,40,50,60],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"16/22/28/34/40","20/30/40/50/60","3","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[0,0,0,0,0],"rangeBurn":"0","image":{"full":"MoltenShield.png","sprite":"spell1.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"InfernalGuardian","name":"Summon: Tibbers","description":"Annie wills her bear Tibbers to life, dealing damage to units in the area. Tibbers can attack and also burns enemies that stand near him.","tooltip":"Summons Tibbers, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies in the target area. For the next 45 seconds, Tibbers burns nearby enemies for {{ e2 }} (+{{ a2 }}) per second and attacks for {{ e3 }} (+{{ f1 }}) as magic damage. Annie can control Tibbers by reactivating this ability.

Tibbers Enrages when: summoned; Annie uses Pyromania on an enemy Champion; and when Annie dies.

Enrages: Tibbers gains 275% Attack Speed and 100% Movement Speed, decaying over 3 seconds.","leveltip":{"label":["Damage","Tibbers Health","Tibbers Armor and Magic Resist","Tibbers Attack Damage","Tibbers Burn Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e0 }} -> {{ e0NL }}","{{ e7 }} -> {{ e7NL }}","{{ e3 }} -> {{ e3NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[150,275,400],[10,15,20],[50,75,100],[-0.1,-0.1,-0.1],[1,1,1],[3,3,3],[30,50,70],[0,900,1800],[0.15,0.15,0.15],[1200,2100,3000]],"effectBurn":[null,"150/275/400","10/15/20","50/75/100","-0.1","1","3","30/50/70","0/900/1800","0.15","1200/2100/3000"],"vars":[{"link":"spelldamage","coeff":0.65,"key":"a1"},{"link":"spelldamage","coeff":0.1,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600],"rangeBurn":"600","image":{"full":"InfernalGuardian.png","sprite":"spell1.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Ashe":{"id":22,"key":"Ashe","name":"Ashe","title":"the Frost Archer","spells":[{"id":"AsheQ","name":"Ranger's Focus","description":"Ashe builds up Focus by attacking. At maximum Focus, Ashe can cast Ranger's Focus to consume all stacks of Focus, temporarily increasing her Attack Speed and transforming her basic attack into a powerful flurry attack for the duration.","tooltip":"Passive: Basic attacks grant Focus for {{ e1 }} seconds, stacking up to {{ e2 }} times. Stacks fall off one at a time, and at {{ e2 }} stacks, Ranger's Focus can be cast, consuming all Focus.

Active: For {{ e3 }} seconds, Ashe gains {{ e4 }}% Attack Speed, and her basic attacks fire a flurry of arrows dealing {{ f1 }} physical damage. During this time, she does not stack Focus. Ranger's Focus applies Frost Shot.","leveltip":{"label":["Attack Speed Bonus","Flurry Attack Damage ratio"],"effect":["{{ e4 }}% -> {{ e4NL }}%","{{ e6 }} -> {{ e6NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[4,4,4,4,4],[4,4,4,4,4],[4,4,4,4,4],[20,25,30,35,40],[0.21,0.22,0.23,0.24,0.25],[1.05,1.1,1.15,1.2,1.25],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[0,0,0,0,0]],"effectBurn":[null,"4","4","4","20/25/30/35/40","0.21/0.22/0.23/0.24/0.25","1.05/1.1/1.15/1.2/1.25","1","1","1","0"],"vars":[],"costType":" Mana, {{ e2 }} Focus","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"AsheQ.png","sprite":"spell1.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana, {{ e2 }} Focus"},{"id":"Volley","name":"Volley","description":"Ashe fires 9 arrows in a cone for increased damage. Also applies Frost Shot.","tooltip":"Fires arrows in a cone, each dealing {{ e2 }} (+{{ a1 }}) physical damage. Enemies can block multiple arrows, but only take damage from the first.

Champion hits count as Critical Strikes for the purposes of Frost Shot.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,12.5,10,7.5,5],"cooldownBurn":"15/12.5/10/7.5/5","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[5,7,9,11,13],[20,35,50,65,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/7/9/11/13","20/35/50/65/80","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200,1200,1200],"rangeBurn":"1200","image":{"full":"Volley.png","sprite":"spell1.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AsheSpiritOfTheHawk","name":"Hawkshot","description":"Ashe sends her Hawk Spirit on a scouting mission anywhere on the map.","tooltip":"Reveals terrain as it flies toward target location anywhere on the map. Grants vision for {{ e4 }} seconds. Ashe can store up to 2 charges of Hawkshot.","leveltip":{"label":["Recharge Time"],"effect":["{{ ammorechargetime }} -> {{ ammorechargetimeNL }}"]},"maxrank":5,"cooldown":[5,5,5,5,5],"cooldownBurn":"5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[3,3,3,3,3],[50,90,130,170,210],[25000,25000,25000,25000,25000],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"3","50/90/130/170/210","25000","5","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"2","range":[25000,25000,25000,25000,25000],"rangeBurn":"25000","image":{"full":"AsheSpiritOfTheHawk.png","sprite":"spell1.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"EnchantedCrystalArrow","name":"Enchanted Crystal Arrow","description":"Ashe fires a missile of ice in a straight line. If the arrow collides with an enemy Champion, it deals damage and stuns the Champion, stunning for longer the farther arrow has traveled. In addition, surrounding enemy units take damage and are slowed.","tooltip":"Launches a crystal arrow of ice that stuns the first enemy Champion hit, dealing {{ e1 }} (+{{ a1 }}) magic damage. The farther the arrow flies, the longer the stun, up to {{ e2 }} seconds. Surrounding enemies take half damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[200,400,600],[3.5,3.5,3.5],[50,50,50],[3,3,3],[5,5,5],[1100,1200,1300],[1,1,1],[400,400,400],[0.5,0.5,0.5],[0,0,0]],"effectBurn":[null,"200/400/600","3.5","50","3","5","1100/1200/1300","1","400","0.5","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"EnchantedCrystalArrow.png","sprite":"spell1.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"AurelionSol":{"id":136,"key":"AurelionSol","name":"Aurelion Sol","title":"The Star Forger","spells":[{"id":"AurelionSolQ","name":"Starsurge","description":"Aurelion Sol creates an expanding disk, which explodes to stun and damage enemies when it moves too far away from him.","tooltip":"First Press: Aurelion Sol creates a new stellar core, which grows over time and grants him {{ e2 }}% Movement Speed.

The core will detonate when it reaches his Outer Limit, applying {{ e1 }} (+{{ a1 }}) magic damage and a {{ e4 }} second stun.

Second Press: Detonate the core early.
","leveltip":{"label":["Damage","Stun Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[70,110,150,190,230],[10,10,10,10,10],[20,20,20,20,20],[1.1,1.2,1.3,1.4,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","10","20","1.1/1.2/1.3/1.4/1.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.65,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"AurelionSolQ.png","sprite":"spell1.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AurelionSolW","name":"Celestial Expansion","description":"Aurelion Sol pushes his stars farther out, magnifying their damage.","tooltip":"Passive: Increases Star base damage by {{ e2 }}.

Toggle: Aurelion Sol's Stars orbit at his Outer Limit and deal {{ e1 }}% damage, for a total of {{ f1 }} (+{{ f2 }}) magic damage.
","leveltip":{"label":["Passive Damage Bonus","Mana Cost Per Second","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[6,5.5,5,4.5,4],"cooldownBurn":"6/5.5/5/4.5/4","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[150,150,150,150,150],[5,10,15,20,25],[22,24,26,28,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"150","5/10/15/20/25","22/24/26/28/30","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana plus {{ e3 }} Mana Per Second","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"AurelionSolW.png","sprite":"spell1.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana plus {{ e3 }} Mana Per Second"},{"id":"AurelionSolE","name":"Comet of Legend","description":"Aurelion Sol gains speed while moving in one continuous direction, and can take off flying for a long distance.","tooltip":"Passive: Continuously moving in one direction grants increasing Movement Speed up to {{ e2 }}%.

Active: Fly for {{ e3 }} units in the chosen direction. Only castable outside of combat. Aurelion Sol can see and be seen over walls while flying.

Taking champion or turret damage will force a landing and reset the passive movement speed.
","leveltip":{"label":["Movement Speed Cap ","Flight Range","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[60,55,50,45,40],"cooldownBurn":"60/55/50/45/40","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[5,6,7,8,9],[25,30,35,40,45],[3000,4000,5000,6000,7000],[600,600,600,600,600],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/6/7/8/9","25/30/35/40/45","3000/4000/5000/6000/7000","600","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[3000,4000,5000,6000,7000],"rangeBurn":"3000/4000/5000/6000/7000","image":{"full":"AurelionSolE.png","sprite":"spell1.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AurelionSolR","name":"Voice of Light","description":"Aurelion Sol projects a blast of pure starfire, damaging and slowing all enemies caught in it and knocking nearby enemies back to a safer distance.","tooltip":"Breathes out a blast of pure starfire, dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing by {{ e2 }}% for {{ e3 }} seconds.

The blast will also knock nearby enemies back to Aurelion Sol's Outer Limit.
","leveltip":{"label":["Damage","Slow ","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[40,50,60],[2,2,2],[0,0,0],[650,650,650],[50,50,50],[1300,1300,1300],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","40/50/60","2","0","650","50","1300","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1500,1500,1500],"rangeBurn":"1500","image":{"full":"AurelionSolR.png","sprite":"spell1.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Azir":{"id":268,"key":"Azir","name":"Azir","title":"the Emperor of the Sands","spells":[{"id":"AzirQWrapper","name":"Conquering Sands","description":"Azir sends all Sand Soldiers towards a location. Sand Soldiers deal magic damage to enemies they pass through and apply a slow for 1 second.","tooltip":"Azir sends all Sand Soldiers towards a location. Sand Soldiers deal {{ e1 }} (+{{ a1 }}) magic damage to enemies they pass through and apply a {{ e2 }}% slow for 1 second.

Enemies hit by multiple Sand Soldiers will not take additional damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f1 }} -> {{ f2 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[65,85,105,125,145],[25,25,25,25,25],[10,9,8,7,6],[70,70,70,70,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/85/105/125/145","25","10/9/8/7/6","70","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[875,875,875,875,875],"rangeBurn":"875","image":{"full":"AzirQWrapper.png","sprite":"spell1.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AzirW","name":"Arise!","description":"Azir summons a Sand Soldier to attack nearby targets for him, replacing his basic attack against targets within the soldier's range. Their attacks deal magic damage to enemies in a line. Arise! also passively grants attack speed to Azir and his Sand Soldiers.","tooltip":"Passive: Gains {{ e3 }}% attack speed.

Active: Azir summons a Sand Soldier for {{ e1 }} seconds. When Azir attacks an enemy in a soldier's range, the soldier attacks instead of Azir, dealing {{ f2 }} (+{{ a1 }}) magic damage to enemies in a line. If multiple soldiers strike the same target, each soldier after the first deals 25% damage.

Azir can store up to {{ maxammo }} Sand Soldiers at a time. A new soldier becomes available every {{ f1 }} seconds.

Sand Soldiers can attack targets outside of Azir's basic attack range.
Sand Soldiers deactivate if they are too far away from Azir.
Sand Soldiers expire twice as fast when near an enemy turret.
","leveltip":{"label":["Recharge","Attack Speed"],"effect":["{{ f1 }} -> {{ f3 }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[9,9,9,9,9],[12,11,10,9,8],[20,30,40,50,60],[40,40,40,40,40],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"9","12/11/10/9/8","20/30/40/50/60","40","1.5","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"2","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"AzirW.png","sprite":"spell1.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AzirEWrapper","name":"Shifting Sands","description":"Azir dashes to one of his Sand Soldiers, damaging enemies. If he hits an enemy champion, he gains a shield.","tooltip":"Azir dashes to one of his Sand Soldiers, damaging enemies hit for {{ e3 }} (+{{ a1 }}) magic damage.

If Azir hits an enemy champion, his dash stops and he gains a shield for {{ e6 }} seconds that absorbs up to {{ e4 }} damage.","leveltip":{"label":["Damage","Cooldown","Shield"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ f2 }} -> {{ f3 }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[0.5,0.5,0.5,0.5,0.5],[19,18,17,16,15],[60,90,120,150,180],[80,120,160,200,240],[-0.5,-0.6,-0.7,-0.8,-0.9],[4,4,4,4,4],[1100,1100,1100,1100,1100],[60,60,60,60,60],[0.15,0.15,0.15,0.15,0.15],[0,0,0,0,0]],"effectBurn":[null,"0.5","19/18/17/16/15","60/90/120/150/180","80/120/160/200/240","-0.5/-0.6/-0.7/-0.8/-0.9","4","1100","60","0.15","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"AzirEWrapper.png","sprite":"spell1.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AzirR","name":"Emperor's Divide","description":"Azir summons a wall of soldiers which charge forward, knocking back and damaging enemies.","tooltip":"Azir summons a wall of armored soldiers that charge forward, knocking back enemies and dealing {{ e1 }} (+{{ a1 }}) magic damage. The soldiers then remain as a wall for {{ e4 }} seconds.

Enemies will be stopped by Emperor's Divide, even if they attempt to dash over the wall, but Azir and his allies can pass freely through it.

Emperor's Divide does not interact with Azir's basic attacks or spells.","leveltip":{"label":["Damage","Number of Soldiers","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,225,300],[0,0,0],[4,5,6],[3,3,3],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/225/300","0","4/5/6","3","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[250,250,250],"rangeBurn":"250","image":{"full":"AzirR.png","sprite":"spell1.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Bard":{"id":432,"key":"Bard","name":"Bard","title":"the Wandering Caretaker","spells":[{"id":"BardQ","name":"Cosmic Binding","description":"Bard fires a missile which will slow the first enemy struck, and continue onward. If it strikes a wall, it will stun the initial target; if it strikes another enemy, it will stun them both.","tooltip":"Bard fires an energy bolt, dealing {{ e1 }} (+{{ a1 }}) magic damage to one or two enemies. The first target hit will be slowed by {{ e3 }}% for {{ e4 }} second(s).

If the bolt hits another enemy or a wall, any enemies hit are stunned for {{ e2 }} second(s).
","leveltip":{"label":["Damage","Slow Duration","Stun Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[80,125,170,215,260],[1,1.2,1.4,1.6,1.8],[60,60,60,60,60],[1,1.2,1.4,1.6,1.8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/125/170/215/260","1/1.2/1.4/1.6/1.8","60","1/1.2/1.4/1.6/1.8","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.65,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000,25000,25000],"rangeBurn":"25000","image":{"full":"BardQ.png","sprite":"spell1.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BardW","name":"Caretaker's Shrine","description":"Reveals a healing shrine which powers up over a short time, disappearing after healing the first ally that touches it.","tooltip":"Bard raises a health shrine that immediately offers {{ e5 }} (+{{ a1 }}) health, but restores up to {{ e6 }} (+{{ a2 }}) health as it gathers power for 10 seconds. The shrine's effect also grants {{ e1 }}% decaying movement speed for {{ e2 }} seconds.

Bard can have up to {{ e3 }} shrines active at once, which remain until visited by an ally champion or crushed by an enemy champion.

Active Shrines: {{ f1 }} / {{ f2 }}
","leveltip":{"label":["Base Heal","Max Heal"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ e6 }} -> {{ e6NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[90,90,90,90,90],"costBurn":"90","effect":[null,[50,50,50,50,50],[1.5,1.5,1.5,1.5,1.5],[3,3,3,3,3],[0,0,0,0,0],[30,60,90,120,150],[70,110,150,190,230],[0,0,0,0,0],[10,10,10,10,10],[90,90,90,90,90],[0,0,0,0,0]],"effectBurn":[null,"50","1.5","3","0","30/60/90/120/150","70/110/150/190/230","0","10","90","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"BardW.png","sprite":"spell1.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BardE","name":"Magical Journey","description":"Bard opens a portal in nearby terrain. Allies and enemies alike can take a one-way trip through that terrain by moving into the portal.","tooltip":"Bard opens a one-way corridor through nearby terrain. Both allies and enemies can use the corridor by right-clicking on any part of it while near its entrance, with allies travelling {{ e2 }}% faster than enemies.

The corridor disappears after {{ e1 }} seconds.
","leveltip":{"label":["Cooldown","Ally Travel Speed Bonus"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[10,10,10,10,10],[10,20,30,40,50],[800,800,800,800,800],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10","10/20/30/40/50","800","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"BardE.png","sprite":"spell1.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BardR","name":"Tempered Fate","description":"Bard sends spirit energy arcing to a location, putting all champions, minions, monsters, and turrets hit into stasis for a brief time.","tooltip":"Bard sends magical energy arcing to a target location. On impact, all champions, minions, monsters, and turrets in the target area are put in stasis, becoming invincible, untargetable, and unable to act for {{ e1 }} seconds.

Epic monsters are also put into stasis, despite normally being immune to disables.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,115,90],"cooldownBurn":"130/115/90","cost":[100,100,100],"costBurn":"100","effect":[null,[2.5,2.5,2.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"2.5","0","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[3400,3400,3400],"rangeBurn":"3400","image":{"full":"BardR.png","sprite":"spell1.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Blitzcrank":{"id":53,"key":"Blitzcrank","name":"Blitzcrank","title":"the Great Steam Golem","spells":[{"id":"RocketGrab","name":"Rocket Grab","description":"Blitzcrank fires his right hand to grab an opponent on its path, dealing damage and dragging it back to him.","tooltip":"Blitzcrank fires his right hand. If it encounters an enemy unit it will stun them and deal {{ e1 }} (+{{ a1 }}) magic damage while he pulls them to himself.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,19,18,17,16],"cooldownBurn":"20/19/18/17/16","cost":[100,100,100,100,100],"costBurn":"100","effect":[null,[80,135,190,245,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/135/190/245/300","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"RocketGrab.png","sprite":"spell1.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Overdrive","name":"Overdrive","description":"Blitzcrank super charges himself to get dramatically increased Movement and Attack Speed. He is temporarily slowed after the effect ends.","tooltip":"Blitzcrank supercharges himself, gaining {{ e1 }}% Movement Speed and {{ e2 }}% Attack Speed for {{ e4 }} seconds. The Movement Speed bonus decays over the duration.

When Overdrive ends, Blitzcrank's Movement Speed is slowed by {{ e3 }}% for {{ e5 }} seconds.","leveltip":{"label":["Movement Speed","Attack Speed"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[75,75,75,75,75],"costBurn":"75","effect":[null,[70,75,80,85,90],[30,38,46,54,62],[30,30,30,30,30],[5,5,5,5,5],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/75/80/85/90","30/38/46/54/62","30","5","1.5","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"Overdrive.png","sprite":"spell1.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PowerFist","name":"Power Fist","description":"Blitzcrank charges up his fist to make his next attack deal double damage and pop his target up in the air.","tooltip":"Blitzcrank charges up his fist to make his next attack deal double his total attack damage as physical damage and pop his target up in the air.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[25,25,25,25,25],"costBurn":"25","effect":[null,[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","0","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"PowerFist.png","sprite":"spell1.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"StaticField","name":"Static Field","description":"Passively causes lightning bolts to damage a nearby enemy. Additionally, Blitzcrank can activate this ability to damage nearby enemies and silence them for 0.5 seconds, but doing so removes the passive lightning until Static Field becomes available again.","tooltip":"Passive: Lightning arcs off of Blitzcrank to hit a random nearby enemy for {{ e1 }} (+{{ a1 }}) magic damage every 2.5 seconds.

Active: Deals {{ e2 }} (+{{ a2 }}) magic damage and silences surrounding enemy units for 0.5 seconds. The passive is not in effect during the cooldown.","leveltip":{"label":["Lightning Damage","Activation Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[60,40,20],"cooldownBurn":"60/40/20","cost":[100,100,100],"costBurn":"100","effect":[null,[100,200,300],[250,375,500],[2.5,2.5,2.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/200/300","250/375/500","2.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":1,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600],"rangeBurn":"600","image":{"full":"StaticField.png","sprite":"spell1.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Brand":{"id":63,"key":"Brand","name":"Brand","title":"the Burning Vengeance","spells":[{"id":"BrandQ","name":"Sear","description":"Brand launches a ball of fire forward that deals magic damage. If the target is ablaze, Sear will stun the target for 1.5 seconds.","tooltip":"Brand launches a ball of fire forward that deals {{ e1 }} (+{{ a1 }}) magic damage.

Blaze: If the target is ablaze, Sear will stun the target for 1.5 seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7.5,7,6.5,6],"cooldownBurn":"8/7.5/7/6.5/6","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[80,110,140,170,200],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/110/140/170/200","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.55,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"BrandQ.png","sprite":"spell1.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BrandW","name":"Pillar of Flame","description":"After a short delay, Brand creates a Pillar of Flame at a target area, dealing magic damage to enemy units within the area. Units that are ablaze take an additional 25% damage.","tooltip":"After a short delay, Brand creates a Pillar of Flame at a target area, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemy units within the area.

Blaze: Units that are ablaze take an additional 25% damage from Pillar of Flame.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9.5,9,8.5,8],"cooldownBurn":"10/9.5/9/8.5/8","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[75,120,165,210,255],[20,40,60,80,100],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/120/165/210/255","20/40/60/80/100","0.25","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"BrandW.png","sprite":"spell1.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BrandE","name":"Conflagration","description":"Brand conjures a powerful blast at his target, dealing magic damage to them. If the target is ablaze, Conflagration spreads to nearby enemies.","tooltip":"Brand conjures a powerful blast at his target, dealing {{ e1 }} (+{{ a1 }}) magic damage.

Blaze: If the target is ablaze, Conflagration spreads to nearby enemies. ","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[70,90,110,130,150],[375,375,375,375,375],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/90/110/130/150","375","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"BrandE.png","sprite":"spell1.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BrandR","name":"Pyroclasm","description":"Brand unleashes a devastating torrent of fire, dealing magic damage each time it bounces, up to 5 bounces. Bounces prioritize stacking Blaze to max on Champions. If a target is ablaze, Pyroclasm will briefly slow them.","tooltip":"Brand unleashes a devastating torrent of fire, dealing {{ e1 }} (+{{ a1 }}) magic damage each time it bounces up to 5 bounces. Bounces prioritize stacking Blaze to max on Champions.

Blaze: If the target is ablaze, Pyroclasm will briefly slow the target by {{ e4 }}%.","leveltip":{"label":["Damage per Bounce","Slow Amount","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[105,90,75],"cooldownBurn":"105/90/75","cost":[100,100,100],"costBurn":"100","effect":[null,[100,200,300],[5,7,9],[150,225,300],[30,45,60],[0.25,0.25,0.25],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/200/300","5/7/9","150/225/300","30/45/60","0.25","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[750,750,750],"rangeBurn":"750","image":{"full":"BrandR.png","sprite":"spell1.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Braum":{"id":201,"key":"Braum","name":"Braum","title":"the Heart of the Freljord","spells":[{"id":"BraumQ","name":"Winter's Bite","description":"Braum propels freezing ice from his shield, slowing and dealing magic damage.

Applies a stack of Concussive Blows.","tooltip":"Braum propels freezing ice from his shield dealing {{ e1 }} (+{{ f1 }}) [2.5% of Braum's Max Health] magic damage to the first enemy hit and slowing them by {{ e2 }}%, decaying over the next {{ e5 }} seconds.

Applies a stack of Concussive Blows. ","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[55,60,65,70,75],"costBurn":"55/60/65/70/75","effect":[null,[60,105,150,195,240],[70,70,70,70,70],[30,30,30,30,30],[0.02,0.02,0.02,0.02,0.02],[2,2,2,2,2],[1050,1050,1050,1050,1050],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","70","30","0.02","2","1050","4","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"BraumQ.png","sprite":"spell1.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BraumW","name":"Stand Behind Me","description":"Braum leaps to a target allied champion or minion. On arrival, Braum and the ally gain Armor and Magic Resist for a few seconds.","tooltip":"Braum leaps to target allied champion or minion.

On arrival, Braum and the ally gain {{ f3 }} Armor and {{ f4 }} Magic Resist ({{ e4 }} plus {{ e3 }}% of Braum's bonus Armor/Magic Resist) for {{ e1 }} seconds.","leveltip":{"label":["Base Armor and Magic Resist","Defensive Scaling","Cooldown","Mana Cost"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[3,3,3,3,3],[750,750,750,750,750],[10,11.5,13,14.5,16],[15,17.5,20,22.5,25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"3","750","10/11.5/13/14.5/16","15/17.5/20/22.5/25","0","0","0","0","0","0"],"vars":[{"link":"@special.BraumWArmor","coeff":0,"key":"f3"},{"link":"@special.BraumWMR","coeff":0,"key":"f4"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"BraumW.png","sprite":"spell1.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BraumE","name":"Unbreakable","description":"Braum raises his shield in a direction for several seconds, intercepting all projectiles causing them to hit him and be destroyed. He negates the damage of the first attack completely and reduces the damage of all subsequent attacks from this direction.","tooltip":"Braum reduces incoming damage and blocks for allies behind him.
Braum raises his shield in a direction for {{ e2 }} seconds negating the damage of the next attack from that direction. Subsequent attacks deal {{ e3 }}% reduced damage.

Braum intercepts projectiles, causing them to hit him and be destroyed.

Braum gains {{ e4 }}% Movement Speed for the duration.","leveltip":{"label":["Duration","Damage Reduction","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[30,35,40,45,50],"costBurn":"30/35/40/45/50","effect":[null,[0,0,0,0,0],[3,3.25,3.5,3.75,4],[30,32.5,35,37.5,40],[10,10,10,10,10],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","3/3.25/3.5/3.75/4","30/32.5/35/37.5/40","10","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000,25000,25000],"rangeBurn":"25000","image":{"full":"BraumE.png","sprite":"spell1.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BraumRWrapper","name":"Glacial Fissure","description":"Braum slams the ground, knocking up enemies nearby and in a line in front of him. A fissure is left along the line that slows enemies.","tooltip":"Braum slams the ground, knocking up enemies nearby and in a line in front of him. A fissure is left along the line for {{ e3 }} seconds, slowing enemies above it by {{ e4 }}%.

Enemies hit take {{ e1 }} (+{{ a1 }}) magic damage. The first champion hit is knocked up for {{ e5 }} seconds, subsequent enemies are knocked up briefly.","leveltip":{"label":["Damage","Knockup Duration","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[0.3,0.3,0.3],[4,4,4],[40,50,60],[1,1.25,1.5],[0.25,0.25,0.25],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","0.3","4","40/50/60","1/1.25/1.5","0.25","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1250,1250,1250],"rangeBurn":"1250","image":{"full":"BraumRWrapper.png","sprite":"spell1.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Caitlyn":{"id":51,"key":"Caitlyn","name":"Caitlyn","title":"the Sheriff of Piltover","spells":[{"id":"CaitlynPiltoverPeacemaker","name":"Piltover Peacemaker","description":"Caitlyn revs up her rifle for 1 second to unleash a penetrating shot that deals physical damage (deals less damage to subsequent targets).","tooltip":"Revs the rifle for {{ e4 }} second to fire a narrow piercing shot dealing {{ e1 }} (+{{ f1 }}) physical damage. After hitting any target, it opens into a wider shot that deals {{ e2 }}% less damage.

Always deals full damage to trap revealed targets.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[30,70,110,150,190],[33,33,33,33,33],[50,50,50,50,50],[1,1,1,1,1],[1.3,1.4,1.5,1.6,1.7],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/70/110/150/190","33","50","1","1.3/1.4/1.5/1.6/1.7","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1250,1250,1250,1250,1250],"rangeBurn":"1250","image":{"full":"CaitlynPiltoverPeacemaker.png","sprite":"spell1.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CaitlynYordleTrap","name":"Yordle Snap Trap","description":"Caitlyn sets a trap to find sneaky yordles. When sprung, the trap reveals and immobilizes the enemy champion for 1.5 seconds.","tooltip":"Sets traps that an enemy Champion can spring, immobilizing them for {{ e1 }} seconds and granting True Sight for a short duration.

Traps last for {{ e3 }} seconds. {{ e5 }} traps may be active at once.

Trapped enemies take an additional {{ e2 }} (+{{ f1 }}) increased damage from Headshot.","leveltip":{"label":["Recharge Rate","Maximum Traps","Bonus Headshot Damage on Trapped Targets"],"effect":["{{ ammorechargetime }} -> {{ ammorechargetimeNL }}","{{ e4 }} -> {{ e4NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[2,2,2,2,2],[30,70,110,150,190],[90,90,90,90,90],[3,3,4,4,5],[3,3,4,4,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2","30/70/110/150/190","90","3/3/4/4/5","3/3/4/4/5","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"2","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"CaitlynYordleTrap.png","sprite":"spell1.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CaitlynEntrapment","name":"90 Caliber Net","description":"Caitlyn fires a heavy net to slow her target. The recoil knocks Caitlyn back.","tooltip":"Fires a net, knocking Caitlyn backwards. The net deals {{ e1 }} (+{{ a1 }}) magic damage and slows the first enemy hit by {{ e3 }}% for {{ e2 }} second.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,14.5,13,11.5,10],"cooldownBurn":"16/14.5/13/11.5/10","cost":[75,75,75,75,75],"costBurn":"75","effect":[null,[70,110,150,190,230],[1,1,1,1,1],[50,50,50,50,50],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","1","50","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"CaitlynEntrapment.png","sprite":"spell1.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CaitlynAceintheHole","name":"Ace in the Hole","description":"Caitlyn takes time to line up the perfect shot, dealing massive damage to a single target at a huge range. Enemy champions can intercept the bullet for their ally.","tooltip":"Takes a second to line up the perfect shot on an enemy Champion at up to {{ e2 }} range. The shot deals {{ e1 }} (+{{ f1 }}) physical damage, but other enemy Champions can intercept it.

Grants True Sight of the target during the channel.","leveltip":{"label":["Damage","Range","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[90,75,60],"cooldownBurn":"90/75/60","cost":[100,100,100],"costBurn":"100","effect":[null,[250,475,700],[2000,2500,3000],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250/475/700","2000/2500/3000","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":2,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[2000,2500,3000],"rangeBurn":"2000/2500/3000","image":{"full":"CaitlynAceintheHole.png","sprite":"spell1.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Camille":{"id":164,"key":"Camille","name":"Camille","title":"the Steel Shadow","spells":[{"id":"CamilleQ","name":"Precision Protocol","description":"Camille's next attack deals bonus damage and grants bonus movement speed. This spell can be recast for a short period of time, doing significantly increased bonus damage if Camille delays a period of time between the two attacks.","tooltip":"Camille's next basic attack deals {{ f1 }} bonus physical damage and increases her movement speed by {{ e4 }}% for {{ e5 }} second. This ability can be recast in the next {{ e1 }} seconds at no cost.

If the second Precision Protocol attack hits at least {{ e1 }} seconds after the first, the bonus damage is increased by {{ e8 }}% to {{ f2 }} and {{ f3 }}% of the attack is converted into true damage.

16","leveltip":{"label":["Total AD Ratio","Cooldown","Movement Speed"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":5,"cooldown":[9,8.25,7.5,6.75,6],"cooldownBurn":"9/8.25/7.5/6.75/6","cost":[25,25,25,25,25],"costBurn":"25","effect":[null,[3,3,3,3,3],[50,50,50,50,50],[20,25,30,35,40],[20,25,30,35,40],[1,1,1,1,1],[0.4,0.4,0.4,0.4,0.4],[0.04,0.04,0.04,0.04,0.04],[80,80,80,80,80],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"3","50","20/25/30/35/40","20/25/30/35/40","1","0.4","0.04","80","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"CamilleQ.png","sprite":"spell1.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CamilleW","name":"Tactical Sweep","description":"Camille blasts in a cone after a delay, dealing damage. Enemies in the outer half are slowed and take extra damage, while also healing Camille.","tooltip":"Camille winds up and then slices in a direction, dealing {{ e1 }} (+{{ f1 }}) physical damage to enemies hit.

Enemies hit by the outer half are slowed by {{ e4 }}%, decaying over {{ e2 }} seconds. They take extra physical damage equal to {{ e5 }}% (+{{ f2 }}%) of their maximum health, and Camille heals for {{ e9 }}% of the bonus damage dealt to champions.

16","leveltip":{"label":["Mana Cost","Damage","Maximum Health Damage","Cooldown"],"effect":["{{ cost }} -> {{ costNL }}","{{ e1 }} -> {{ e1NL }}","{{ e5 }}% -> {{ e5NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,13.5,12,10.5,9],"cooldownBurn":"15/13.5/12/10.5/9","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[65,95,125,155,185],[2,2,2,2,2],[0.75,0.75,0.75,0.75,0.75],[80,80,80,80,80],[6,6.5,7,7.5,8],[650,650,650,650,650],[30,30,30,30,30],[50,50,50,50,50],[100,100,100,100,100],[0.1,0.1,0.1,0.1,0.1]],"effectBurn":[null,"65/95/125/155/185","2","0.75","80","6/6.5/7/7.5/8","650","30","50","100","0.1"],"vars":[],"costType":" Mana","maxammo":"-1","range":[610,610,610,610,610],"rangeBurn":"610","image":{"full":"CamilleW.png","sprite":"spell1.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CamilleE","name":"Hookshot","description":"Camille pulls herself to a wall, leaping off and knocking up enemies upon landing.","tooltip":"First Cast: Fire a hookshot that attaches to terrain, pulling Camille to it and allowing Hookshot to be recast for 1 second.

Second Cast: Camille leaps from the wall, colliding with the first enemy champion hit. Upon landing, she deals {{ e3 }} (+{{ f1 }}) physical damage and a {{ e1 }} second stun to nearby enemies. Dashes towards enemy champions travel twice as far and grant {{ e2 }}% attack speed for {{ e5 }} seconds on impact.","leveltip":{"label":["Cooldown","Damage","Attack Speed"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e3 }} -> {{ e3NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[16,14.5,13,11.5,10],"cooldownBurn":"16/14.5/13/11.5/10","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[0.75,0.75,0.75,0.75,0.75],[40,50,60,70,80],[70,115,160,205,250],[1050,1050,1050,1050,1050],[5,5,5,5,5],[1,1,1,1,1],[800,800,800,800,800],[400,400,400,400,400],[130,130,130,130,130],[0,0,0,0,0]],"effectBurn":[null,"0.75","40/50/60/70/80","70/115/160/205/250","1050","5","1","800","400","130","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"CamilleE.png","sprite":"spell1.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CamilleR","name":"The Hextech Ultimatum","description":"Camille dashes to target champion, anchoring them to the area. She also deals bonus magic damage to the target with her basic attacks.","tooltip":"Camille briefly becomes untargetable and leaps onto an enemy champion, interrupting channels and locking them into an area they cannot escape by any means for {{ e3 }} seconds. Other nearby enemies are knocked away. Her basic attacks against the trapped enemy deal bonus magic damage equal to {{ e2 }} plus {{ e1 }}% of their current health.

16","leveltip":{"label":["Duration","Cooldown","Bonus Damage","Current Health Damage"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%>"]},"maxrank":3,"cooldown":[140,115,90],"cooldownBurn":"140/115/90","cost":[100,100,100],"costBurn":"100","effect":[null,[4,6,8],[5,10,15],[2.5,3.25,4],[-0.9,-0.9,-0.9],[425,425,425],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"4/6/8","5/10/15","2.5/3.25/4","-0.9","425","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[475,475,475],"rangeBurn":"475","image":{"full":"CamilleR.png","sprite":"spell2.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Cassiopeia":{"id":69,"key":"Cassiopeia","name":"Cassiopeia","title":"the Serpent's Embrace","spells":[{"id":"CassiopeiaQ","name":"Noxious Blast","description":"Cassiopeia blasts an area with Poison after a brief delay, granting her increased Movement Speed if she hits an enemy champion.","tooltip":"Blasts enemies in an area with Noxious Poison. If a champion is hit, Cassiopeia gains {{ e3 }}% Movement Speed decaying over {{ e4 }} seconds.

Noxious Poison deals {{ e1 }} (+{{ a1 }}) magic damage over {{ e2 }} seconds.","leveltip":{"label":["Base Damage","Movement Speed","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[3.5,3.5,3.5,3.5,3.5],"cooldownBurn":"3.5","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[75,120,165,210,255],[3,3,3,3,3],[30,35,40,45,50],[3,3,3,3,3],[7,7,7,7,7],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/120/165/210/255","3","30/35/40/45/50","3","7","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"CassiopeiaQ.png","sprite":"spell2.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CassiopeiaW","name":"Miasma","description":"Cassiopeia releases several clouds of poison, slowing, grounding, and lightly damaging enemies that pass through them. Grounded enemies cannot use Movement abilities.","tooltip":"Cassiopeia spews venom in an arc, leaving toxic clouds for {{ e4 }} seconds.

Enemies in the clouds are continually afflicted with Debilitating Poison, inflicting a decaying {{ e2 }}% slow and grounding them, prohibiting the use of Movement abilities. They also take {{ e1 }} (+{{ a1 }}) magic damage per second.

Miasma has a minimum cast range.","leveltip":{"label":["Base Damage","Slow Amount","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[20,35,50,65,80],[40,50,60,70,80],[1,1,1,1,1],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/35/50/65/80","40/50/60/70/80","1","5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.15,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"CassiopeiaW.png","sprite":"spell2.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CassiopeiaE","name":"Twin Fang","description":"Cassiopeia lets loose an attack that deals increased damage to Poisoned targets and heals her for a percentage of the damage dealt. If the target dies from this attack, Cassiopeia regains Mana.","tooltip":"Deal {{ f1 }} (+{{ a1 }}) magic damage to a target. If the target is killed by Twin Fang, or is killed during its flight, Cassiopeia gains {{ cost }} Mana.

If the victim is Poisoned when Twin Fang hits, it takes {{ e1 }} (+{{ a2 }}) additional magic damage and Cassiopeia heals for {{ f4 }} (+{{ f2 }}).","leveltip":{"label":["Bonus Poison Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[0.75,0.75,0.75,0.75,0.75],"cooldownBurn":"0.75","cost":[40,50,60,70,80],"costBurn":"40/50/60/70/80","effect":[null,[10,40,70,100,130],[0,0,0,0,0],[0.1,0.1,0.1,0.1,0.1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/40/70/100/130","0","0.1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.55,"key":"f1"},{"link":"spelldamage","coeff":0.1,"key":"a1"},{"link":"spelldamage","coeff":0.35,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"CassiopeiaE.png","sprite":"spell2.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CassiopeiaR","name":"Petrifying Gaze","description":"Cassiopeia releases a swirl of magical energy from her eyes, stunning any enemies in front of her that are facing her and slowing any others with their back turned.","tooltip":"Cassiopeia deals {{ e1 }} (+{{ a1 }}) magic damage to all enemies in front of her. Enemies facing her turn to stone and are stunned for {{ e3 }} seconds while enemies facing away are slowed by {{ e2 }}%.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[40,40,40],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","40","2","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[825,825,825],"rangeBurn":"825","image":{"full":"CassiopeiaR.png","sprite":"spell2.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Chogath":{"id":31,"key":"Chogath","name":"Cho'Gath","title":"the Terror of the Void","spells":[{"id":"Rupture","name":"Rupture","description":"Ruptures the ground at target location, popping enemy units into the air, dealing damage and slowing them.","tooltip":"Ruptures the ground at target location. Enemies caught in the rupture are launched into the air for {{ e5 }} second, take {{ e1 }} (+{{ a1 }}) magic damage, and are slowed by {{ e2 }}% for {{ e3 }} seconds.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[90,90,90,90,90],"costBurn":"90","effect":[null,[80,135,190,245,305],[60,60,60,60,60],[1.5,1.5,1.5,1.5,1.5],[0.625,0.625,0.625,0.625,0.625],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/135/190/245/305","60","1.5","0.63","1","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"Rupture.png","sprite":"spell2.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FeralScream","name":"Feral Scream","description":"Cho'Gath unleashes a terrible scream at enemies in a cone, dealing magic damage and Silencing enemies for a few seconds.","tooltip":"Silences enemies in a cone for {{ e2 }} seconds and deals {{ e1 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Cooldown","Silence Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[75,125,175,225,275],[1.5,1.625,1.75,1.875,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/125/175/225/275","1.5/1.625/1.75/1.875/2","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"FeralScream.png","sprite":"spell2.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VorpalSpikes","name":"Vorpal Spikes","description":"Cho'Gath's attacks passively release deadly spikes, dealing damage to all enemy units in front of him.","tooltip":"Toggle: Basic attacks launch spikes that deal {{ e1 }} (+{{ a1 }}) magic damage and apply spell effects. Spikes grow wider as Cho'Gath gains Feast stacks.","leveltip":{"label":["Damage"],"effect":[" {{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,35,50,65,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/35/50/65/80","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[40,40,40,40,40],"rangeBurn":"40","image":{"full":"VorpalSpikes.png","sprite":"spell2.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"Feast","name":"Feast","description":"Devours an enemy unit, dealing a high amount of true damage. If the target is killed, Cho'Gath grows, gaining maximum Health.","tooltip":"Ravenously feed on an enemy, dealing {{ e2 }} (+{{ f2 }}) (+{{ a2 }}) true damage to champions or {{ e1 }} (+{{ f2 }}) (+{{ a2 }}) to minions and monsters. If the target is killed, Cho'Gath gains a stack of Feast, which causes him to grow in size and gain {{ e3 }} maximum health.

  • Only {{ e4 }} total stacks can be gained from minions and non-epic monsters. (Current: {{ f3 }}/{{ e4 }})","leveltip":{"label":["Champion Damage","Health per Stack"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}"]},"maxrank":3,"cooldown":[80,80,80],"cooldownBurn":"80","cost":[100,100,100],"costBurn":"100","effect":[null,[1000,1000,1000],[300,475,650],[80,120,160],[6,6,6],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1000","300/475/650","80/120/160","6","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[175,175,175],"rangeBurn":"175","image":{"full":"Feast.png","sprite":"spell2.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Corki":{"id":42,"key":"Corki","name":"Corki","title":"the Daring Bombardier","spells":[{"id":"PhosphorusBomb","name":"Phosphorus Bomb","description":"Corki fires a flash bomb at a target location, dealing magic damage to enemies in the area. This attack additionally reveals units in the area for a duration.","tooltip":"Corki lobs a bomb, dealing {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) magic damage to enemies in the target area. In addition, the blast reveals the area and champions hit by the blast for {{ e2 }} seconds (does not reveal stealth).","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[70,115,160,205,250],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","6","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[825,825,825,825,825],"rangeBurn":"825","image":{"full":"PhosphorusBomb.png","sprite":"spell2.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CarpetBomb","name":"Valkyrie","description":"Corki flies a short distance, dropping bombs that create a trail of fire that damages opponents who remain in it.","tooltip":"Active: Corki flies a short distance, dealing {{ e1 }} (+{{ a1 }}) magic damage per second to enemies in the fire left along his path.

    Special Delivery: Corki flies a great distance, dropping bombs that knock aside enemies and leave a burning trail for 5 seconds. The trail slows enemies by {{ e2 }}% and burns them for {{ f1 }} (+{{ f2 }}) (+{{ a2 }}) magic damage per second.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,19,18,17,16],"cooldownBurn":"20/19/18/17/16","cost":[100,100,100,100,100],"costBurn":"100","effect":[null,[60,90,120,150,180],[90,90,90,90,90],[0,0,0,0,0],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/90/120/150/180","90","0","4","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.2,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"CarpetBomb.png","sprite":"spell2.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GGun","name":"Gatling Gun","description":"Corki's gatling gun rapidly fires in a cone in front of him, dealing damage and reducing enemy Armor and Magic Resist.","tooltip":"Corki's gatling gun fires continuously at targets in front of him for {{ e2 }} seconds, dealing up to {{ e1 }} (+{{ f1 }}) damage and reducing up to {{ e4 }} armor and magic resist.

    Gatling Gun's damage is {{ e7 }}% physical, {{ e8 }}% magic.
    Defense reductions last for {{ e6 }} seconds after last being damaged by Gatling Gun.
    ","leveltip":{"label":["Damage","Defense Reduction"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[80,140,200,260,320],[4,4,4,4,4],[4,4,4,4,4],[4,8,12,16,20],[8,8,8,8,8],[2,2,2,2,2],[50,50,50,50,50],[50,50,50,50,50],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/140/200/260/320","4","4","4/8/12/16/20","8","2","50","50","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"GGun.png","sprite":"spell2.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MissileBarrage","name":"Missile Barrage","description":"Corki fires a missile toward his target location that explodes on impact, dealing damage to enemies in an area. Corki stores missiles over time, up to a maximum. Every 3rd missile fired will be a Big One, dealing extra damage.","tooltip":"Active: Corki fires a missile that explodes at the first enemy it hits, dealing {{ e1 }} (+{{ f3 }}) (+{{ a1 }}) magic damage to all nearby enemies.

    Corki can store up to 7 missiles, and every 3rd missile will be a Big One, dealing {{ e8 }}% increased damage.","leveltip":{"label":["Base Damage","Attack Damage Scaling","Missile Reload Time"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ f1 }} -> {{ f2 }}"]},"maxrank":3,"cooldown":[2,2,2],"cooldownBurn":"2","cost":[20,20,20],"costBurn":"20","effect":[null,[75,100,125],[15,45,75],[300,300,300],[2,2,2],[0.15,0.45,0.75],[0,0,0],[50,50,50],[100,100,100],[200,200,200],[0,0,0]],"effectBurn":[null,"75/100/125","15/45/75","300","2","0.15/0.45/0.75","0","50","100","200","0"],"vars":[{"link":"@dynamic.attackdamage","coeff":0,"key":"f3"},{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"@cooldownchampion","coeff":10,"key":"f1"}],"costType":" Mana","maxammo":"7","range":[1225,1225,1225],"rangeBurn":"1225","image":{"full":"MissileBarrage.png","sprite":"spell2.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Darius":{"id":122,"key":"Darius","name":"Darius","title":"the Hand of Noxus","spells":[{"id":"DariusCleave","name":"Decimate","description":"Darius winds up and swings his axe in a wide circle. Enemies struck by the blade take more damage than those struck by the handle. Darius heals based on enemy champions hit by the blade.","tooltip":"After a short delay, Darius swings his axe around himself, striking enemies in its path. Enemies hit by the axe's blade take {{ e2 }} (+{{ f1 }}) physical damage. Enemies hit by the handle take {{ e6 }}% damage (does not apply Hemorrhage).

    Darius heals for {{ e5 }}% of his missing Health per enemy champion hit by the blade (max: {{ e7 }}%).","leveltip":{"label":["Damage","Attack Damage Scaling","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[30,35,40,45,50],"costBurn":"30/35/40/45/50","effect":[null,[100,110,120,130,140],[40,70,100,130,160],[99,99,99,99,99],[0.1,0.1,0.1,0.1,0.1],[12,12,12,12,12],[35,35,35,35,35],[36,36,36,36,36],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100/110/120/130/140","40/70/100/130/160","99","0.1","12","35","36","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.05,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"DariusCleave.png","sprite":"spell2.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DariusNoxianTacticsONH","name":"Crippling Strike","description":"Darius's next attack strikes an enemy's crucial artery. As they bleed out, their Movement Speed is slowed.","tooltip":" Darius's next basic attack deals {{ f1 }} physical damage and slows the target by {{ e2 }}% for {{ e5 }} second.

    Crippling Strike refunds its Mana cost and {{ e3 }}% of its cooldown if it kills the target.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[0,0,0,0,0],[90,90,90,90,90],[50,50,50,50,50],[1.4,1.4,1.4,1.4,1.4],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","90","50","1.4","1","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"DariusNoxianTacticsONH.png","sprite":"spell2.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DariusAxeGrabCone","name":"Apprehend","description":"Darius hones his axe, passively causing his physical damage to ignore a percentage of his target's Armor. When activated, Darius sweeps up his enemies with his axe's hook and pulls them to him.","tooltip":"Passive: Darius gains {{ e1 }}% Armor Penetration.

    Active: Pulls in all enemies in front of Darius and slows them by {{ e2 }}% for {{ e3 }} second.","leveltip":{"label":["Armor Penetration","Cooldown "],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[24,21,18,15,12],"cooldownBurn":"24/21/18/15/12","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[5,10,15,20,25],[40,40,40,40,40],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/10/15/20/25","40","1","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[535,535,535,535,535],"rangeBurn":"535","image":{"full":"DariusAxeGrabCone.png","sprite":"spell2.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DariusExecute","name":"Noxian Guillotine","description":"Darius leaps to an enemy champion and strikes a lethal blow, dealing true damage. This damage is increased for each stack of Hemorrhage on the target. If Noxian Guillotine is a killing blow, its cooldown is refreshed for a brief duration.","tooltip":"Leaps to target enemy champion and strikes a lethal blow, dealing {{ e1 }} (+{{ f1 }}) true damage. For each stack of Hemorrhage on the target, Noxian Guillotine deals an additional {{ e3 }}% damage.8","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,0],"costBurn":"100/100/0","effect":[null,[100,200,300],[0.75,0.75,0.75],[20,20,20],[25,50,100],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/200/300","0.75","20","25/50/100","2","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.75,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[460,460,460],"rangeBurn":"460","image":{"full":"DariusExecute.png","sprite":"spell2.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Diana":{"id":131,"key":"Diana","name":"Diana","title":"Scorn of the Moon","spells":[{"id":"DianaArc","name":"Crescent Strike","description":"Diana swings her blade to unleash a bolt of lunar energy that deals damage in an arc before exploding. Afflicts enemies struck with the Moonlight debuff, revealing them if they are not stealthed. ","tooltip":"Unleashes a bolt of lunar energy in an arc dealing {{ e1 }} (+{{ a1 }}) magic damage.

    Afflicts enemies struck with Moonlight, revealing them if they are not stealthed for {{ e4 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[55,55,55,55,55],"costBurn":"55","effect":[null,[60,95,130,165,200],[15,15,15,15,15],[55,55,55,55,55],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","15","55","3","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"DianaArc.png","sprite":"spell2.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DianaOrbs","name":"Pale Cascade","description":"Diana creates three orbiting spheres that detonate on contact with enemies to deal damage in an area. She also gains a temporary shield that absorbs damage. If her third sphere detonates, the shield gains additional strength.","tooltip":"Creates three orbiting spheres that explode on contact with enemies dealing {{ e2 }} (+{{ a1 }}) magic damage. Lasts {{ e1 }} seconds.

    Grants a temporary shield that absorbs {{ e3 }} (+{{ a2 }}) damage. If the third sphere detonates, the shield increases by {{ e3 }} (+{{ a2 }}).","leveltip":{"label":["Damage","Shield","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[5,5,5,5,5],[22,34,46,58,70],[40,55,70,85,100],[1,1,1,1,1],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5","22/34/46/58/70","40/55/70/85/100","1","5","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a2"},{"link":"spelldamage","coeff":0.3,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"DianaOrbs.png","sprite":"spell2.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DianaVortex","name":"Moonfall","description":"Diana draws in and slows all nearby enemies.","tooltip":"Reveals and draws in all nearby enemies and then slows them by {{ e1 }}% for {{ e2 }} seconds.","leveltip":{"label":["Slow Amount","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[26,24,22,20,18],"cooldownBurn":"26/24/22/20/18","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[35,40,45,50,55],[2,2,2,2,2],[50,50,50,50,50],[4,4,4,4,4],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/40/45/50/55","2","50","4","6","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"DianaVortex.png","sprite":"spell2.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DianaTeleport","name":"Lunar Rush","description":"Diana dashes to an enemy and deals magic damage. Lunar Rush has no cooldown when used to teleport to a target afflicted with Moonlight.","tooltip":"Becomes the living embodiment of the vengeful moon, dashing to an enemy and dealing {{ e2 }} (+{{ a1 }}) magic damage.

    Lunar Rush has no cooldown when used to dash to an enemy afflicted with Moonlight. All other enemies will have the Moonlight debuff removed regardless of whether they were the target of Lunar Rush.","leveltip":{"label":["Damage","Mana Cost","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[25,20,15],"cooldownBurn":"25/20/15","cost":[50,65,80],"costBurn":"50/65/80","effect":[null,[4,4,4],[100,160,220],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"4","100/160/220","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[825,825,825],"rangeBurn":"825","image":{"full":"DianaTeleport.png","sprite":"spell2.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Draven":{"id":119,"key":"Draven","name":"Draven","title":"the Glorious Executioner","spells":[{"id":"DravenSpinning","name":"Spinning Axe","description":"Draven's next attack will deal bonus physical damage. This axe will ricochet off the target high up into the air. If Draven catches it, he automatically readies another Spinning Axe. Draven can have two Spinning Axes at once.","tooltip":"Draven's next attack will deal {{ f1 }} bonus physical damage. The bonus is equal to {{ e5 }} plus {{ e2 }}% of his bonus Attack Damage.

    This axe will ricochet off the target high up into the air. If Draven catches it, he automatically readies another Spinning Axe.

    Draven can have two Spinning Axes at once.","leveltip":{"label":["Base Damage","Bonus AD Percentage","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[100,100,100,100,100],[65,75,85,95,105],[30,35,40,45,50],[5.75,5.75,5.75,5.75,5.75],[30,35,40,45,50],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100","65/75/85/95/105","30/35/40/45/50","5.75","30/35/40/45/50","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.45,0.55,0.65,0.75,0.85],"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"DravenSpinning.png","sprite":"spell2.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DravenFury","name":"Blood Rush","description":"Draven gains increased Movement Speed and Attack Speed. The Movement Speed bonus decreases rapidly over its duration. Catching a Spinning Axe will refresh the cooldown of Blood Rush.","tooltip":"","leveltip":{"label":["Attack Speed","Mana Cost","Movement Speed"],"effect":["{{ e4 }}% -> {{ e4NL }}%","{{ cost }} -> {{ costNL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[40,35,30,25,20],"costBurn":"40/35/30/25/20","effect":[null,[4,5,6,7,8],[40,45,50,55,60],[1.5,1.5,1.5,1.5,1.5],[20,25,30,35,40],[3,3,3,3,3],[-0.05,-0.056,-0.062,-0.069,-0.075],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/5/6/7/8","40/45/50/55/60","1.5","20/25/30/35/40","3","-0.05/-0.056/-0.062/-0.069/-0.075","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"DravenFury.png","sprite":"spell2.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DravenDoubleShot","name":"Stand Aside","description":"Draven throws his axes, dealing physical damage to targets hit and knocking them aside. Targets hit are slowed.","tooltip":"Draven throws his axes, dealing {{ e1 }} (+{{ f1 }}) physical damage to targets hit and knocking them aside. Targets hit are slowed by {{ e2 }}% for {{ e3 }} seconds.","leveltip":{"label":["Damage","Slow Amount","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[70,105,140,175,210],[20,25,30,35,40],[2,2,2,2,2],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/105/140/175/210","20/25/30/35/40","2","0.5","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"DravenDoubleShot.png","sprite":"spell2.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DravenRCast","name":"Whirling Death","description":"Draven hurls two massive axes to deal physical damage to each unit struck. Whirling Death slowly reverses direction and returns to Draven after striking an enemy champion. Draven may also activate this ability while the axes are in flight to cause it to return early. Deals less damage for each unit hit and resets when the axes reverse direction.","tooltip":"Draven hurls two massive axes to deal {{ e1 }} (+{{ f1 }}) physical damage to each unit struck.

    Whirling Death slowly reverses direction and returns to Draven after striking an enemy champion. Draven may also activate this ability while the axes are in flight to cause it to return early. Deals {{ e4 }}% less damage for each unit hit (Minimum {{ e2 }}%) and resets when the axes reverse direction.","leveltip":{"label":["Damage","Cooldown"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[175,275,375],[40,40,40],[0,0,0],[8,8,8],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"175/275/375","40","0","8","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.1,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[20000,20000,20000],"rangeBurn":"20000","image":{"full":"DravenRCast.png","sprite":"spell2.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"DrMundo":{"id":36,"key":"DrMundo","name":"Dr. Mundo","title":"the Madman of Zaun","spells":[{"id":"InfectedCleaverMissileCast","name":"Infected Cleaver","description":"Dr. Mundo hurls his cleaver, dealing damage equal to a portion of his target's current Health and slowing them for a short time. Dr. Mundo delights in the suffering of others, so he is returned half of the Health cost when he successfully lands a cleaver (increased to the full Health cost on killing blows).","tooltip":"Dr. Mundo hurls his cleaver, dealing magic damage equal to {{ e2 }}% of the target's current Health and slowing them by {{ e4 }}% for {{ e5 }} seconds. {{ e1 }} damage minimum, max {{ e6 }} damage to Monsters.

    Half of the Health cost is refunded if the cleaver hits an enemy. The full Health cost is refunded if the cleaver kills an enemy.","leveltip":{"label":["Minimum Damage","Damage Percent","Health Cost","Max Monster Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e3 }} -> {{ e3NL }}","{{ e6 }} -> {{ e6NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[80,130,180,230,280],[15,17.5,20,22.5,25],[50,60,70,80,90],[40,40,40,40,40],[2,2,2,2,2],[300,350,400,450,500],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/130/180/230/280","15/17.5/20/22.5/25","50/60/70/80/90","40","2","300/350/400/450/500","0.5","0","0","0"],"vars":[],"costType":" Health","maxammo":"-1","range":[975,975,975,975,975],"rangeBurn":"975","image":{"full":"InfectedCleaverMissileCast.png","sprite":"spell2.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ e3 }} Health"},{"id":"BurningAgony","name":"Burning Agony","description":"Dr. Mundo drains his Health to reduce the duration of disables and deal continual damage to nearby enemies.","tooltip":"Toggle: Dr. Mundo deals {{ e3 }} (+{{ charabilitypower*2 }}) magic damage to nearby enemies per second, and reduces the duration of disables on Dr. Mundo by {{ e2 }}%.","leveltip":{"label":["Damage Per Second","Disable Reduction","Health Cost"],"effect":[" {{ e3 }} -> {{ e3NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,15,20,25,30],[10,15,20,25,30],[40,55,70,85,100],[325,325,325,325,325],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/15/20/25/30","10/15/20/25/30","40/55/70/85/100","325","0","0","0","0","0","0"],"vars":[],"costType":" Health Per Sec","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"BurningAgony.png","sprite":"spell2.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ e1 }} Health Per Sec"},{"id":"Masochism","name":"Masochism","description":"Masochism deals bonus damage on Dr. Mundo's next basic attack and increases Attack Damage by a flat amount for 5 seconds. In addition, Dr. Mundo also gains an additional amount of Attack Damage for each percentage of Health he is missing.","tooltip":"Dr. Mundo deals an additional {{ f1 }} physical damage ({{ e6 }}% of max health) on his next basic attack and gains {{ e1 }} Attack Damage for {{ e4 }} seconds. Dr. Mundo gains an additional +{{ e3 }} Attack Damage ({{ f2 }}) for each percent of his Health he is missing.","leveltip":{"label":["Attack Damage Bonus","Bonus Factor","Max Health %","Health Cost"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e6 }}% -> {{ e6NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,50,70,90,110],[25,35,45,55,65],[0.3,0.5,0.7,0.9,1.1],[5,5,5,5,5],[5,20,35,50,65],[3,3.5,4,4.5,5],[25,25,25,25,25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","25/35/45/55/65","0.3/0.5/0.7/0.9/1.1","5","5/20/35/50/65","3/3.5/4/4.5/5","25","0","0","0"],"vars":[],"costType":" Health","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"Masochism.png","sprite":"spell2.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ e2 }} Health"},{"id":"Sadism","name":"Sadism","description":"Dr. Mundo sacrifices a portion of his Health for increased Movement Speed and drastically increased Health Regeneration.","tooltip":"Dr. Mundo regenerates {{ f1 }} Health ({{ e1 }}% of max health) over {{ e2 }} seconds. Additionally, he gains {{ e3 }}% movement speed during this time.","leveltip":{"label":["Regeneration","Movement Speed","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[0,0,0],"costBurn":"0","effect":[null,[40,50,60],[12,12,12],[15,25,35],[20,20,20],[0.015,0.015,0.015],[5,5,5],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"40/50/60","12","15/25/35","20","0.01","5","0","0","0","0"],"vars":[],"costType":"% of Current Health","maxammo":"-1","range":[20,20,20],"rangeBurn":"20","image":{"full":"Sadism.png","sprite":"spell2.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ e4 }}% of Current Health"}]},"Ekko":{"id":245,"key":"Ekko","name":"Ekko","title":"the Boy Who Shattered Time","spells":[{"id":"EkkoQ","name":"Timewinder","description":"Ekko throws a temporal grenade that expands into a time-distortion field upon hitting an enemy champion, slowing and damaging anyone caught inside. After a delay, the grenade rewinds back to Ekko, dealing damage on its return.","tooltip":"Ekko throws a device that deals {{ e1 }} (+{{ a1 }}) magic damage to enemies it passes through. It expands into a slowing field on the first champion hit, slowing everything inside by {{ e2 }}%. It then returns to him after a delay, dealing {{ e3 }} (+{{ a2 }}) magic damage to all targets hit upon return.","leveltip":{"label":["Outgoing Damage","Return Damage","Slow","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,8.5,8,7.5,7],"cooldownBurn":"9/8.5/8/7.5/7","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[60,75,90,105,120],[32,39,46,53,60],[40,65,90,115,140],[100,100,100,100,100],[0,0,0,0,0],[165,165,165,165,165],[1.75,1.75,1.75,1.75,1.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/75/90/105/120","32/39/46/53/60","40/65/90/115/140","100","0","165","1.75","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"EkkoQ.png","sprite":"spell2.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EkkoW","name":"Parallel Convergence","description":"Ekko splits the timeline, creating an anomaly after a few seconds that slows enemies caught inside. If Ekko enters the anomaly, he gains shielding and triggers a detonation, stunning enemies by suspending them in time.","tooltip":"Passive: Ekko's basic attacks deal bonus magic damage to enemies under 30% health equal to {{ e3 }}% (+{{ a2 }}%) of their missing health. Deals a minimum of {{ e6 }} damage, and a maximum of {{ e5 }} damage vs. minions and monsters.

    Active: After a 3 second delay, Ekko creates a short-lived chronosphere at the target location that slows enemies who enter by {{ e0 }}%. If Ekko enters the sphere, he will detonate it, gaining a shield that absorbs up to {{ e4 }} (+{{ a1 }}) damage for 2 seconds. Enemies caught inside are stunned for {{ e2 }} seconds.","leveltip":{"label":["Shield Amount","Cooldown","Mana Cost"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}<"]},"maxrank":5,"cooldown":[22,20,18,16,14],"cooldownBurn":"22/20/18/16/14","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[375,375,375,375,375],[1.75,1.75,1.75,1.75,1.75],[3,3,3,3,3],[80,100,120,140,160],[150,150,150,150,150],[15,15,15,15,15],[1.5,1.5,1.5,1.5,1.5],[3,3,3,3,3],[2,2,2,2,2],[40,40,40,40,40]],"effectBurn":[null,"375","1.75","3","80/100/120/140/160","150","15","1.5","3","2","40"],"vars":[{"link":"spelldamage","coeff":0.03,"key":"a2"},{"link":"spelldamage","coeff":1.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1600,1600,1600,1600,1600],"rangeBurn":"1600","image":{"full":"EkkoW.png","sprite":"spell2.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EkkoE","name":"Phase Dive","description":"Ekko rolls evasively while charging up his Z-Drive. His next attack deals bonus damage and warps reality, teleporting him to his target.","tooltip":" Ekko dashes a short distance in the targeted direction. His next attack will deal {{ e1 }} (+{{ a1 }}) bonus magic damage and teleport him to his target.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[40,50,60,70,80],"costBurn":"40/50/60/70/80","effect":[null,[40,65,90,115,140],[350,350,350,350,350],[3,3,3,3,3],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/65/90/115/140","350","3","300","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"EkkoE.png","sprite":"spell2.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EkkoR","name":"Chronobreak","description":"Ekko shatters his timeline, becoming untargetable and rewinding to a more favorable point in time. He returns to whenever he was a few seconds ago, and heals for a percentage of the damage received in that duration. Enemies near his arrival zone take massive damage.","tooltip":"Ekko turns back time, going briefly untargetable and invulnerable. He teleports to where he was 4 seconds ago and deals {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies on arrival. Additionally, Ekko heals for {{ e3 }} (+{{ a2 }}), increased by {{ e5 }}% for each 1% of his health lost over the last 4 seconds.","leveltip":{"label":["Damage","Flat Heal","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,90,70],"cooldownBurn":"110/90/70","cost":[100,100,100],"costBurn":"100","effect":[null,[150,300,450],[20,20,20],[100,150,200],[375,375,375],[3,3,3],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/300/450","20","100/150/200","375","3","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1.5,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[850,850,850],"rangeBurn":"850","image":{"full":"EkkoR.png","sprite":"spell2.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Elise":{"id":60,"key":"Elise","name":"Elise","title":"the Spider Queen","spells":[{"id":"EliseHumanQ","name":"Neurotoxin / Venomous Bite","description":"Human Form: Deals damage based upon how high the target's Health is.

    Spider Form: Lunges at an enemy and deals damage based upon how low their Health is.","tooltip":"Deals magic damage equal to {{ e1 }} plus {{ e6 }}% (+{{ a1 }}%) of the target's current health. Maximum bonus against monsters: {{ e2 }}.","leveltip":{"label":["Neurotoxin Damage","Venomous Bite Damage","Max Damage to Monsters","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[80,85,90,95,100],"costBurn":"80/85/90/95/100","effect":[null,[40,75,110,145,180],[75,100,125,150,175],[15,20,25,30,35],[36,42,48,54,60],[60,100,140,180,220],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/75/110/145/180","75/100/125/150/175","15/20/25/30/35","36/42/48/54/60","60/100/140/180/220","4","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.03,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"EliseHumanQ.png","sprite":"spell2.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EliseHumanW","name":"Volatile Spiderling / Skittering Frenzy","description":"Human Form: Releases a venom-gorged Spiderling that explodes when it nears a target.

    Spider Form: Elise and her Spiderlings gain Attack Speed.","tooltip":"Summons a venom-gorged Spiderling that moves to target location and explodes, dealing {{ e3 }} (+{{ a1 }}) Magic Damage when it nears an enemy or after 3 seconds.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[4,4,4,4,4],[60,80,100,120,140],[60,110,160,210,260],[3,3,3,3,3],[65,75,85,95,105],[275,275,275,275,275],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4","60/80/100/120/140","60/110/160/210/260","3","65/75/85/95/105","275","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"EliseHumanW.png","sprite":"spell2.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EliseHumanE","name":"Cocoon / Rappel","description":"Human Form: Stuns the first enemy unit hit and reveals them if they are not stealthed.

    Spider Form: Elise and her Spiderlings ascend into the air and then descend upon target enemy.","tooltip":"Stuns the first enemy hit for {{ e5 }} seconds and reveals them if they are not stealthed.","leveltip":{"label":["Stun Duration","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[14,13,12,11,10],[15,20,25,30,35],[26,23,20,17,14],[2,2,2,2,2],[1.6,1.7,1.8,1.9,2],[40,55,70,85,100],[250,250,250,250,250],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"14/13/12/11/10","15/20/25/30/35","26/23/20/17/14","2","1.6/1.7/1.8/1.9/2","40/55/70/85/100","250","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"EliseHumanE.png","sprite":"spell2.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EliseR","name":"Spider Form","description":"Transforms into a menacing spider, reducing her attack range in exchange for movement speed, new abilities, and a Spiderling swarm that will attack her foes.","tooltip":"Elise transforms into a menacing spider, sacrificing 425 attack range in exchange for {{ e3 }} movement speed and access to arachnid abilities. All dormant Spiderlings are awakened and will attack nearby foes.

    Spiderlings: Spiderlings deal {{ e1 }} (+{{ a2 }}) damage and take {{ e4 }}% reduced damage from multi-target abilities.","leveltip":{"label":["Spider Form Bite Damage","Spiderling Bonus Damage","Maximum Number of Spiderlings","Spiderling Armor","Spiderling Magic Resist"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ e6 }} -> {{ e6NL }}","{{ e7 }} -> {{ e7NL }}"]},"maxrank":4,"cooldown":[4,4,4,4],"cooldownBurn":"4","cost":[0,0,0,0],"costBurn":"0","effect":[null,[10,15,20,25],[10,20,30,40],[25,25,25,25],[25,25,25,25],[2,3,4,5],[30,50,70,90],[50,70,90,110],[4,6,8,10],[0.1,0.1,0.1,0.1],[0,0,0,0]],"effectBurn":[null,"10/15/20/25","10/20/30/40","25","25","2/3/4/5","30/50/70/90","50/70/90/110","4/6/8/10","0.1","0"],"vars":[{"link":"spelldamage","coeff":0.15,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20],"rangeBurn":"20","image":{"full":"EliseR.png","sprite":"spell2.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Evelynn":{"id":28,"key":"Evelynn","name":"Evelynn","title":"the Widowmaker","spells":[{"id":"EvelynnQ","name":"Hate Spike","description":"Evelynn fires a line of spikes through an enemy, dealing damage to all enemies in its path.","tooltip":"Evelynn fires a line of spikes through a nearby enemy dealing {{ e1 }} (+{{ f2 }}) (+{{ f1 }}) magic damage to all enemies in its path.

    Hate Spike prioritizes the target Evelynn is attacking.","leveltip":{"label":["Damage","Ability Power Scaling","Attack Damage Scaling","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[1.5,1.5,1.5,1.5,1.5],"cooldownBurn":"1.5","cost":[12,18,24,30,36],"costBurn":"12/18/24/30/36","effect":[null,[40,50,60,70,80],[35,40,45,50,55],[50,55,60,65,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/50/60/70/80","35/40/45/50/55","50/55/60/65/70","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"EvelynnQ.png","sprite":"spell2.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EvelynnW","name":"Dark Frenzy","description":"Evelynn passively increases her Movement Speed when hitting enemy champions with her spells. Upon activation, Evelynn breaks free from slows affecting her and gains a massive Movement Speed boost for a short duration.","tooltip":"Active: Evelynn removes all slows affecting her and gains {{ e3 }}% Movement Speed for {{ e4 }} seconds.

    Passive: Evelynn's spell hits on enemy champions reduce Dark Frenzy's cooldown by 1 second.

    Champion kills and assists refresh Dark Frenzy's cooldown.","leveltip":{"label":["Active Movement Speed"],"effect":["{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[1,1,1,1,1],[3,3,3,3,3],[30,40,50,60,70],[3,3,3,3,3],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1","3","30/40/50/60/70","3","4","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[10000,10000,10000,10000,10000],"rangeBurn":"10000","image":{"full":"EvelynnW.png","sprite":"spell2.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EvelynnE","name":"Ravage","description":"Evelynn slashes her target twice, dealing damage with each hit. She then gains increased Attack Speed for a short duration.","tooltip":"Evelynn swiftly attacks a target {{ e3 }} times (applies on-hit effects), dealing a total of {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) physical damage.

    Evelynn then gains {{ e4 }}% Attack Speed for {{ e2 }} seconds.","leveltip":{"label":["Damage","Attack Speed","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[70,110,150,190,230],[3,3,3,3,3],[2,2,2,2,2],[60,75,90,105,120],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","3","2","60/75/90/105/120","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"},{"link":"bonusattackdamage","coeff":0.4,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[225,225,225,225,225],"rangeBurn":"225","image":{"full":"EvelynnE.png","sprite":"spell2.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EvelynnR","name":"Agony's Embrace","description":"Evelynn summons spikes from the ground to deal damage and slow enemies in the area. She then gains a shield based on how many enemy champions were hit.","tooltip":"Evelynn impales all enemies in the targeted area, dealing {{ f1 }}% [{{ e1 }}% + {{ f2 }}% per 100 ability power] of each target's current health as magic damage and slowing their movement speed by {{ e2 }}% for {{ e3 }} seconds.

    Evelynn siphons their pain, gaining a {{ e4 }} health shield for each enemy champion hit which lasts up to {{ e6 }} seconds.","leveltip":{"label":["Current Health %","Slow","Shield Amount","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[15,20,25],[40,60,80],[2,2,2],[150,225,300],[50,100,150],[6,6,6],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"15/20/25","40/60/80","2","150/225/300","50/100/150","6","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"EvelynnR.png","sprite":"spell3.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Ezreal":{"id":81,"key":"Ezreal","name":"Ezreal","title":"the Prodigal Explorer","spells":[{"id":"EzrealMysticShot","name":"Mystic Shot","description":"Ezreal fires a damaging bolt of energy which reduces all of his cooldowns by 1.5 seconds if it strikes an enemy unit.","tooltip":"Ezreal fires a bolt of energy, dealing {{ e1 }} (+{{ a1 }}) (+{{ a2 }}) physical damage (applies on-hit effects).

    Ezreal's cooldowns are reduced by {{ e2 }} seconds if Mystic Shot hits a target.","leveltip":{"label":["Bonus Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6.5,6,5.5,5,4.5],"cooldownBurn":"6.5/6/5.5/5/4.5","cost":[28,31,34,37,40],"costBurn":"28/31/34/37/40","effect":[null,[35,55,75,95,115],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/55/75/95/115","1.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.1,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1150,1150,1150,1150,1150],"rangeBurn":"1150","image":{"full":"EzrealMysticShot.png","sprite":"spell3.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EzrealEssenceFlux","name":"Essence Flux","description":"Ezreal fires a fluctuating wave of energy, dealing magic damage to enemy champions, while increasing the Attack Speed of allied champions.","tooltip":"Ezreal fires a wave of energy that damages all enemy champions it passes through for {{ e1 }} (+{{ a1 }}) magic damage. If Ezreal or his Allied champions are hit by the wave, their Attack Speed is increased by {{ e2 }}% for {{ e3 }} seconds.","leveltip":{"label":["Damage","Attack Speed","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }}% -> {{ e2NL }}%"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[70,115,160,205,250],[20,25,30,35,40],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","20/25/30/35/40","5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"EzrealEssenceFlux.png","sprite":"spell3.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EzrealArcaneShift","name":"Arcane Shift","description":"Ezreal teleports to a target nearby location and fires a homing bolt which strikes the nearest enemy unit.","tooltip":"Ezreal teleports to a nearby location and fires a bolt at the nearest enemy, dealing {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[19,17.5,16,14.5,13],"cooldownBurn":"19/17.5/16/14.5/13","cost":[90,90,90,90,90],"costBurn":"90","effect":[null,[75,125,175,225,275],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/125/175/225/275","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"a2"},{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[475,475,475,475,475],"rangeBurn":"475","image":{"full":"EzrealArcaneShift.png","sprite":"spell3.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EzrealTrueshotBarrage","name":"Trueshot Barrage","description":"Ezreal winds up for 1 second to fire a powerful barrage of energy missiles which do massive damage to each unit they pass through (deals 10% less damage to each unit it passes through).","tooltip":"Ezreal winds up for 1 second to fire a long range missile that deals {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) magic damage to each enemy it passes through. Deals {{ e2 }}% less damage for each enemy hit (minimum {{ e3 }}%).","leveltip":{"label":["Damage"],"effect":[" {{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[120,120,120],"cooldownBurn":"120","cost":[100,100,100],"costBurn":"100","effect":[null,[350,500,650],[10,10,10],[30,30,30],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"350/500/650","10","30","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20000,20000,20000],"rangeBurn":"20000","image":{"full":"EzrealTrueshotBarrage.png","sprite":"spell3.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Fiddlesticks":{"id":9,"key":"Fiddlesticks","name":"Fiddlesticks","title":"the Harbinger of Doom","spells":[{"id":"Terrify","name":"Terrify","description":"Strikes a target unit with fear, causing it to flee in terror for a duration.","tooltip":"Terrifies a target, causing it to flee from Fiddlesticks for {{ e1 }} seconds.","leveltip":{"label":["Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[1.25,1.5,1.75,2,2.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.25/1.5/1.75/2/2.25","0","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[575,575,575,575,575],"rangeBurn":"575","image":{"full":"Terrify.png","sprite":"spell3.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Drain","name":"Drain","description":"Fiddlesticks saps the life force of an enemy, dealing damage to a target over time and healing himself.","tooltip":"Fiddlesticks creates a tether to the target unit and channels Drain on it, granting True Sight and dealing {{ e1 }} (+{{ a1 }}) magic damage per second to the target. Fiddlesticks heals for {{ e3 }}% of the damage dealt.

    Drain lasts up to {{ e4 }} seconds, dealing a total of {{ f1 }} (+{{ f2 }}) damage.

    16","leveltip":{"label":["Damage Per Second","Cooldown","Mana Cost","Drain Ratio"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[4,3.75,3.5,3.25,3],"cooldownBurn":"4/3.75/3.5/3.25/3","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[80,105,130,155,180],[4,4,4,4,4],[60,65,70,75,80],[5,5,5,5,5],[650,650,650,650,650],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/105/130/155/180","4","60/65/70/75/80","5","650","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[575,575,575,575,575],"rangeBurn":"575","image":{"full":"Drain.png","sprite":"spell3.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FiddlesticksDarkWind","name":"Dark Wind","description":"A wisp of wind strikes an enemy unit and then bounces to nearby enemy units, dealing damage and silencing the victims.","tooltip":"Unleashes a Crow at the targeted enemy, dealing {{ e1 }} (+{{ a1 }}) magic damage before bouncing to another target. Units struck are also silenced for {{ e2 }} seconds the first time per cast.

    Dark Wind can strike up to {{ e3 }} times and will prioritize enemies being Drained or ones it has not yet hit. Deals {{ e4 }}% damage to minions and monsters.","leveltip":{"label":["Damage","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11.5,11,10.5,10],"cooldownBurn":"12/11.5/11/10.5/10","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[65,85,105,125,145],[1.25,1.25,1.25,1.25,1.25],[6,6,6,6,6],[150,150,150,150,150],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/85/105/125/145","1.25","6","150","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"FiddlesticksDarkWind.png","sprite":"spell3.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Crowstorm","name":"Crowstorm","description":"A murder of crows flock wildly around Fiddlesticks, dealing damage per second to all enemy units in the area.","tooltip":"Fiddlesticks channels for {{ e3 }} seconds, then blinks to the target point unleashing a murder of crows that flock wildly around him, dealing {{ e1 }} (+{{ a1 }}) magic damage per second to all enemy units in the area.

    The effect lasts for {{ e2 }} seconds, dealing up to {{ f1 }} (+{{ f2 }}) total magic damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,140,130],"cooldownBurn":"150/140/130","cost":[100,100,100],"costBurn":"100","effect":[null,[125,225,325],[5,5,5],[1.5,1.5,1.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"125/225/325","5","1.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800],"rangeBurn":"800","image":{"full":"Crowstorm.png","sprite":"spell3.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Fiora":{"id":114,"key":"Fiora","name":"Fiora","title":"the Grand Duelist","spells":[{"id":"FioraQ","name":"Lunge","description":"Fiora lunges in a direction and stabs a nearby enemy, dealing physical damage and applying on-hit effects.","tooltip":"Fiora lunges in a direction and stabs a nearby enemy, dealing {{ e1 }} (+{{ f1 }}) physical damage and applying on-hit effects. This attack prioritizes Vitals and enemies it will kill.

    If this ability hits an enemy, {{ e4 }}% of its cooldown is refunded. ","leveltip":{"label":["Damage","Attack Damage Ratio","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,14,12,10,8],"cooldownBurn":"16/14/12/10/8","cost":[20,25,30,35,40],"costBurn":"20/25/30/35/40","effect":[null,[65,75,85,95,105],[0.95,1,1.05,1.1,1.15],[50,50,50,50,50],[60,60,60,60,60],[2.5,2.5,2.5,2.5,2.5],[2,2,2,2,2],[90,120,150,180,210],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/75/85/95/105","0.95/1/1.05/1.1/1.15","50","60","2.5","2","90/120/150/180/210","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"FioraQ.png","sprite":"spell3.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FioraW","name":"Riposte","description":"Fiora parries all incoming damage and disables for a short time, then stabs in a direction. This stab slows the first enemy champion hit, or stuns them if Fiora blocked an immobilizing effect with this ability.","tooltip":"Fiora parries all incoming damage, debuffs, and disables for the next {{ e2 }} seconds and then stabs in the target direction.

    The stab deals {{ e1 }} (+{{ f1 }}) magic damage to the first enemy champion hit and slows their movement and attack speed by {{ e4 }}% for {{ e3 }} seconds.

    If Fiora parries an immobilizing effect, Riposte stuns instead of slowing.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f2 }} -> {{ f3 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[90,130,170,210,250],[0.75,0.75,0.75,0.75,0.75],[1.5,1.5,1.5,1.5,1.5],[50,50,50,50,50],[0.03,0.03,0.03,0.03,0.03],[0.15,0.15,0.15,0.15,0.25],[24,22,20,18,16],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"90/130/170/210/250","0.75","1.5","50","0.03","0.15/0.15/0.15/0.15/0.25","24/22/20/18/16","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"FioraW.png","sprite":"spell3.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FioraE","name":"Bladework","description":"Fiora has increased attack speed for the next two attacks. The first attack slows the target, and the second attack will critically strike.","tooltip":"Fiora gains {{ e4 }}% attack speed for her next two attacks. The first attack cannot critically strike, but will apply a {{ e3 }}% slow for {{ e2 }} second. The second attack is guaranteed to critically strike for {{ f4 }}% damage ({{ f3 }}).","leveltip":{"label":["Critical Strike Damage","Cooldown","Mana Cost"],"effect":["{{ f4 }}% -> {{ f5 }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,11,9,7,5],"cooldownBurn":"13/11/9/7/5","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[3,3,3,3,3],[1,1,1,1,1],[30,30,30,30,30],[50,50,50,50,50],[100,100,100,100,100],[140,155,170,185,200],[5,10,15,20,25],[40,50,60,70,80],[10,12.5,15,17.5,20],[20,20,20,20,20]],"effectBurn":[null,"3","1","30","50","100","140/155/170/185/200","5/10/15/20/25","40/50/60/70/80","10/12.5/15/17.5/20","20"],"vars":[],"costType":" Mana","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"FioraE.png","sprite":"spell3.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FioraR","name":"Grand Challenge","description":"Fiora reveals all four Vitals on an enemy champion and gains movement speed while near them. If Fiora hits all 4 Vitals or if the target dies after she has hit at least one, Fiora and her allies in the area are healed over the next few seconds.","tooltip":"Fiora reveals all four Vitals on target champion, for a potential {{ f8 }}% max health true damage. In addition, Fiora gains Duelist's Dance's movement speed bonus ({{ f6 }}%) while near the target.

    If Fiora hits all 4 Vitals in {{ e1 }} seconds or if the target dies after she has hit at least one, Fiora and her allies in the area are healed for {{ e7 }} (+{{ f9 }}) each second for between {{ e9 }} to {{ e6 }} seconds, scaling with the amount of Vitals hit.","leveltip":{"label":["Cooldown","Duelist Movement Speed","Heal Per Second"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e7 }} -> {{ e7NL }}"]},"maxrank":3,"cooldown":[110,90,70],"cooldownBurn":"110/90/70","cost":[100,100,100],"costBurn":"100","effect":[null,[8,8,8],[30,40,50],[0,0,0],[30,45,60],[500,500,500],[5,5,5],[80,110,140],[550,550,550],[2,2,2],[1,1,1]],"effectBurn":[null,"8","30/40/50","0","30/45/60","500","5","80/110/140","550","2","1"],"vars":[],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"FioraR.png","sprite":"spell3.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Fizz":{"id":105,"key":"Fizz","name":"Fizz","title":"the Tidal Trickster","spells":[{"id":"FizzQ","name":"Urchin Strike","description":"Fizz dashes through his target, dealing magic damage and applying on hit effects.","tooltip":"Fizz dashes through his target, dealing {{ a2 }} physical damage plus {{ e1 }} (+{{ a1 }}) magic damage. This spell applies on-hit effects.

    16","leveltip":{"label":["Magic Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7.5,7,6.5,6],"cooldownBurn":"8/7.5/7/6.5/6","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[10,25,40,55,70],[0,0,0,0,0],[650,750,850,950,1050],[1.5,1.5,1.5,1.5,1.5],[600,600,600,600,600],[1,1,1,1,1],[0.35,0.35,0.35,0.35,0.35],[40,40,40,40,40],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/25/40/55/70","0","650/750/850/950/1050","1.5","600","1","0.35","40","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.55,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"FizzQ.png","sprite":"spell3.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FizzW","name":"Seastone Trident","description":"Fizz's attacks bleed his enemies, dealing magic damage over several seconds. Fizz can empower his next attack to deal bonus damage, increased by how long the bleed was active on his target.","tooltip":"Passive: Fizz's basic attacks bleed his enemies, dealing {{ e1 }} (+{{ a2 }}) magic damage over {{ e8 }} seconds.

    Active: Fizz's next basic attack deals {{ e3 }} (+{{ a2 }}) bonus magic damage. If the target has been bleeding for at least {{ e7 }} seconds, this bonus increases to {{ e3 }} (+{{ a1 }}).

    16","leveltip":{"label":["Passive / Active Damage","Charged Active Damage","Cooldown","Mana Cost","Mana Refund"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[10,9.5,9,8.5,8],"cooldownBurn":"10/9.5/9/8.5/8","cost":[30,40,50,60,70],"costBurn":"30/40/50/60/70","effect":[null,[20,30,40,50,60],[20,28,36,42,50],[60,90,120,150,180],[6,6,6,6,6],[2,2,2,2,2],[4,4,4,4,4],[2,2,2,2,2],[3,3,3,3,3],[750,750,750,750,750],[1,1,1,1,1]],"effectBurn":[null,"20/30/40/50/60","20/28/36/42/50","60/90/120/150/180","6","2","4","2","3","750","1"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a2"},{"link":"spelldamage","coeff":0.4,"key":"a2"},{"link":"spelldamage","coeff":1.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"FizzW.png","sprite":"spell3.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FizzE","name":"Playful / Trickster","description":"Fizz hops into the air, landing gracefully upon his spear and becoming untargetable. From this position, Fizz can either slam the ground or choose to jump again before smashing back down.","tooltip":"Fizz hops on his trident towards the cursor, becoming briefly untargetable.

    Reactivation: Fizz cancels the spell early, jumping towards the cursor a second time and dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies.

    If Fizz does not reactivate, he deals damage in a larger area and slows all enemies hit by {{ e2 }}% for {{ e4 }} seconds.","leveltip":{"label":["Damage","Slow Percentage","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[90,95,100,105,110],"costBurn":"90/95/100/105/110","effect":[null,[70,120,170,220,270],[40,45,50,55,60],[225,225,225,225,225],[2,2,2,2,2],[375,375,375,375,375],[80,80,80,80,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/120/170/220/270","40/45/50/55/60","225","2","375","80","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"FizzE.png","sprite":"spell3.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"FizzR","name":"Chum the Waters","description":"Fizz tosses a fish in a direction that attaches to any champion that touches it, slowing the target. After a short delay, a shark erupts from the ground, knocking up the target and knocking any nearby enemies aside. All enemies hit are dealt magic damage and slowed.","tooltip":"Fizz launches a fish that attaches to the first enemy champion it hits, slowing them, granting True Sight, and attracting a shark. After {{ e8 }} seconds, the shark erupts out of the ground, knocking its target up and knocking other enemies aside.

    The farther the fish travels before attaching, the bigger the shark it attracts, dealing {{ e1 }} (+{{ a1 }}) to {{ e3 }} (+{{ a2 }}) magic damage and slowing by 40% to 80%.

    16","leveltip":{"label":["Small Shark Damage","Medium Shark Damage","Big Shark Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} ->{{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[225,325,425],[300,400,500],[200,200,200],[325,325,325],[450,450,450],[1300,1300,1300],[2,2,2],[2,2,2],[60,60,60]],"effectBurn":[null,"150/250/350","225/325/425","300/400/500","200","325","450","1300","2","2","60"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"},{"link":"spelldamage","coeff":1.2,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1300,1300,1300],"rangeBurn":"1300","image":{"full":"FizzR.png","sprite":"spell3.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Galio":{"id":3,"key":"Galio","name":"Galio","title":"the Colossus","spells":[{"id":"GalioQ","name":"Winds of War","description":"Galio fires two windblasts that converge into a large tornado.","tooltip":"Galio fires two windblasts that deal {{ e1 }} (+{{ a1 }}) magic damage.

    When the windblasts meet, they combine into a giant tornado that deals {{ e5 }} (+{{ charabilitypower2*3 }}) plus {{ e2 }}% of enemies' maximum Health (max {{ e4 }} vs. monsters) as magic damage over {{ e3 }} seconds.","leveltip":{"label":["Windblast Damage","Tornado Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[60,95,130,165,200],[6,6,6,6,6],[1.5,1.5,1.5,1.5,1.5],[150,150,150,150,150],[45,60,75,90,105],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","6","1.5","150","45/60/75/90/105","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[825,825,825,825,825],"rangeBurn":"825","image":{"full":"GalioQ.png","sprite":"spell3.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GalioW","name":"Shield of Durand","description":"Galio charges a defensive stance, moving slowly. Upon releasing the charge, Galio will taunt nearby enemies.","tooltip":"Passive: After not taking damage for {{ e6 }} seconds Galio gains a shield that absorbs {{ f3 }} magic damage.

    First Cast: Galio starts to charge, gaining {{ e1 }} (+{{ f2 }})% Damage Reduction but slowing himself by {{ e3 }}%.

    Second Cast: Galio taunts nearby enemy champions for {{ e4 }} to {{ e7 }} seconds and refreshes the Damage Reduction for {{ e8 }} seconds. Taunt duration and radius increase with charge time.

    Shield of Durand's charge cannot be interrupted by crowd control.","leveltip":{"label":["Shield Health Ratio","Damage Reduction","Cooldown"],"effect":["{{ e5 }}% -> {{ e5NL }}%","{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[20,25,30,35,40],[2,2,2,2,2],[30,30,30,30,30],[0.5,0.5,0.5,0.5,0.5],[8,11,14,17,20],[12,12,12,12,12],[1.5,1.5,1.5,1.5,1.5],[2,2,2,2,2],[1.25,1.25,1.25,1.25,1.25],[0,0,0,0,0]],"effectBurn":[null,"20/25/30/35/40","2","30","0.5","8/11/14/17/20","12","1.5","2","1.25","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[275,275,275,275,275],"rangeBurn":"275","image":{"full":"GalioW.png","sprite":"spell3.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GalioE","name":"Justice Punch","description":"Galio will briefly step back and charge, knocking up the first enemy champion he encounters.","tooltip":"Galio lunges forward with a mighty blow, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies and knocking them into the air for {{ e2 }} seconds. Galio will stop upon hitting a champion or terrain.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[100,140,180,220,260],[0.75,0.75,0.75,0.75,0.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100/140/180/220/260","0.75","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"GalioE.png","sprite":"spell3.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GalioR","name":"Hero's Entrance","description":"Galio grants damage reduction to an ally. After a delay Galio smashes down on the ally's original location, knocking up nearby enemies.","tooltip":"Galio designates an allied champion's current position as his landing spot and grants {{ e5 }}
    (+{{ f2 }})% damage reduction to that ally until he lands.

    When Galio lands, enemies in the area take {{ e1 }} (+{{ a1 }}) magic damage and are knocked into the air for {{ e3 }} seconds ({{ e2 }} seconds in the center).","leveltip":{"label":["Damage","Damage Reduction to Ally","Range","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }}% -> {{ e5NL }}%","{{ e8 }} -> {{ e8NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,140,120],"cooldownBurn":"160/140/120","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[1.25,1.25,1.25],[0.75,0.75,0.75],[2.5,2.5,2.5],[20,30,40],[0,0,0],[0,0,0],[4000,4750,5500],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","1.25","0.75","2.5","20/30/40","0","0","4000/4750/5500","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[4000,4750,5500],"rangeBurn":"4000/4750/5500","image":{"full":"GalioR.png","sprite":"spell3.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Gangplank":{"id":41,"key":"Gangplank","name":"Gangplank","title":"the Saltwater Scourge","spells":[{"id":"GangplankQWrapper","name":"Parrrley","description":"Shoots target, plundering Gold for each enemy unit killed.","tooltip":"Fires a bullet that deals {{ e1 }} (+{{ a1 }}) physical damage (can crit and applies on-hit effects).

    If Parrrley kills the target, Gangplank plunders {{ e2 }} bonus Gold and {{ e5 }} Silver Serpents. (Trade Silver Serpents at the shop to upgrade Cannon Barrage).

    ","leveltip":{"label":["Damage","Plunder Gold","Plunder Silver Serpents"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e5 }} -> {{ e5NL }}"]},"maxrank":5,"cooldown":[5,5,5,5,5],"cooldownBurn":"5","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[20,45,70,95,120],[2,3,4,5,6],[0,0,0,0,0],[500,500,500,500,500],[4,5,6,7,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/45/70/95/120","2/3/4/5/6","0","500","4/5/6/7/8","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"GangplankQWrapper.png","sprite":"spell3.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GangplankW","name":"Remove Scurvy","description":"Eats citrus to cure crowd control effects and restore Health.","tooltip":"Gangplank consumes a large quantity of citrus fruit, curing all disabling effects and healing him for {{ e1 }} (+{{ a1 }}) + {{ e2 }}% of his missing Health.","leveltip":{"label":["Healing","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[22,20,18,16,14],"cooldownBurn":"22/20/18/16/14","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[50,75,100,125,150],[15,15,15,15,15],[0.25,0.25,0.25,0.25,0.25],[200,200,200,200,200],[30,40,50,60,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","15","0.25","200","30/40/50/60/70","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"GangplankW.png","sprite":"spell3.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GangplankE","name":"Powder Keg","description":"Gangplank uncovers a powder keg at target location. If he attacks it, it explodes, spreading the attack's damage to enemies in the area, slowing them.","tooltip":"Places a powder keg that can be attacked by Gangplank or his enemies at a location for {{ e5 }} seconds.

    If Gangplank destroys a keg, it explodes dealing the attack's damage as physical damage (ignores {{ e0 }}% Armor) to enemies and slowing them by {{ e4 }}% for {{ e2 }} seconds. Champions take {{ e3 }} bonus physical damage from the explosion.

    When a keg explodes, other kegs with overlapping blast zones chain explode (damage does not stack).

    Kegs health decays every {{ f5 }} seconds. (Decay rate increases at level 7 and 13)

    Keg explosions apply Parrrley's plunder effect.","leveltip":{"label":["Barrel Ammo Charge","Slow","Bonus Damage Against Champions"],"effect":["{{ f2 }} -> {{ f3 }}","{{ e4 }}% -> {{ e4NL }}%","{{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[3,3,3,3,3],[2,2,2,2,2],[60,90,120,150,180],[40,50,60,70,80],[60,60,60,60,60],[2,2,2,2,2],[10,10,10,10,10],[100,100,100,100,100],[0.5,0.5,0.5,0.5,0.5],[40,40,40,40,40]],"effectBurn":[null,"3","2","60/90/120/150/180","40/50/60/70/80","60","2","10","100","0.5","40"],"vars":[],"costType":"No Cost","maxammo":"3","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"GangplankE.png","sprite":"spell3.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"GangplankR","name":"Cannon Barrage","description":"Gangplank signals his ship to bombard an area, slowing and damaging enemies.","tooltip":"Signals Gangplank's ship to fire {{ f3 }} waves of cannonballs at an area over {{ e3 }} seconds. Each wave deals {{ e1 }} (+{{ a1 }}) magic damage and slows enemies by {{ e2 }}% for {{ e6 }} seconds.

    Collect Silver Serpents with Parrrley to upgrade in the shop.","leveltip":{"label":["Damage Per Wave","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[180,160,140],"cooldownBurn":"180/160/140","cost":[100,100,100],"costBurn":"100","effect":[null,[35,60,85],[30,30,30],[8,8,8],[2,2,2],[1.5,0.5,0.5],[0.5,0.5,0.5],[300,300,300],[60,60,60],[1,1,1],[30,30,30]],"effectBurn":[null,"35/60/85","30","8","2","1.5/0.5/0.5","0.5","300","60","1","30"],"vars":[{"link":"spelldamage","coeff":0.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[30000,30000,30000],"rangeBurn":"30000","image":{"full":"GangplankR.png","sprite":"spell3.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Garen":{"id":86,"key":"Garen","name":"Garen","title":"The Might of Demacia","spells":[{"id":"GarenQ","name":"Decisive Strike","description":"Garen gains a burst of movement speed, breaking free of all slows affecting him. His next attack strikes a vital area of his foe, dealing bonus damage and silencing them.","tooltip":"Garen breaks free from all slows affecting him, and gains {{ e2 }}% movement speed for {{ e4 }} seconds.

    His next basic attack within {{ e5 }} seconds deals {{ e1 }} (+{{ a1 }}) physical damage and silences his target for {{ e3 }} seconds.","leveltip":{"label":["Bonus Damage","Movement Speed Duration"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,55,80,105,130],[30,30,30,30,30],[1.5,1.5,1.5,1.5,1.5],[1.5,2,2.5,3,3.5],[4.5,4.5,4.5,4.5,4.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/55/80/105/130","30","1.5","1.5/2/2.5/3/3.5","4.5","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.4,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"GarenQ.png","sprite":"spell3.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"GarenW","name":"Courage","description":"Garen passively increases his armor and magic resist by killing enemies. He may also activate this ability to grant himself a shield that reduces incoming damage for a short time.","tooltip":"Passive: Killing units permanently grants {{ f2 }} armor and magic resist, up to a maximum of {{ e3 }}. Current Bonus: {{ f1 }}

    Active: Garen gains a defensive shield for {{ e4 }} seconds, reducing incoming damage by {{ e2 }}%.","leveltip":{"label":["Active Duration","Cooldown"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[24,23,22,21,20],"cooldownBurn":"24/23/22/21/20","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[0.25,0.25,0.25,0.25,0.25],[30,30,30,30,30],[30,30,30,30,30],[2,3,4,5,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0.25","30","30","2/3/4/5/6","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"GarenW.png","sprite":"spell3.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"GarenE","name":"Judgment","description":"Garen performs a dance of death with his sword, dealing damage around him for the duration and shredding the armor of enemy champions hit.","tooltip":"Garen rapidly spins his sword around his body for {{ e2 }} seconds, dealing {{ f3 }} physical damage to nearby enemies -- {{ e1 }} plus {{ e3 }}% of his attack damage (+{{ f1 }}), {{ f2 }} times (increased by 1 every 3 champion levels) -- over the duration.

    Enemy champions hit by {{ e6 }} spins lose {{ e5 }}% of their armor for {{ e7 }} seconds.

    Judgment deals {{ e4 }}% increased damage when striking only one enemy.
    Cancelling Judgment returns cooldown equal to the remaining duration.
    Judgment can critically strike for bonus damage.","leveltip":{"label":["Base Damage per hit","Total Attack Damage Ratio per hit"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[14,18,22,26,30],[3,3,3,3,3],[34,35,36,37,38],[33,33,33,33,33],[25,25,25,25,25],[4,4,4,4,4],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"14/18/22/26/30","3","34/35/36/37/38","33","25","4","6","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"GarenE.png","sprite":"spell3.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"GarenR","name":"Demacian Justice","description":"The enemy champion with the most recent kills is the Villain. Garen's attacks deal additional true damage to that champion.

    Garen can call upon the might of Demacia to deal a finishing blow to an enemy champion that deals damage based upon how much health his target has missing. This damage is true damage against the Villain.","tooltip":"Passive: The enemy champion with the most recent kills is the Villain. Judgment and basic attacks against it deal an additional {{ e3 }}% of its maximum health as true damage.

    Active: Garen calls upon the might of Demacia to attempt to execute an enemy champion, dealing magic damage equal to {{ e1 }} plus {{ e2 }}% of the target's missing health. Deals true damage to the Villain.","leveltip":{"label":["Damage","Missing Health Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[0,0,0],"costBurn":"0","effect":[null,[175,350,525],[28,33,40],[1,1,1],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"175/350/525","28/33/40","1","0","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[400,400,400],"rangeBurn":"400","image":{"full":"GarenR.png","sprite":"spell3.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Gnar":{"id":150,"key":"Gnar","name":"Gnar","title":"the Missing Link","spells":[{"id":"GnarQ","name":"Boomerang Throw / Boulder Toss","description":"Gnar throws a boomerang that damages and slows enemies it hits before returning to him. If he catches the boomerang its cooldown is reduced.

    Mega Gnar instead throws a boulder that stops on the first unit hit, damaging and slowing everything nearby. It can then be picked up to reduce the cooldown.","tooltip":"Mini Gnar: Throws a boomerang that deals {{ e1 }} (+{{ a1 }}) physical damage and slows enemies by {{ e3 }}% for {{ e4 }} seconds. The boomerang returns towards Gnar after hitting an enemy, dealing {{ e7 }}% damage to subsequent targets. Each enemy can only be hit once. Catching the boomerang reduces its cooldown by {{ f1 }}%.

    Mega Gnar: Throws a boulder that stops when it hits an enemy, slowing all nearby enemies and dealing {{ e2 }} (+{{ a2 }}) physical damage. Picking up the boulder reduces its cooldown by {{ e6 }}%.","leveltip":{"label":["Boomerang Damage","Boulder Damage","Slow Amount","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,17.5,15,12.5,10],"cooldownBurn":"20/17.5/15/12.5/10","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[5,35,65,95,125],[5,45,85,125,165],[15,20,25,30,35],[2,2,2,2,2],[3000,3000,3000,3000,3000],[60,60,60,60,60],[50,50,50,50,50],[200,200,200,200,200],[6,6,6,6,6],[280,280,280,280,280]],"effectBurn":[null,"5/35/65/95/125","5/45/85/125/165","15/20/25/30/35","2","3000","60","50","200","6","280"],"vars":[{"link":"attackdamage","coeff":1.15,"key":"a1"},{"link":"attackdamage","coeff":1.2,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"GnarQ.png","sprite":"spell3.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"GnarW","name":"Hyper / Wallop","description":"Gnar's attacks and spells hype him up, dealing bonus damage and granting him Movement Speed.

    Mega Gnar is too enraged to be hyper and instead can rear up on his hind legs and smash down on the area in front of him, stunning enemies in an area.","tooltip":"Mini Gnar: Passive: Every 3rd attack or spell on the same target deals an additional {{ e1 }} (+{{ a1 }}) + {{ e2 }}% of the target's max Health as magic damage and grants Gnar {{ f1 }}% Movement Speed that decays over 3 seconds (max {{ e4 }} damage vs. monsters).

    Mega Gnar: Stuns enemies in an area for {{ e5 }} seconds, dealing {{ e3 }} (+{{ a2 }}) physical damage.

    Gnar gains Hyper's Movement Speed bonus when he leaves Mega form.","leveltip":{"label":["Hyper Damage","Hyper % Health Damage","Hyper Max Damage to monsters","Wallop Damage","Wallop Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e4 }} -> {{ e4NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,13,11,9,7],"cooldownBurn":"15/13/11/9/7","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,20,30,40,50],[6,8,10,12,14],[25,45,65,85,105],[100,150,200,250,300],[1.25,1.25,1.25,1.25,1.25],[3.5,3.5,3.5,3.5,3.5],[3,3,3,3,3],[550,550,550,550,550],[200,200,200,200,200],[0,0,0,0,0]],"effectBurn":[null,"10/20/30/40/50","6/8/10/12/14","25/45/65/85/105","100/150/200/250/300","1.25","3.5","3","550","200","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"},{"link":"attackdamage","coeff":1,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[0,0,0,0,0],"rangeBurn":"0","image":{"full":"GnarW.png","sprite":"spell3.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"GnarE","name":"Hop / Crunch","description":"Gnar leaps to a location and bounces off the head of any unit he lands on, traveling further.

    Mega Gnar is too large to bounce and instead lands with earth-shattering force, dealing damage in an area around him.","tooltip":"Mini Gnar: Leaps to a location, gaining {{ e2 }}% Attack Speed for {{ e3 }} seconds. If Gnar lands on a unit he will bounce off it, traveling further. Deals {{ e1 }} (+{{ f1 }}) [6% of Gnar's Max Health] physical damage and slows briefly if the unit landed on was an enemy.

    Mega Gnar: Leaps to a location and deals {{ e1 }} (+{{ f1 }}) [6% of Gnar's Max Health] physical damage to nearby enemies on landing. Enemies Gnar lands directly on top of are slowed briefly.

    If Crunch is used to transform, Gnar will still be able to bounce.","leveltip":{"label":["Damage","Attack Speed","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[22,19.5,17,14.5,12],"cooldownBurn":"22/19.5/17/14.5/12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,60,100,140,180],[20,30,40,50,60],[3,3,3,3,3],[-0.8,-0.8,-0.8,-0.8,-0.8],[0.5,0.5,0.5,0.5,0.5],[475,475,475,475,475],[525,525,525,525,525],[200,200,200,200,200],[0.6,0.6,0.6,0.6,0.6],[375,375,375,375,375]],"effectBurn":[null,"20/60/100/140/180","20/30/40/50/60","3","-0.8","0.5","475","525","200","0.6","375"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[475,475,475,475,475],"rangeBurn":"475","image":{"full":"GnarE.png","sprite":"spell3.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"GnarR","name":"GNAR!","description":"Mega Gnar throws everything around him in a chosen direction, dealing damage and slowing them. Any enemy that hits a wall is stunned and takes bonus damage.","tooltip":"Mini Gnar: Passive: Increases Boomerang Throw's cooldown reduction on catch to {{ e4 }}% and Hyper's Movement Speed bonus to {{ e2 }}%.

    Mega Gnar: Knocks nearby enemies in the specified direction, dealing {{ e1 }} (+{{ a1 }}) (+{{ a2 }}) physical damage and slowing them by {{ e8 }}% for {{ e3 }} seconds. Enemies that hit a wall take {{ e7 }}% damage and are stunned instead of slowed.","leveltip":{"label":["Damage","Slow/Stun Duration","Boomerang Cooldown Reduction","Hyper Movement Speed","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[0,0,0],"costBurn":"0","effect":[null,[200,300,400],[45,60,75],[1.25,1.5,1.75],[50,55,60],[475,475,475],[500,500,500],[150,150,150],[45,45,45],[0,0,0],[0,0,0]],"effectBurn":[null,"200/300/400","45/60/75","1.25/1.5/1.75","50/55/60","475","500","150","45","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.5,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[590,590,590],"rangeBurn":"590","image":{"full":"GnarR.png","sprite":"spell3.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Gragas":{"id":79,"key":"Gragas","name":"Gragas","title":"the Rabble Rouser","spells":[{"id":"GragasQ","name":"Barrel Roll","description":"Gragas rolls his cask to a location, which can be activated to explode or will explode on its own after 4 seconds. Enemies struck by the blast have their Movement Speed slowed.","tooltip":"Gragas rolls his cask to a location. When reactivated, or after {{ e4 }} seconds, the cask will explode, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and slowing their movement speed by {{ e2 }}% for {{ e3 }} seconds.

    The damage and slow amount increase as the cask ferments, up to {{ e6 }}% after {{ e5 }} seconds. Deals {{ e7 }}% damage to minions.","leveltip":{"label":["Damage","Slow %","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[80,120,160,200,240],[40,45,50,55,60],[2,2,2,2,2],[4,4,4,4,4],[2,2,2,2,2],[150,150,150,150,150],[70,70,70,70,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","40/45/50/55/60","2","4","2","150","70","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"GragasQ.png","sprite":"spell3.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GragasW","name":"Drunken Rage","description":"Gragas guzzles down brew from his cask for 1 second. After finishing, he becomes drunkenly empowered, dealing magic damage to all nearby enemies on his next basic attack and reducing damage received.","tooltip":"Gragas guzzles down his brew, taking {{ e1 }}% reduced damage for {{ e5 }} seconds.

    After finishing his drink, his next basic attack deals magic damage to nearby enemies equal to {{ e3 }} (+{{ a1 }}) plus {{ e2 }}% of their max Health (max {{ e4 }} vs monsters).","leveltip":{"label":["Damage Reduction","Base Damage","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7,6,5,4],"cooldownBurn":"8/7/6/5/4","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[10,12,14,16,18],[8,8,8,8,8],[20,50,80,110,140],[300,300,300,300,300],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/12/14/16/18","8","20/50/80/110/140","300","2.5","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"GragasW.png","sprite":"spell3.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GragasE","name":"Body Slam","description":"Gragas charges to a location and collides with the first enemy unit he comes across, dealing damage to all nearby enemy units and stunning them.","tooltip":"Gragas charges forward, colliding with the first enemy unit. He deals {{ e1 }} (+{{ a1 }}) magic damage to enemies in the area and bumps them back, stunning them for {{ e2 }} second.

    Body Slam's Cooldown is reduced by {{ f1 }} seconds if Gragas collides with a unit.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[80,130,180,230,280],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/130/180/230/280","1","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"GragasE.png","sprite":"spell3.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GragasR","name":"Explosive Cask","description":"Gragas hurls his cask to a location, dealing damage and knocking back enemies caught in the blast radius.","tooltip":"Gragas hurls his cask, causing it to explode when it lands. Enemies hit take {{ e1 }} (+{{ a1 }}) magic damage and are knocked away from the explosion.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[200,300,400],[900,900,900],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"200/300/400","900","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"GragasR.png","sprite":"spell3.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Graves":{"id":104,"key":"Graves","name":"Graves","title":"the Outlaw","spells":[{"id":"GravesQLineSpell","name":"End of the Line","description":"Graves fires an explosive shell that detonates after 2 seconds, or 0.2 seconds if it strikes terrain.","tooltip":"Fires a powder round that deals {{ e1 }} (+{{ f1 }}) physical damage to enemies in a line.

    After 2 seconds or {{ e6 }} seconds on collision with terrain, the round detonates, dealing {{ e2 }} (+{{ f2 }}) physical damage to all nearby enemies.","leveltip":{"label":["Damage","Detonation AD Ratio","Mana Cost","Cooldown "],"effect":[" {{ e1 }} -> {{ e1NL }} / {{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[40,55,70,85,100],[80,110,140,170,200],[0.4,0.7,1,1.3,1.6],[0,0,0,0,0],[1,1,1,1,1],[0.2,0.2,0.2,0.2,0.2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/55/70/85/100","80/110/140/170/200","0.4/0.7/1/1.3/1.6","0","1","0.2","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"GravesQLineSpell.png","sprite":"spell3.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GravesSmokeGrenade","name":"Smoke Screen","description":"Graves fires a smoke canister at the target area creating a cloud of smoke. Enemies inside the smoke cloud have reduced sight range and Movement Speed. ","tooltip":"Creates a cloud of Black Smoke lasting 4 seconds. Enemies inside Black Smoke cannot see out.

    Deals {{ e1 }} (+{{ a1 }}) magic damage and slows by {{ e2 }}% on impact.","leveltip":{"label":["Damage","Slow","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[26,24,22,20,18],"cooldownBurn":"26/24/22/20/18","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[60,110,160,210,260],[15,20,25,30,35],[200,200,200,200,200],[4,4,4,4,4],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/110/160/210/260","15/20/25/30/35","200","4","1.5","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"GravesSmokeGrenade.png","sprite":"spell3.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GravesMove","name":"Quickdraw","description":"Graves dashes forward gaining an Armor boost for several seconds. If Graves dashes towards an enemy champion, gain two stacks of True Grit instead. Hitting enemies with basic attacks lowers the cooldown of this skill and refreshes the resistance boost. ","tooltip":"Dashes in a direction, reloading one shell. Graves gains True Grit for 4 seconds. If Graves dashes towards an enemy champion, gain two stacks of True Grit instead.

    Basic attack hits lower the cooldown of Quickdraw by {{ e4 }} seconds. Damage against non-minions also refreshes True Grit.

    True Grit grants {{ e5 }} Armor (stacks up to {{ e0 }} times).","leveltip":{"label":["True Grit Armor","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[30,40,50,60,70],[4,4,4,4,4],[20,25,30,35,40],[0.5,0.5,0.5,0.5,0.5],[5,7.5,10,12.5,15],[750,750,750,750,750],[375,375,375,375,375],[275,275,275,275,275],[60,60,60,60,60],[8,8,8,8,8]],"effectBurn":[null,"30/40/50/60/70","4","20/25/30/35/40","0.5","5/7.5/10/12.5/15","750","375","275","60","8"],"vars":[],"costType":" Mana","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"GravesMove.png","sprite":"spell3.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"GravesChargeShot","name":"Collateral Damage","description":"Graves fires an explosive shell dealing heavy damage to the first champion it hits. After hitting a champion or reaching the end of its range, the shell explodes dealing damage in a cone. ","tooltip":"Fires an explosive shell with such force that it knocks Graves back. The shell deals {{ e1 }} (+{{ f1 }}) physical damage to the first enemy hit. After hitting an enemy Champion or reaching the end of its range, the shell explodes, dealing {{ e2 }} (+{{ f2 }}) physical damage in a cone.","leveltip":{"label":["Primary Damage","Cone Damage","Cooldown "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[250,400,550],[200,320,440],[400,400,400],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250/400/550","200/320/440","400","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"GravesChargeShot.png","sprite":"spell4.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Hecarim":{"id":120,"key":"Hecarim","name":"Hecarim","title":"the Shadow of War","spells":[{"id":"HecarimRapidSlash","name":"Rampage","description":"Hecarim cleaves nearby enemies dealing physical damage. ","tooltip":"Hecarim cleaves nearby enemies for {{ e1 }} (+{{ f1 }}) physical damage. ({{ e5 }}% damage to minions)

    If Hecarim damages at least one enemy with this attack he gains a stack of Rampage, reducing the base cooldown of this skill by {{ e3 }} second for a short duration. This effect can stack up to {{ e2 }} times.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[32,34,36,38,40],"costBurn":"32/34/36/38/40","effect":[null,[50,85,120,155,190],[2,2,2,2,2],[1,1,1,1,1],[3,3,3,3,3],[66,66,66,66,66],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/85/120/155/190","2","1","3","66","6","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"HecarimRapidSlash.png","sprite":"spell4.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HecarimW","name":"Spirit of Dread","description":"Hecarim deals magic damage to nearby enemies for a short duration. Hecarim gains Health equal to a percentage of any damage those enemies suffer.","tooltip":"Hecarim deals {{ e2 }} (+{{ a1 }}) magic damage over {{ e3 }} seconds to all nearby enemies. Hecarim is healed for {{ e1 }}% of the damage these enemies take from any source.

    Hecarim cannot heal more than {{ e4 }} Health from minions or monsters.","leveltip":{"label":["Damage","Healing Cap","Cooldown","Mana Cost "],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[22,21,20,19,18],"cooldownBurn":"22/21/20/19/18","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[20,20,20,20,20],[80,120,160,200,240],[4,4,4,4,4],[90,120,150,180,210],[8,8,8,8,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20","80/120/160/200/240","4","90/120/150/180/210","8","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[575,575,575,575,575],"rangeBurn":"575","image":{"full":"HecarimW.png","sprite":"spell4.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HecarimRamp","name":"Devastating Charge","description":"Hecarim gains increasing Movement Speed and can move through units for a short duration. His next attack knocks the target back and deals additional physical damage based on the distance he has traveled since activating the ability. ","tooltip":"Hecarim gains increasing Movement Speed and can move through units for {{ e5 }} seconds. His next attack knocks the target back dealing {{ e4 }} (+{{ a1 }}) to {{ e3 }} (+{{ a2 }}) physical damage based on how far Hecarim has traveled during Devastating Charge (knockback distance also scales).","leveltip":{"label":["Minimum Damage","Maximum Damage","Cooldown"],"effect":[" {{ e4 }} -> {{ e4NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[24,22,20,18,16],"cooldownBurn":"24/22/20/18/16","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[250,250,250,250,250],[450,450,450,450,450],[80,150,220,290,360],[40,75,110,145,180],[4,4,4,4,4],[0.75,0.75,0.75,0.75,0.75],[1200,1200,1200,1200,1200],[0.25,0.25,0.25,0.25,0.25],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0]],"effectBurn":[null,"250","450","80/150/220/290/360","40/75/110/145/180","4","0.75","1200","0.25","2.5","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"a1"},{"link":"bonusattackdamage","coeff":1,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"HecarimRamp.png","sprite":"spell4.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HecarimUlt","name":"Onslaught of Shadows","description":"Hecarim summons spectral riders and charges forward, dealing magic damage in a line. Hecarim creates a shockwave when he finishes his charge, causing nearby enemies to flee in terror.","tooltip":"Hecarim summons spectral riders and charges forward, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies hit.

    Hecarim releases a shockwave when he finishes his charge, causing nearby enemies to flee from him for {{ e2 }} second.

    Hecarim himself will only move to the targeted location. The riders will always move the full distance.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[1,1,1],[50,125,200],[1100,1100,1100],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","1","50/125/200","1100","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[50000,50000,50000],"rangeBurn":"50000","image":{"full":"HecarimUlt.png","sprite":"spell4.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Heimerdinger":{"id":74,"key":"Heimerdinger","name":"Heimerdinger","title":"the Revered Inventor","spells":[{"id":"HeimerdingerQ","name":"H-28G Evolution Turret","description":"Heimerdinger lays down a rapid-fire cannon turret equipped with a secondary pass-through beam attack (turrets deal half damage to towers).","tooltip":"Places a Turret. Turret attacks prioritize Heimerdinger's targets and enemies attacking Heimerdinger. Turrets shut down if Heimerdinger moves far away. Heimerdinger generates a Turret Kit every {{ f1 }} seconds and can hold {{ e3 }} Kits at once.

    H-28G Evolution Turret Stats
    Health: {{ f4 }} (+{{ f2 }})
    Attack - Cannon: {{ e1 }} (+{{ a1 }}) Magic Damage.
    Attack - Beam: {{ e2 }} (+{{ a2 }}) Magic Damage every {{ e6 }} seconds.
    Maximum Turrets Placed: 3","leveltip":{"label":["Cannon Damage","Beam Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[1,1,1,1,1],"cooldownBurn":"1","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[6,9,12,15,18],[40,60,80,100,120],[3,3,3,3,3],[26,22,18,14,10],[900,900,900,900,900],[90,90,90,90,90],[0,0,0,0,0],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[20,20,20,20,20]],"effectBurn":[null,"6/9/12/15/18","40/60/80/100/120","3","26/22/18/14/10","900","90","0","0.25","0","20"],"vars":[{"link":"@cooldownchampion","coeff":0,"key":"f1"},{"link":"@text","coeff":[150,175,200,225,250,275,300,325,350,375,400,425,450,475,500,525,550,575],"key":"f4"},{"link":"@cooldownchampion","coeff":0,"key":"f2"},{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.55,"key":"a2"}],"costType":" Mana and Turret Kit","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"HeimerdingerQ.png","sprite":"spell4.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana and Turret Kit"},{"id":"HeimerdingerW","name":"Hextech Micro-Rockets","description":"Heimerdinger fires long-range rockets that converge on his cursor. ","tooltip":"Unleashes a barrage of 5 rockets that converge towards the cursor and fan out past it. Rockets deal {{ e1 }} (+{{ a1 }}) Magic Damage. Additional rocket hits deal reduced damage:

    Champions and Monsters: {{ e2 }} (+{{ a2 }}) Magic Damage, max {{ e6 }} (+{{ f2 }}) total damage
    Minions: 60% of base damage

    Rocket hits against champions charge nearby turret beam attacks.","leveltip":{"label":["Cooldown","Damage","Maximum Damage","Mana Cost"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}","{{ e6 }} -> {{ e6NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[60,90,120,150,180],[12,18,24,30,36],[25,25,25,25,25],[20,20,20,20,20],[30,30,30,30,30],[108,162,216,270,324],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/90/120/150/180","12/18/24/30/36","25","20","30","108/162/216/270/324","5","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"},{"link":"spelldamage","coeff":0.12,"key":"a2"},{"link":"@dynamic.abilitypower","coeff":0.93,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[1325,1325,1325,1325,1325],"rangeBurn":"1325","image":{"full":"HeimerdingerW.png","sprite":"spell4.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HeimerdingerE","name":"CH-2 Electron Storm Grenade","description":"Heimerdinger lobs a grenade at a location, dealing damage to enemy units, as well as stunning anyone directly hit and slowing surrounding units.","tooltip":"Hurls a grenade that deals {{ e1 }} (+{{ a1 }}) Magic Damage to enemies and slows their Movement Speed by {{ e3 }}% for {{ e2 }} seconds. Enemies in the center of the blast are also stunned for {{ e4 }} seconds.

    Hitting a champion fully charges nearby turret beams.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[85,85,85,85,85],"costBurn":"85","effect":[null,[60,100,140,180,220],[2,2,2,2,2],[35,35,35,35,35],[1.25,1.25,1.25,1.25,1.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","2","35","1.25","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[970,970,970,970,970],"rangeBurn":"970","image":{"full":"HeimerdingerE.png","sprite":"spell4.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HeimerdingerR","name":"UPGRADE!!!","description":"Heimerdinger invents an upgrade, causing his next spell to have increased effects. ","tooltip":"Makes Heimerdinger's next ability free and gives it bonus effects. Reactivate to cancel.

    H-28Q Apex Turret: Places a Turret for 8 seconds that deals {{ e7 }} (+{{ a2 }}) Magic Damage with its cannon and {{ e1 }} (+{{ a1 }}) Magic Damage with its beam. It has splash damage, immunity to disables, its attacks slow by 25% for 2 seconds and it doesn't count toward the Turret limit.

    Hextech Rocket Swarm: Fires 4 waves of rockets that each deal {{ e8 }} (+{{ f1 }}) Magic Damage. Champions and Monsters hit by multiple rockets take reduced damage, max {{ e5 }} (+{{ f2 }}).

    CH-3X Lightning Grenade: Throws a bouncing grenade that discharges three times, dealing {{ e0 }} (+{{ f4 }}) Magic Damage. Both the stun and slow areas are larger and the slow is improved to 80%. ","leveltip":{"label":["Cooldown","Apex Turret Cannon Damage","Turret Beam Damage","Rocket Swarm Damage","Rocket Swarm Max Damage","Lightning Grenade Damage"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e7 }} -> {{ e7NL }}","{{ e1 }} -> {{ e1NL }}","{{ e8 }} -> {{ e8NL }}","{{ e5 }} -> {{ e5NL }}","{{ e0 }} -> {{ e0NL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[100,100,100],"costBurn":"100","effect":[null,[200,270,340],[80,80,80],[1.5,1.5,1.5],[0.12,0.12,0.12],[500,690,865],[0.45,0.45,0.45],[80,100,120],[135,180,225],[28,39,49],[150,250,350]],"effectBurn":[null,"200/270/340","80","1.5","0.12","500/690/865","0.45","80/100/120","135/180/225","28/39/49","150/250/350"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a2"},{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"@dynamic.abilitypower","coeff":0,"key":"f1"},{"link":"@dynamic.abilitypower","coeff":1.83,"key":"f2"},{"link":"@dynamic.abilitypower","coeff":0.6,"key":"f4"}],"costType":" Mana","maxammo":"-1","range":[1,1,1],"rangeBurn":"1","image":{"full":"HeimerdingerR.png","sprite":"spell4.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Illaoi":{"id":420,"key":"Illaoi","name":"Illaoi","title":"the Kraken Priestess","spells":[{"id":"IllaoiQ","name":"Tentacle Smash","description":"Increases the damage dealt by Tentacles. When activated, Illaoi smashes down a Tentacle that deals physical damage.","tooltip":"Passive: Slam damage is increased by {{ e6 }}% ({{ f1 }}).

    Active: Illaoi swings her idol, causing a Tentacle to Slam forward.","leveltip":{"label":["Slam Bonus","Cooldown","Mana Cost"],"effect":["{{ e6 }}% -> {{ e6NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[10,10,10,10,10],[200,200,200,200,200],[800,800,800,800,800],[10,8,6,4,2],[5,5,5,5,5],[10,15,20,25,30],[1.2,1.2,1.2,1.2,1.2],[-0.3,-0.35,-0.4,-0.45,-0.5],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0]],"effectBurn":[null,"10","200","800","10/8/6/4/2","5","10/15/20/25/30","1.2","-0.3/-0.35/-0.4/-0.45/-0.5","1.5","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"IllaoiQ.png","sprite":"spell4.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IllaoiW","name":"Harsh Lesson","description":"Illaoi leaps to her target, dealing physical damage and causing nearby Tentacles to also swing at the target.","tooltip":"Illaoi leaps at her target on her next basic attack, dealing bonus physical damage equal to {{ f1*100 }}% of their maximum health [{{ e1 }}% + {{ f2*100 }}% per 100 attack damage].

    When she strikes, nearby Tentacles will Slam at the target.

    Harsh Lesson's bonus damage against monsters is capped at {{ e3 }} damage per hit.","leveltip":{"label":["Base Damage"],"effect":["{{ e1 }}% -> {{ e1NL }}%"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[3,3.5,4,4.5,5],[2,2,2,2,2],[300,300,300,300,300],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"3/3.5/4/4.5/5","2","300","6","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"IllaoiW.png","sprite":"spell4.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IllaoiE","name":"Test of Spirit","description":"Illaoi rips the spirit from a foe's body, forcing it to stand before her. Spirits echo a percentage of the damage they take to the original target. If killed, or if the target gets too far from the spirit, the target will become a Vessel and begin spawning Tentacles.","tooltip":"Illaoi pulls the spirit from an enemy champion for {{ e3 }} seconds (reduced when Illaoi is attacked by the target). The spirit can be attacked, with {{ e1 }}% (+{{ f5 }}%) of the damage taken echoing to its owner.

    If the spirit dies or the target leaves its range, the target becomes a Vessel for {{ e2 }} seconds and is slowed by {{ e8 }}% for {{ e5 }} seconds. Vessels spawn Tentacles every {{ f1 }} seconds if no other Tentacles are nearby.

    Tentacles will automatically Slam at spirits and Vessels once every 10 seconds.","leveltip":{"label":["Damage Echo","Cooldown","Mana Cost"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ f4 }} -> {{ f6 }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[35,40,45,50,55],"costBurn":"35/40/45/50/55","effect":[null,[25,30,35,40,45],[12,12,12,12,12],[10,10,10,10,10],[1500,1500,1500,1500,1500],[1.5,1.5,1.5,1.5,1.5],[3,3,3,3,3],[20,18,16,14,12],[80,80,80,80,80],[8,8,8,8,8],[4,3,0,0,0]],"effectBurn":[null,"25/30/35/40/45","12","10","1500","1.5","3","20/18/16/14/12","80","8","4/3/0/0/0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"IllaoiE.png","sprite":"spell4.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IllaoiR","name":"Leap of Faith","description":"Illaoi smashes her idol into the ground, dealing physical damage to nearby enemies. A Tentacle spawns for each enemy champion hit.","tooltip":"Illaoi smashes her idol into the ground, dealing {{ e2 }} (+{{ a1 }}) physical damage to nearby enemies and spawning a Tentacle for each enemy champion hit.

    For the next {{ e1 }} seconds Tentacles are untargetable and will Slam 50% faster, and Harsh Lesson has a 2 second cooldown.

    'There are kind and gentle gods. Mine isn't one of those.'","leveltip":{"label":["Base Damage","Cooldown<"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[100,100,100],"costBurn":"100","effect":[null,[8,8,8],[150,250,350],[1500,1500,1500],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"8","150/250/350","1500","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450],"rangeBurn":"450","image":{"full":"IllaoiR.png","sprite":"spell4.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Irelia":{"id":39,"key":"Irelia","name":"Irelia","title":"the Will of the Blades","spells":[{"id":"IreliaGatotsu","name":"Bladesurge","description":"Irelia dashes forward to strike her target. If it kills the target, Bladesurge's cooldown refreshes and refunds a portion of the Mana Cost.","tooltip":"Irelia dashes forward to strike her target, dealing {{ e1 }} (+{{ f1 }}) physical damage and applying on-hit effects.

    If it kills the target, Bladesurge's cooldown is refreshed and Irelia gains {{ e2 }} Mana.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[14,12,10,8,6],"cooldownBurn":"14/12/10/8/6","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[20,50,80,110,140],[35,35,35,35,35],[1400,1400,1400,1400,1400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/50/80/110/140","35","1400","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"IreliaGatotsu.png","sprite":"spell4.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IreliaHitenStyle","name":"Hiten Style","description":"Irelia is skilled in the art of Hiten, passively giving her physical attacks Health restoration. Activating Hiten Style doubles her Health restoration and gives her basic attacks true damage.","tooltip":"Passive: Basic attacks restore {{ e1 }} Health.

    Active: Irelia's basic attacks deal {{ e2 }} true damage and Hiten Style restores twice as much health for {{ e3 }} seconds.","leveltip":{"label":["Health Restoration","True Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[5,7,9,11,13],[15,30,45,60,75],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/7/9/11/13","15/30/45/60/75","6","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"IreliaHitenStyle.png","sprite":"spell4.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IreliaEquilibriumStrike","name":"Equilibrium Strike","description":"Irelia's attack balances the scales, dealing damage and slowing the target. However, if the target has a higher Health % than Irelia, then the blow stuns the target instead of slowing.","tooltip":"Irelia pierces her target, dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing them by {{ e3 }}% for {{ e2 }} second(s).

    If the target has a higher Health % than Irelia, she stuns them for the duration instead.","leveltip":{"label":["Damage","Disable Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[80,120,160,200,240],[1,1.25,1.5,1.75,2],[60,60,60,60,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","1/1.25/1.5/1.75/2","60","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"IreliaEquilibriumStrike.png","sprite":"spell4.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IreliaTranscendentBlades","name":"Transcendent Blades","description":"Irelia summons 4 spirit blades that she can fling to deal physical damage and siphon life from enemies they pass through.","tooltip":"Irelia summons 4 spirit blades that she can fling to deal {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) physical damage to enemies they pass through. She heals for {{ e3 }}% of the damage dealt against champions. ({{ e4 }}% vs. minions and monsters)","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,85,60],"cooldownBurn":"110/85/60","cost":[100,100,100],"costBurn":"100","effect":[null,[80,120,160],[10,10,10],[25,25,25],[10,10,10],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"80/120/160","10","25","10","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.7,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"IreliaTranscendentBlades.png","sprite":"spell4.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Ivern":{"id":427,"key":"Ivern","name":"Ivern","title":"the Green Father","spells":[{"id":"IvernQ","name":"Rootcaller","description":"Ivern conjures a vine, dealing damage and rooting enemy targets hit. Ivern's allies can dash to the rooted target.","tooltip":"Ivern conjures a vine dealing {{ e5 }} (+{{ f1 }}) magic damage and rooting the first enemy hit for {{ e1 }} second(s). Allies can click on rooted enemies to dash into attack range.","leveltip":{"label":["Root Duration","Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[1.2,1.4,1.6,1.8,2],[15,20,25,30,35],[26,23,20,17,14],[2,2,2,2,2],[80,125,170,215,260],[40,55,70,85,100],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.2/1.4/1.6/1.8/2","15/20/25/30/35","26/23/20/17/14","2","80/125/170/215/260","40/55/70/85/100","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"IvernQ.png","sprite":"spell4.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IvernW","name":"Brushmaker","description":"In brush, Ivern's attacks are ranged and deal bonus magic damage. Ivern can activate this ability to create a patch of brush.","tooltip":"Passive: In brush, Ivern's attacks are ranged and deal {{ e5 }} (+{{ f2 }}) bonus magic damage.

    Active: Ivern grows a patch of brush for {{ e1 }} seconds. For 3 seconds the area in and around the brush is revealed.","leveltip":{"label":["Bonus Magic Damage","Brush Recharge"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[30,30,30,30,30],[90,90,90,90,90],[190,190,190,190,190],[40,36,32,28,24],[20,30,40,50,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30","90","190","40/36/32/28/24","20/30/40/50/60","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"3","range":[1650,1650,1650,1650,1650],"rangeBurn":"1650","image":{"full":"IvernW.png","sprite":"spell4.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IvernE","name":"Triggerseed","description":"Ivern places a shield on an ally which explodes after a short duration slowing and damaging enemies.","tooltip":"Ivern shields an ally, absorbing up to {{ e1 }} (+{{ f3 }}) damage. After 2 seconds, the shield bursts dealing {{ e2 }} (+{{ f4 }}) magic damage and slowing enemies by {{ e3 }}% for {{ e4 }} seconds.","leveltip":{"label":["Shield","Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[70,100,130,160,190],[60,90,120,150,180],[40,45,50,55,60],[2,2,2,2,2],[1,1,1,1,1],[80,120,160,200,240],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","60/90/120/150/180","40/45/50/55/60","2","1","80/120/160/200/240","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"IvernE.png","sprite":"spell4.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IvernR","name":"Daisy!","description":"Ivern summons his Sentinel friend Daisy to fight with him. Daisy will send out a shockwave if she attacks the same champion three times in a row.","tooltip":"Ivern summons his sentinel friend Daisy. If Daisy attacks the same champion three times in a row, she will create a shockwave knocking enemies up for 1 second (3s cooldown). Daisy gains additional damage and defenses based on Ivern's Ability Power.

    Recasting this ability will direct Daisy to a new target or position.","leveltip":{"label":["Cooldown","Daisy Health","Daisy Resistances","Daisy Bonus Attack Speed"],"effect":["{{ e6 }} -> {{ e6NL }}","{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":3,"cooldown":[160,140,120],"cooldownBurn":"160/140/120","cost":[100,100,100],"costBurn":"100","effect":[null,[1250,2500,3750],[15,40,90],[30,50,70],[20,25,30],[150,250,350],[160,140,120],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1250/2500/3750","15/40/90","30/50/70","20/25/30","150/250/350","160/140/120","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"IvernR.png","sprite":"spell4.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Janna":{"id":40,"key":"Janna","name":"Janna","title":"the Storm's Fury","spells":[{"id":"HowlingGale","name":"Howling Gale","description":"By creating a localized change in pressure and temperature, Janna is able to create a small storm that grows in size with time. She can activate the spell again to release the storm. On release this storm will fly towards the direction it was cast in, dealing damage and knocking away any enemies in its path.","tooltip":"Summons a whirlwind, which deals {{ e1 }} (+{{ a1 }}) magic damage to enemies in its path and knocks them into the air for {{ e4 }} seconds.

    The whirlwind can be charged for up to {{ e6 }} seconds. For each second it charges, it deals {{ e2 }} (+{{ a2 }}) bonus damage, knocks up for an additional {{ e5 }} seconds, and travels {{ e3 }}% further.

    Activate again to release the whirlwind early.","leveltip":{"label":["Base Damage","Damage Per Second Charged","Mana Cost","Cooldown "],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ e2 }} -> {{ e2NL }} ","{{ cost }} -> {{ costNL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[90,105,120,135,150],"costBurn":"90/105/120/135/150","effect":[null,[60,85,110,135,160],[15,20,25,30,35],[35,35,35,35,35],[0.5,0.5,0.5,0.5,0.5],[0.25,0.25,0.25,0.25,0.25],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/85/110/135/160","15/20/25/30/35","35","0.5","0.25","3","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"},{"link":"spelldamage","coeff":0.1,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1700,1700,1700,1700,1700],"rangeBurn":"1700","image":{"full":"HowlingGale.png","sprite":"spell4.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SowTheWind","name":"Zephyr","description":"Janna summons an air elemental that passively increases her Movement Speed and enables her to pass through units. She may also activate this ability to deal damage and slow an enemy's Movement Speed. The passive is lost while this ability is on cooldown.","tooltip":"Passive: Increases Movement Speed by {{ e1 }}% (+{{ f1 }}%) and allows movement through units.

    Active: Deals {{ e2 }} (+{{ a1 }}) magic damage to an enemy and slows their Movement Speed by {{ e3 }}% (+{{ f2 }}%) for {{ e4 }} seconds. Passive benefit is lost while Zephyr is on cooldown.","leveltip":{"label":["Passive Movement Speed","Damage","Slow %","Mana Cost"],"effect":["{{ e1 }}% -> {{ e1NL }}% ","{{ e2 }} -> {{ e2NL }} ","{{ e3 }}% -> {{ e3NL }}% ","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[40,50,60,70,80],"costBurn":"40/50/60/70/80","effect":[null,[9,11,13,15,17],[60,115,170,225,280],[24,28,32,36,40],[3,3,3,3,3],[0.0002,0.0002,0.0002,0.0002,0.0002],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"9/11/13/15/17","60/115/170/225/280","24/28/32/36/40","3","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.02,"key":"f1"},{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.06,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"SowTheWind.png","sprite":"spell4.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"EyeOfTheStorm","name":"Eye Of The Storm","description":"Janna conjures a defensive gale that shields an ally champion or turret from incoming damage and increases their Attack Damage.","tooltip":"Shields an allied Champion or turret for {{ e3 }} seconds. The shield absorbs up to {{ e1 }} (+{{ a1 }}) damage and grants {{ e2 }} (+{{ a2 }}) Attack Damage until it breaks.","leveltip":{"label":["Shield Health","Attack Damage Bonus","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[80,120,160,200,240],[10,17.5,25,32.5,40],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","10/17.5/25/32.5/40","5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"spelldamage","coeff":0.1,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"EyeOfTheStorm.png","sprite":"spell4.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ReapTheWhirlwind","name":"Monsoon","description":"Janna surrounds herself in a magical storm, throwing enemies back. After the storm has settled, soothing winds heal nearby allies while the ability is active.","tooltip":"Summons forth the might of the wind to knock surrounding enemies back and restores {{ e1 }} (+{{ a1 }}) Health to nearby allies each second for {{ e3 }} seconds.","leveltip":{"label":["Heal Per Second","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,135,120],"cooldownBurn":"150/135/120","cost":[100,100,100],"costBurn":"100","effect":[null,[100,150,200],[300,450,600],[3,3,3],[700,700,700],[875,875,875],[875,875,875],[1200,1200,1200],[10,10,10],[0.5,0.5,0.5],[0,0,0]],"effectBurn":[null,"100/150/200","300/450/600","3","700","875","875","1200","10","0.5","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[725,725,725],"rangeBurn":"725","image":{"full":"ReapTheWhirlwind.png","sprite":"spell4.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"JarvanIV":{"id":59,"key":"JarvanIV","name":"Jarvan IV","title":"the Exemplar of Demacia","spells":[{"id":"JarvanIVDragonStrike","name":"Dragon Strike","description":"Jarvan IV extends his lance, dealing physical damage and lowering the Armor of enemies in its path. Additionally, this will pull Jarvan to his Demacian Standard, knocking up enemies in his path.","tooltip":"Extends Jarvan IV's lance dealing {{ e1 }} (+{{ a1 }}) physical damage and lowering the Armor of enemies hit by {{ e2 }}% for {{ e3 }} seconds.

    If the lance contacts Demacian Standard it will pull Jarvan IV to its location, knocking up enemies in his path. This effect can be triggered even while immobilized.","leveltip":{"label":["Damage","Armor Reduction","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[70,115,160,205,250],[10,14,18,22,26],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","10/14/18/22/26","3","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[770,770,770,770,770],"rangeBurn":"770","image":{"full":"JarvanIVDragonStrike.png","sprite":"spell4.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JarvanIVGoldenAegis","name":"Golden Aegis","description":"Jarvan IV calls upon the ancient kings of Demacia to shield him from harm and slow surrounding enemies.","tooltip":"Grants a shield that absorbs {{ e1 }} (+{{ f1 }} [{{ e6 }}% max Health] for each nearby enemy champion) damage for {{ e4 }} seconds, and slows surrounding enemies by {{ e2 }}% for {{ e5 }} seconds.","leveltip":{"label":["Shield","Bonus Shield Percent Health","Slow Amount"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e6 }}% -> {{ e6NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[65,90,115,140,165],[15,20,25,30,35],[10,20,30,40,50],[5,5,5,5,5],[2,2,2,2,2],[2,2.5,3,3.5,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/90/115/140/165","15/20/25/30/35","10/20/30/40/50","5","2","2/2.5/3/3.5/4","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"JarvanIVGoldenAegis.png","sprite":"spell4.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JarvanIVDemacianStandard","name":"Demacian Standard","description":"Jarvan IV carries the pride of Demacia, passively granting him bonus Attack Speed. Activating Demacian Standard allows Jarvan IV to place a Demacian flag that deals magic damage on impact and grants Attack Speed to nearby allied champions.","tooltip":"Passive: Gains {{ e3 }}% Attack Speed.

    Active: Throws a Demacian Standard to a nearby area dealing {{ e2 }} (+{{ a1 }}) magic damage to enemies. The Standard lasts for {{ e4 }} seconds and grants surrounding allied champions {{ e3 }}% Attack Speed.","leveltip":{"label":["Damage","Passive Attack Speed","Active Attack Speed","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}"," {{ e3 }}% -> {{ e3NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12.5,12,11.5,11],"cooldownBurn":"13/12.5/12/11.5/11","cost":[55,55,55,55,55],"costBurn":"55","effect":[null,[10,13,16,19,22],[60,105,150,195,240],[15,17.5,20,22.5,25],[8,8,8,8,8],[0.15,0.025,0.025,0.025,0.025],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/13/16/19/22","60/105/150/195/240","15/17.5/20/22.5/25","8","0.15/0.025/0.025/0.025/0.025","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[860,860,860,860,860],"rangeBurn":"860","image":{"full":"JarvanIVDemacianStandard.png","sprite":"spell4.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JarvanIVCataclysm","name":"Cataclysm","description":"Jarvan IV heroically leaps into battle at a target with such force that he terraforms the surrounding area to create an arena around them. Nearby enemies are damaged at the moment of impact.","tooltip":"Heroically leaps to an enemy Champion dealing {{ e1 }} (+{{ a1 }}) physical damage to nearby enemies and creating an arena of impassable terrain around them for {{ e2 }} seconds.

    Activate again to collapse the terrain.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[100,100,100],"costBurn":"100","effect":[null,[200,325,450],[3.5,3.5,3.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"200/325/450","3.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"JarvanIVCataclysm.png","sprite":"spell4.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Jax":{"id":24,"key":"Jax","name":"Jax","title":"Grandmaster at Arms","spells":[{"id":"JaxLeapStrike","name":"Leap Strike","description":"Jax leaps toward a unit. If they are an enemy, he strikes them with his weapon.","tooltip":"Jax leaps to target unit, dealing {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) physical damage if it is an enemy.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[70,110,150,190,230],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"f1"},{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"JaxLeapStrike.png","sprite":"spell4.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JaxEmpowerTwo","name":"Empower","description":"Jax charges his weapon with energy, causing his next attack to deal additional damage.","tooltip":"Jax charges his weapon with energy, causing his next basic attack or Leap Strike to deal an additional {{ e1 }} (+{{ a1 }}) Magic Damage.","leveltip":{"label":["Damage","Cooldown"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[7,6,5,4,3],"cooldownBurn":"7/6/5/4/3","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[40,75,110,145,180],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/75/110/145/180","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"JaxEmpowerTwo.png","sprite":"spell4.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JaxCounterStrike","name":"Counter Strike","description":"Jax's combat prowess allows him to dodge all incoming attacks for a short duration and then quickly counterattack, stunning all surrounding enemies.","tooltip":"Jax enters a defensive stance for up to {{ e6 }} seconds, dodging all incoming basic attacks and taking {{ e3 }}% less damage from area of effect abilities.

    After 2 seconds or if activated again, Jax stuns surrounding enemies for {{ e2 }} second and deals {{ e1 }} (+{{ f2 }}) physical damage to them.

    Counter Strike deals {{ e5 }}% more damage for each attack Jax dodged (max: {{ e4 }}% increased damage).","leveltip":{"label":["Cooldown","Damage","Mana Cost"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }} "]},"maxrank":5,"cooldown":[16,14,12,10,8],"cooldownBurn":"16/14/12/10/8","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[50,75,100,125,150],[1,1,1,1,1],[25,25,25,25,25],[100,100,100,100,100],[20,20,20,20,20],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","1","25","100","20","2","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"JaxCounterStrike.png","sprite":"spell4.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JaxRelentlessAssault","name":"Grandmaster's Might","description":"Every third consecutive attack deals additional Magic Damage. Additionally, Jax can activate this ability to strengthen his resolve, increasing his Armor and Magic Resist for a short duration.","tooltip":"Passive: Every third consecutive strike Jax deals {{ e1 }} (+{{ a1 }}) additional Magic Damage.

    Active: Jax strengthens his resolve, granting him {{ f2 }} Armor and {{ f1 }} Magic Resist for {{ e5 }} seconds.

    Armor bonus is equal to {{ e3 }} + {{ e6 }}% bonus Attack Damage.

    Magic Resist bonus is equal to {{ e3 }} + {{ e7 }}% Ability Power.
    ","leveltip":{"label":["Passive Damage","Armor Bonus","Magic Resist Bonus"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e3 }} -> {{ e3NL }}"]},"maxrank":3,"cooldown":[80,80,80],"cooldownBurn":"80","cost":[100,100,100],"costBurn":"100","effect":[null,[100,160,220],[3,3,3],[30,50,70],[6,8,10],[8,8,8],[50,50,50],[20,20,20],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/160/220","3","30/50/70","6/8/10","8","50","20","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"@special.jaxrarmor","coeff":0.3,"key":"f2"},{"link":"@special.jaxrmr","coeff":0.2,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[100,100,100],"rangeBurn":"100","image":{"full":"JaxRelentlessAssault.png","sprite":"spell4.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Jayce":{"id":126,"key":"Jayce","name":"Jayce","title":"the Defender of Tomorrow","spells":[{"id":"JayceToTheSkies","name":"To the Skies! / Shock Blast","description":"Hammer Stance: Leaps to an enemy dealing physical damage and slowing enemies.

    Cannon Stance: Fires an orb of electricity that detonates upon hitting an enemy (or reaching the end of its path) dealing physical damage to all enemies hit.","tooltip":"Leaps to an enemy dealing {{ e1 }} (+{{ a1 }}) physical damage to nearby enemies and slowing them by {{ e3 }}% for {{ e5 }} seconds.","leveltip":{"label":["Damage","Cooldown","Slow"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":6,"cooldown":[16,14,12,10,8,6],"cooldownBurn":"16/14/12/10/8/6","cost":[40,40,40,40,40,40],"costBurn":"40","effect":[null,[35,70,105,140,175,210],[1.5,1.5,1.5,1.5,1.5,2],[30,35,40,45,50,55],[20,40,60,80,0,0],[2,2,2,2,2,2],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]],"effectBurn":[null,"35/70/105/140/175/210","1.5/1.5/1.5/1.5/1.5/2","30/35/40/45/50/55","20/40/60/80/0/0","2","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600,600],"rangeBurn":"600","image":{"full":"JayceToTheSkies.png","sprite":"spell4.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JayceStaticField","name":"Lightning Field / Hyper Charge","description":"Hammer Stance: Passive: Restores Mana per strike. Active: Creates a field of lightning damaging nearby enemies for several seconds.

    Cannon Stance: Gains a burst of energy, increasing Attack Speed to maximum for several attacks.","tooltip":"Passive: Gains {{ e5 }} Mana per strike while in Hammer Stance.

    Active: Creates an electric aura dealing {{ e4 }} (+{{ a1 }}) magic damage over {{ e2 }} seconds to nearby enemies.","leveltip":{"label":["Damage","Mana Return"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e5 }} -> {{ e5NL }}"]},"maxrank":6,"cooldown":[10,10,10,10,10,10],"cooldownBurn":"10","cost":[40,40,40,40,40,40],"costBurn":"40","effect":[null,[25,35,45,55,65,75],[4,4,4,4,4,4],[25,30,35,40,45,50],[100,160,220,280,340,400],[6,8,10,12,14,16],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]],"effectBurn":[null,"25/35/45/55/65/75","4","25/30/35/40/45/50","100/160/220/280/340/400","6/8/10/12/14/16","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[285,285,285,285,285,285],"rangeBurn":"285","image":{"full":"JayceStaticField.png","sprite":"spell4.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JayceThunderingBlow","name":"Thundering Blow / Acceleration Gate","description":"Hammer Stance: Deals magic damage to an enemy and knocks them back a short distance.

    Cannon Stance: Deploys an Acceleration Gate increasing the Movement Speed of all allied champions who pass through it. If Shock Blast is fired through the gate the missile speed, range, and damage will increase.","tooltip":"Deals {{ e4 }}% of the target's maximum health (+{{ a1 }}) as magic damage and knocks them back a short distance. ({{ e1 }} Maximum Damage against monsters)","leveltip":{"label":["Maximum Health %","Cooldown","Maximum Damage against monsters"],"effect":["{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":6,"cooldown":[15,14,13,12,11,10],"cooldownBurn":"15/14/13/12/11/10","cost":[55,55,55,55,55,55],"costBurn":"55","effect":[null,[200,300,400,500,600,700],[0.25,0.25,0.25,0.25,0.25,0.25],[10,20,30,40,50,60],[8,10.4,12.8,15.2,17.6,20],[40,70,100,130,160,190],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]],"effectBurn":[null,"200/300/400/500/600/700","0.25","10/20/30/40/50/60","8/10.4/12.8/15.2/17.6/20","40/70/100/130/160/190","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[240,240,240,240,240,240],"rangeBurn":"240","image":{"full":"JayceThunderingBlow.png","sprite":"spell4.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JayceStanceHtG","name":"Mercury Cannon / Mercury Hammer","description":"Hammer Stance: Transforms the Mercury Hammer into the Mercury Cannon gaining new abilities and increased range. The first attack in this form reduces the target's Armor and Magic Resist.

    Cannon Stance: Transforms the Mercury Cannon into the Mercury Hammer gaining new abilities and increasing Armor and Magic Resist. The first attack in this form deals additional magic damage.","tooltip":"Active: Transforms the Mercury Hammer into the Mercury Cannon gaining new abilities and ranged attacks.

    The next attack in Cannon Stance reduces the target's Armor and Magic Resist by {{ f3 }}% for {{ e3 }} seconds.","maxrank":1,"cooldown":[6],"cooldownBurn":"6","cost":[0],"costBurn":"0","effect":[null,[5],[10],[5],[5],[20],[0],[0],[0],[0],[0]],"effectBurn":[null,"5","10","5","5","20","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[600],"rangeBurn":"600","image":{"full":"JayceStanceHtG.png","sprite":"spell4.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Jhin":{"id":202,"key":"Jhin","name":"Jhin","title":"the Virtuoso","spells":[{"id":"JhinQ","name":"Dancing Grenade","description":"Jhin launches a magical cartridge at an enemy. It can hit up to four targets and gains damage each time it kills.","tooltip":"Jhin launches a cartridge at the targeted enemy that deals {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) physical damage before bouncing to a nearby target that has not yet been hit.

    The cartridge can hit a maximum of 4 times. Each kill by the cartridge increases the damage of subsequent hits by {{ e2 }}%.","leveltip":{"label":["Damage","Total AD Ratio","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[50,75,100,125,150],[35,35,35,35,35],[0.3,0.35,0.4,0.45,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","35","0.3/0.35/0.4/0.45/0.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"JhinQ.png","sprite":"spell4.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JhinW","name":"Deadly Flourish","description":"Jhin brandishes his cane, firing a single shot with incredible range. It pierces through minions and monsters, but stops on the first champion hit. If the target was recently struck by Jhin's allies, lotus traps, or basic attacks, they are rooted.","tooltip":"Jhin fires a long range shot that stops on the first champion hit, dealing {{ e1 }} (+{{ a1 }}) physical damage to it, and {{ e4 }}% of that damage to minions and monsters hit along the way.

    If Deadly Flourish strikes a champion that has been hit by one of Jhin's basic attacks, Lotus Traps, or allies within the last 4 seconds it will root them for {{ e2 }} seconds and grants Jhin movement speed as though he had crit them.","leveltip":{"label":["Damage","Root Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[14,14,14,14,14],"cooldownBurn":"14","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[50,85,120,155,190],[0.75,1,1.25,1.5,1.75],[0,0,0,0,0],[75,75,75,75,75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/85/120/155/190","0.75/1/1.25/1.5/1.75","0","75","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[3000,3000,3000,3000,3000],"rangeBurn":"3000","image":{"full":"JhinW.png","sprite":"spell4.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JhinE","name":"Captive Audience","description":"Jhin places an invisible lotus trap that blooms when walked over. It slows nearby enemies before dealing damage with an explosion of serrated petals.

    Beauty in Death - When Jhin kills an enemy champion, a lotus trap will bloom near their corpse.","tooltip":"Jhin places an invisible Lotus Trap that lasts {{ e5 }} minutes and activates when stepped on, revealing nearby enemies for 4 seconds.

    The trap leaves behind a zone that slows enemies inside by {{ e7 }}% and detonates after 2 seconds, dealing {{ e2 }} (+{{ a1 }}) (+{{ a2 }}) magic damage ({{ e1 }}% damage on subsequent hits and vs. minions and monsters).

    Jhin prepares a new trap every {{ f1 }} seconds and can store 2 at once.

    Beauty in Death - When Jhin kills an enemy champion a Lotus Trap will spawn and detonate where they were killed.","leveltip":{"label":["Damage","Recharge Time","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ f1 }} -> {{ f2 }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[2,2,2,2,2],"cooldownBurn":"2","cost":[30,35,40,45,50],"costBurn":"30/35/40/45/50","effect":[null,[65,65,65,65,65],[20,80,140,200,260],[0,0,0,0,0],[0.75,0.75,0.75,0.75,0.75],[2,2,2,2,2],[0,0,0,0,0],[35,35,35,35,35],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65","20/80/140/200/260","0","0.75","2","0","35","1","0","0"],"vars":[{"link":"attackdamage","coeff":1.2,"key":"a1"},{"link":"spelldamage","coeff":1,"key":"a2"}],"costType":" Mana","maxammo":"2","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"JhinE.png","sprite":"spell4.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JhinR","name":"Curtain Call","description":"Jhin channels, transforming Whisper into a shoulder-mounted mega-cannon. It is able to fire 4 super shots at extreme range that pierce through minions and monsters, but stop on the first champion impacted. Whisper cripples enemies hit, which slows them and deals execute damage. The 4th shot is perfectly crafted, epically powerful, and guaranteed to critically strike.","tooltip":"Jhin sets up and channels, enabling him to fire 4 super shots at extreme range in a cone in front of him. The shots stop on the first champion hit, slowing it by {{ e3 }}% for {{ e4 }} seconds and dealing {{ e1 }} (+{{ a1 }}) physical damage, increased by {{ e5 }}% for each 1% health the target is missing (up to {{ f2 }} (+{{ f3 }})). The 4th shot crits for {{ f1 }}% damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[100,100,100],"costBurn":"100","effect":[null,[40,100,160],[200,200,200],[80,80,80],[0.5,0.5,0.5],[2.5,2.5,2.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"40/100/160","200","80","0.5","2.5","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"JhinR.png","sprite":"spell5.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Jinx":{"id":222,"key":"Jinx","name":"Jinx","title":"the Loose Cannon","spells":[{"id":"JinxQ","name":"Switcheroo!","description":"Jinx modifies her basic attacks by swapping between Pow-Pow, her minigun and Fishbones, her rocket launcher. Attacks with Pow-Pow grant Attack Speed, while attacks with Fishbones deal area of effect damage, gain increased range, and drain Mana.","tooltip":"Jinx swaps weapons.

    Fishbones, the Rocket Launcher: Basic attacks deal 110% Damage to the target and nearby enemies, gain {{ e3 }} range, drain Mana, and scale {{ e5 }}% less with bonus Attack Speed.

    Pow-Pow, the Minigun: Basic attacks grant bonus Attack Speed for 2.5 seconds. This effect stacks up to 3 times for a total bonus of {{ f4 }}% (bonus scales with spell rank and Jinx's level). Stacks fall off one at a time and only benefit Jinx's first attack after switching to Rocket Launcher.","leveltip":{"label":["Rocket Bonus Range","Minigun Total Attack Speed"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ f4 }}% -> {{ f5 }}%"]},"maxrank":5,"cooldown":[1,1,1,1,1],"cooldownBurn":"1","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,40,50,60,70],[100,100,100,100,100],[75,100,125,150,175],[20,20,20,20,20],[25,25,25,25,25],[33.25,33.25,33.25,33.25,33.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/40/50/60/70","100","75/100/125/150/175","20","25","33.25","0","0","0","0"],"vars":[],"costType":" Mana per Rocket","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"JinxQ.png","sprite":"spell5.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ e4 }} Mana per Rocket"},{"id":"JinxW","name":"Zap!","description":"Jinx uses Zapper, her shock pistol, to fire a blast that deals damage to the first enemy hit, slowing and revealing it.","tooltip":"Jinx fires a shock blast that deals {{ e1 }} (+{{ a1 }}) Physical Damage to the first enemy hit, revealing it and slowing it by {{ e2 }}% for 2 seconds.","leveltip":{"label":["Damage","Slow","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[10,60,110,160,210],[30,40,50,60,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/60/110/160/210","30/40/50/60/70","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1450,1450,1450,1450,1450],"rangeBurn":"1450","image":{"full":"JinxW.png","sprite":"spell5.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JinxE","name":"Flame Chompers!","description":"Jinx throws out a line of snare grenades that explode after 5 seconds, lighting enemies on fire. Flame Chompers will bite enemy champions who walk over them, rooting them in place.","tooltip":"Jinx tosses out 3 chompers that, once armed, explode on contact with enemy champions, rooting the target for 1.5 seconds and dealing {{ e1 }} (+{{ a1 }}) Magic Damage over 1.5 seconds to nearby enemies. Chompers last for 5 seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[24,20.5,17,13.5,10],"cooldownBurn":"24/20.5/17/13.5/10","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[70,120,170,220,270],[100,140,180,220,260],[30,35,40,45,50],[35,50,65,80,95],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/120/170/220/270","100/140/180/220/260","30/35/40/45/50","35/50/65/80/95","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"JinxE.png","sprite":"spell5.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JinxR","name":"Super Mega Death Rocket!","description":"Jinx fires a super rocket across the map that gains damage as it travels. The rocket will explode upon colliding with an enemy champion, dealing damage to it and surrounding enemies based on their missing Health. ","tooltip":"Jinx fires a rocket that gains damage over the first second it travels. It explodes on the first enemy champion hit, dealing {{ e1 }} (+{{ a1 }}) to {{ e2 }} (+{{ a2 }}) Physical Damage plus {{ e3 }}% of the target's missing Health. Nearby enemies take 80% Damage.","leveltip":{"label":["Minimum Damage","Maximum Damage","Percent Missing Health Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[90,75,60],"cooldownBurn":"90/75/60","cost":[100,100,100],"costBurn":"100","effect":[null,[25,35,45],[250,350,450],[25,30,35],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"25/35/45","250/350/450","25/30/35","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.15,"key":"a1"},{"link":"bonusattackdamage","coeff":1.5,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"JinxR.png","sprite":"spell5.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Kalista":{"id":429,"key":"Kalista","name":"Kalista","title":"the Spear of Vengeance","spells":[{"id":"KalistaMysticShot","name":"Pierce","description":"Throw a fast moving spear that passes through enemies it kills.","tooltip":"Hurl a fast but narrow spear that deals {{ e1 }} (+{{ a1 }}) physical damage. Triggers Martial Poise (P), Sentinel (W), and Rend (E).

    If it kills a target, Pierce continues onward, passing all stacks of Rend to the next target.","leveltip":{"label":["Mana Cost","Damage"],"effect":["{{ cost }} -> {{ costNL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[10,70,130,190,250],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/70/130/190/250","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1150,1150,1150,1150,1150],"rangeBurn":"1150","image":{"full":"KalistaMysticShot.png","sprite":"spell5.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KalistaW","name":"Sentinel","description":"Passively gain Attack Speed when near her Oathsworn ally, and deal bonus damage when striking the same target.

    Activate to send a soul to scout out the path, revealing the area in front of it.","tooltip":"Passive: When Kalista is near her Oathsworn, she gains +{{ f1 }}% Attack Speed. If they both basic attack the same target, she deals {{ e2 }}% of their max health as magic damage. {{ e7 }} second cooldown per target.

    Active: Send a soul Sentinel to patrol an unseen area. Champions spotted are revealed for 4 seconds. Sentinels last 3 laps.

    Kalista gains a charge of Sentinel every {{ e4 }} seconds.","leveltip":{"label":["Damage","Ammo Recharge"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[30,30,30,30,30],"cooldownBurn":"30","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[80,80,80,80,80],[5,7.5,10,12.5,15],[125,150,175,200,225],[90,80,70,60,50],[0,0,0,0,0],[75,100,125,150,175],[10,10,10,10,10],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80","5/7.5/10/12.5/15","125/150/175/200/225","90/80/70/60/50","0","75/100/125/150/175","10","0","0","0"],"vars":[],"costType":" Mana","maxammo":"2","range":[5000,5000,5000,5000,5000],"rangeBurn":"5000","image":{"full":"KalistaW.png","sprite":"spell5.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KalistaExpungeWrapper","name":"Rend","description":"Attacks impale their targets with spears. Activate to rip the spears out, slowing and dealing escalating damage.","tooltip":"Passive: Kalista's spears pierce their target, lingering for 4 seconds.

    Active: Rip the spears from nearby targets, dealing {{ e1 }} (+{{ a1 }}) physical damage and slowing their Movement Speed by {{ e2 }}% for {{ e6 }} seconds.


    If Rend kills at least one target, its cooldown is reset and its mana cost is refunded.","leveltip":{"label":["Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ e5 }} -> {{ e5NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[20,30,40,50,60],[25,30,35,40,45],[30,30,30,30,30],[33,37.5,41.667,45.833,50],[14,12.5,11,9.5,8],[2,2,2,2,2],[254,254,254,254,254],[10,10,10,10,10],[10,14,19,25,32],[0,0,0,0,0]],"effectBurn":[null,"20/30/40/50/60","25/30/35/40/45","30","33/37.5/41.667/45.833/50","14/12.5/11/9.5/8","2","254","10","10/14/19/25/32","0"],"vars":[{"link":"attackdamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"KalistaExpungeWrapper.png","sprite":"spell5.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KalistaRx","name":"Fate's Call","description":"Kalista teleports the Oathsworn ally to herself. They gain the ability to dash toward a position, knocking enemy champions back.","tooltip":"Draw Kalista's Oathsworn to her. For up to 4 seconds, the Oathsworn is untargetable and pacified.

    The Oathsworn may mouse click to fly toward target position, stopping at the first enemy champion hit and knocking all enemies in a small radius back.

    Kalista's Oathsworn must be within 1100 units for her to cast this ability.","leveltip":{"label":["Cooldown","Knock Up Duration"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":3,"cooldown":[120,90,60],"cooldownBurn":"120/90/60","cost":[100,100,100],"costBurn":"100","effect":[null,[0,0,0],[1.5,1.75,2],[40,60,80],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"0","1.5/1.75/2","40/60/80","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"KalistaRx.png","sprite":"spell5.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Karma":{"id":43,"key":"Karma","name":"Karma","title":"the Enlightened One","spells":[{"id":"KarmaQ","name":"Inner Flame","description":"Karma sends forth a ball of spirit energy that explodes and deals damage upon hitting an enemy unit.

    Mantra Bonus: In addition to the explosion, Mantra increases the destructive power of her Inner Flame, creating a cataclysm which deals damage after a short delay.","tooltip":"Fires a blast of energy that explodes upon enemy contact, dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing Movement Speed by {{ e2 }}% for {{ e3 }} seconds.

    Mantra Bonus - Soulflare: Deals {{ f1 }} (+{{ a2 }}) additional magic damage and leaves a circle of flame that slows enemies by {{ f3 }}%. After 1.5 seconds the circle erupts, dealing {{ f2 }} (+{{ a1 }}) magic damage to enemies in the area.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[80,125,170,215,260],[25,25,25,25,25],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/125/170/215/260","25","1.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"},{"link":"@text","coeff":[25,75,125,175],"key":"f1"},{"link":"spelldamage","coeff":0.3,"key":"a2"},{"link":"@text","coeff":[50,150,250,350],"key":"f2"},{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"KarmaQ.png","sprite":"spell5.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KarmaSpiritBind","name":"Focused Resolve","description":"Karma creates a tether between herself and a targeted enemy, dealing damage and revealing them. If the tether is not broken, the enemy will be rooted and damaged again.

    Mantra Bonus: Karma strengthens the link, healing herself and extending the root duration.","tooltip":"Links Karma to an enemy champion or monster, granting True Sight and dealing {{ effect1amount*0.5 }} (+{{ f4 }}) magic damage. If the link is unbroken for {{ e3 }} seconds, the target is rooted for {{ e2 }} second(s) and takes an additional {{ effect1amount*0.5 }} (+{{ f4 }}) magic damage.

    Mantra Bonus - Renewal: Karma is healed for {{ f1 }}% (+{{ f3 }}%) of her missing health. If the link is not broken or the target dies, the root duration is increased by {{ f2 }} second(s) and Karma is healed for {{ f1 }}% (+{{ f3 }}%) of her missing health.","leveltip":{"label":["Damage","Root Duration","Mana Cost"],"effect":["{{ effect1amount*0.5 }} -> {{ effect1amountnl*0.5 }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[60,110,160,210,260],[1,1.25,1.5,1.75,2],[2,2,2,2,2],[2,2,2,2,2],[825,825,825,825,825],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/110/160/210/260","1/1.25/1.5/1.75/2","2","2","825","0","0","0","0","0"],"vars":[{"link":"@text","coeff":[75,150,225,300],"key":"f1"},{"link":"@text","coeff":[75,150,225,300],"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[675,675,675,675,675],"rangeBurn":"675","image":{"full":"KarmaSpiritBind.png","sprite":"spell5.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KarmaSolKimShield","name":"Inspire","description":"Karma summons a protective shield that absorbs incoming damage and increases the Movement Speed of the protected ally.

    Mantra Bonus: Energy radiates out from her target, strengthening the initial shield and applying Inspire to nearby allied champions.","tooltip":"Target ally gains a shield, granting {{ e3 }}% Movement Speed for {{ e2 }} seconds and absorbing {{ e1 }} (+{{ a1 }}) damage for {{ e4 }} seconds.

    Mantra Bonus - Defiance: The shield overflows with energy, absorbing an additional {{ f1 }} (+{{ a2 }}) damage. Nearby allied champions gain shields that absorb {{ f2 }}% as much as the initial target's shield. All champions shielded gain {{ f3 }}% movement speed for {{ e2 }} seconds.","leveltip":{"label":["Damage Absorption","Movement Speed","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9.5,9,8.5,8],"cooldownBurn":"10/9.5/9/8.5/8","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[70,100,130,160,190],[1.5,1.5,1.5,1.5,1.5],[40,45,50,55,60],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","1.5","40/45/50/55/60","4","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"@text","coeff":[60,140,220,300],"key":"f1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"KarmaSolKimShield.png","sprite":"spell5.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KarmaMantra","name":"Mantra","description":"Karma empowers her next ability to do an additional effect. Mantra is available at level 1 and does not require a skill point.","tooltip":"Karma empowers her next ability within 8 seconds for an additional effect.

    Soulflare: Deals bonus damage and leaves behind a circle of flame, slowing enemies and dealing additional damage.

    Renewal: Karma heals for a portion of her missing health. If the link is unbroken the root is longer and Karma is healed again.

    Defiance: The shield is stronger and allied champions around the target also gain a shield and movement speed.","leveltip":{"label":["Soulflare Impact","Soulflare Circle Damage","Renewal Root Extension","Defiance Shield","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e5 }} -> {{ e5NL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":4,"cooldown":[45,42,39,36],"cooldownBurn":"45/42/39/36","cost":[0,0,0,0],"costBurn":"0","effect":[null,[25,75,125,175],[50,150,250,350],[0.5,0.5,0.5,0.5],[1.5,1.5,1.5,1.5],[0.5,0.75,1,1.25],[0.2,0.2,0.2,0.2],[30,90,150,210],[0.6,0.6,0.6,0.6],[0.5,0.5,0.5,0.5],[0.01,0.01,0.01,0.01]],"effectBurn":[null,"25/75/125/175","50/150/250/350","0.5","1.5","0.5/0.75/1/1.25","0.2","30/90/150/210","0.6","0.5","0.01"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"KarmaMantra.png","sprite":"spell5.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Karthus":{"id":30,"key":"Karthus","name":"Karthus","title":"the Deathsinger","spells":[{"id":"KarthusLayWasteA1","name":"Lay Waste","description":"Karthus unleashes a delayed blast at a location, dealing damage to nearby enemies.","tooltip":"Creates a delayed blast at Karthus' cursor position dealing {{ e1 }} (+{{ a1 }}) magic damage to each nearby enemy.

    If the blast hits only a single unit it deals double damage.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[20,26,32,38,44],"costBurn":"20/26/32/38/44","effect":[null,[40,60,80,100,120],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/60/80/100/120","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"2","range":[875,875,875,875,875],"rangeBurn":"875","image":{"full":"KarthusLayWasteA1.png","sprite":"spell5.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KarthusWallOfPain","name":"Wall of Pain","description":"Karthus creates a passable screen of leeching energy. Any enemy units that walk through the screen have their Movement Speed and Magic Resist reduced for a period.","tooltip":"Creates a wall that lasts for {{ e4 }} seconds. Enemies that pass through the wall have their Magic Resist reduced by {{ e1 }}% and Movement Speed reduced by {{ e3 }}% for {{ e5 }} seconds (their Movement Speed gradually recovers over the duration).","leveltip":{"label":["Wall Width","Movement Speed Slow"],"effect":["{{ e2 }} -> {{ e2NL }}"," {{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[18,18,18,18,18],"cooldownBurn":"18","cost":[100,100,100,100,100],"costBurn":"100","effect":[null,[15,15,15,15,15],[800,900,1000,1100,1200],[40,50,60,70,80],[5,5,5,5,5],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15","800/900/1000/1100/1200","40/50/60/70/80","5","5","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"KarthusWallOfPain.png","sprite":"spell5.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KarthusDefile","name":"Defile","description":"Karthus passively steals energy from his victims, gaining Mana on each kill. Alternatively, Karthus can surround himself in the souls of his prey, dealing damage to nearby enemies, but quickly draining his own Mana. ","tooltip":"Toggle Off: When Karthus kills a unit, he restores {{ e2 }} Mana.

    Toggle On: Drains {{ cost }} Mana to deal {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies each second. ","leveltip":{"label":["Damage per second","Mana Restore","Mana Cost per second"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[30,42,54,66,78],"costBurn":"30/42/54/66/78","effect":[null,[30,50,70,90,110],[20,27,34,41,48],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","20/27/34/41/48","0.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana Per Second","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"KarthusDefile.png","sprite":"spell5.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana Per Second"},{"id":"KarthusFallenOne","name":"Requiem","description":"After channeling for 3 seconds, Karthus deals damage to all enemy champions.","tooltip":"After channeling for 3 seconds, Karthus deals {{ e1 }} (+{{ a1 }}) magic damage to all enemy champions (regardless of distance). ","leveltip":{"label":["Damage","Mana cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[200,180,160],"cooldownBurn":"200/180/160","cost":[150,175,200],"costBurn":"150/175/200","effect":[null,[250,400,550],[200,300,400],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250/400/550","200/300/400","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[10000,10000,10000],"rangeBurn":"10000","image":{"full":"KarthusFallenOne.png","sprite":"spell5.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Kassadin":{"id":38,"key":"Kassadin","name":"Kassadin","title":"the Void Walker","spells":[{"id":"NullLance","name":"Null Sphere","description":"Kassadin fires an orb of void energy at a target, dealing damage and interrupting channels. The excess energy forms around himself, granting a temporary shield that absorbs magic damage.","tooltip":"Kassadin fires an orb of void energy that deals {{ e1 }} (+{{ a1 }}) magic damage and interrupts channels.

    The excess energy forms around himself, granting a shield that absorbs {{ e3 }} (+{{ a2 }}) magic damage for {{ e4 }} seconds.","leveltip":{"label":["Damage","Shield","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[65,95,125,155,185],[0,0,0,0,0],[40,70,100,130,160],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/95/125/155/185","0","40/70/100/130/160","1.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"NullLance.png","sprite":"spell5.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NetherBlade","name":"Nether Blade","description":"Passive: Kassadin's basic attacks deal bonus magic damage. Active: Kassadin's next basic attack deals significant bonus magic damage and restores Mana.","tooltip":"Passive: Kassadin's basic attacks draw energy from the void, dealing {{ e2 }} (+{{ a1 }}) bonus magic damage.

    Active: Kassadin charges his Nether Blade, causing his next basic attack to deal {{ e3 }} (+{{ a2 }}) bonus magic damage and restore {{ e1 }}% of his missing Mana (increases to {{ e4 }}% against champions).","leveltip":{"label":["Active Damage","Base Mana Restore","Champion Mana Restore"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[4,5,6,7,8],[20,20,20,20,20],[40,65,90,115,140],[20,25,30,35,40],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/5/6/7/8","20","40/65/90/115/140","20/25/30/35/40","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.1,"key":"a1"},{"link":"spelldamage","coeff":0.7,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"NetherBlade.png","sprite":"spell5.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"ForcePulse","name":"Force Pulse","description":"Kassadin draws energy from spells cast in his vicinity. Upon charging up, Kassadin can use Force Pulse to damage and slow enemies in a cone in front of him.","tooltip":"Kassadin draws energy from spells cast in his vicinity, gaining a charge whenever a spell is cast near him.

    Upon reaching 6 charges, Kassadin can use Force Pulse to deal {{ e1 }} (+{{ a1 }}) magic damage and slow enemies by {{ e2 }}% for {{ e3 }} second in a cone in front of him.","leveltip":{"label":["Damage","Slow %","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[5,5,5,5,5],"cooldownBurn":"5","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[80,105,130,155,180],[50,60,70,80,90],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/105/130/155/180","50/60/70/80/90","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"ForcePulse.png","sprite":"spell5.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RiftWalk","name":"Riftwalk","description":"Kassadin teleports to a nearby location dealing damage to nearby enemy units. Multiple Riftwalks in a short period of time cost additional Mana but also deal additional damage.","tooltip":"Kassadin teleports to a nearby location dealing {{ e1 }} (+{{ f2 }}) (+{{ a1 }}) magic damage to surrounding enemy units.

    Each subsequent Riftwalk within the next {{ e2 }} seconds doubles the Mana cost and deals an additional {{ e3 }} (+{{ f1 }}) (+{{ f3 }}) magic damage per stack, stacking up to {{ e6 }} times.","leveltip":{"label":["Base Damage","Damage Per Stack","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[6,4,2],"cooldownBurn":"6/4/2","cost":[50,50,50],"costBurn":"50","effect":[null,[80,100,120],[15,15,15],[40,50,60],[1,1,1],[0,0,0],[4,4,4],[2,2,2],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"80/100/120","15","40/50/60","1","0","4","2","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"RiftWalk.png","sprite":"spell5.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Katarina":{"id":55,"key":"Katarina","name":"Katarina","title":"the Sinister Blade","spells":[{"id":"KatarinaQ","name":"Bouncing Blade","description":"Katarina throws a Dagger at the target that then bounces to nearby enemies before ricocheting onto the ground.","tooltip":"Katarina throws a Dagger, dealing {{ e1 }} (+{{ a1 }}) magic damage to the target and {{ e4 }} nearby enemies. The dagger then ricochets onto the ground behind the primary target.

    16","leveltip":{"label":["Initial Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[75,105,135,165,195],[350,350,350,350,350],[450,450,450,450,450],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/105/135/165/195","350","450","2","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"KatarinaQ.png","sprite":"spell5.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"KatarinaW","name":"Preparation","description":"Katarina gains a burst of movement speed, tossing a Dagger into the air directly above herself.","tooltip":"Katarina tosses a Dagger into the air and gains {{ e4 }}% decaying movement speed over {{ e2 }} seconds.","leveltip":{"label":["Movement Speed Buff","Cooldown"],"effect":["{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[0,0,0,0,0],[1.25,1.25,1.25,1.25,1.25],[0,0,0,0,0],[50,60,70,80,90],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","1.25","0","50/60/70/80/90","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[25000,25000,25000,25000,25000],"rangeBurn":"25000","image":{"full":"KatarinaW.png","sprite":"spell5.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"KatarinaEWrapper","name":"Shunpo","description":"Katarina blinks to the target, striking it if its an enemy, or striking the nearest enemy otherwise.","tooltip":"Katarina dashes in the blink of an eye to the target ally, enemy, or Dagger. If it is an enemy, Katarina strikes for {{ e1 }} (+{{ a1 }}) (+{{ a2 }}) magic damage - otherwise she strikes the nearest enemy in range.

    Picking up a Dagger will reduce Shunpo's cooldown by {{ f1 }} seconds ({{ f3*100 }}%).

    16","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}
    {{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,45,60,75,90],[0,0,0,0,0],[14,12.5,11,9.5,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/45/60/75/90","0","14/12.5/11/9.5/8","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.25,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"KatarinaEWrapper.png","sprite":"spell5.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"KatarinaR","name":"Death Lotus","description":"Katarina becomes a flurry of blades, dealing massive magic damage while she channels to the 3 nearest enemy champions.","tooltip":"Katarina becomes a flurry of blades, rapidly throwing knives at the three nearest enemy champions, dealing {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) magic damage per knife. Total over {{ e3 }} seconds to each enemy: {{ f1 }} (+{{ f2 }}) (+{{ f3 }}) magic damage.

    Applies Grievous Wounds to all enemies struck, reducing their healing and regeneration by 40%.","leveltip":{"label":["Damage Per Knife","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[90,60,45],"cooldownBurn":"90/60/45","cost":[0,0,0],"costBurn":"0","effect":[null,[25,37.5,50],[6,6,6],[2.5,2.5,2.5],[3,3,3],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"25/37.5/50","6","2.5","3","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.22,"key":"a2"},{"link":"spelldamage","coeff":0.19,"key":"a1"},{"link":"bonusattackdamage","coeff":3,"key":"f1"}],"costType":"No Cost","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"KatarinaR.png","sprite":"spell5.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Kayle":{"id":10,"key":"Kayle","name":"Kayle","title":"The Judicator","spells":[{"id":"JudicatorReckoning","name":"Reckoning","description":"Blasts an enemy unit with angelic force, dealing damage, slowing Movement Speed, and applying Holy Fervor.","tooltip":"Blasts an enemy, dealing {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) magic damage, slowing their Movement Speed by {{ e2 }}% for {{ e3 }} seconds, and applying a stack of Holy Fervor.","leveltip":{"label":["Damage","Slow","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[60,110,160,210,260],[35,40,45,50,55],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/110/160/210/260","35/40/45/50/55","3","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"JudicatorReckoning.png","sprite":"spell5.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JudicatorDivineBlessing","name":"Divine Blessing","description":"Blesses a target friendly champion, granting them increased Movement Speed and healing them.","tooltip":"Blesses an allied champion, increasing their Movement Speed by {{ e2 }}% (+{{ a2 }}%) for {{ e3 }} seconds and healing them for {{ e1 }} (+{{ a1 }}) Health.","leveltip":{"label":["Healing","Movement Speed","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[60,105,150,195,240],[18,21,24,27,30],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","18/21/24/27/30","3","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.07,"key":"a2"},{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"JudicatorDivineBlessing.png","sprite":"spell5.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JudicatorRighteousFury","name":"Righteous Fury","description":"Passive: Grants Kayle on Hit magic Damage. Activate: Ignite Kayle's sword with a holy flame, granting Kayle a ranged attack that damages multiple enemies and deals bonus magic damage.","tooltip":"Passive: Kayle's Basic Attacks deal an additional {{ e4 }} (+{{ a2 }}) magic damage on hit.

    Active: Kayle increases her Attack Range by 400 for 10 seconds and the passive bonus is increased to {{ e1 }} (+{{ a1 }}) magic damage and apply spell effects on hit.

    In addition, deals {{ e1 }} (+{{ f2 }}) (+{{ a1 }}) magic damage and applies spell effects on attack to enemies near her target.","leveltip":{"label":["Passive Damage","Active Damage","Damage to Nearby Enemies Ratio "],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[20,30,40,50,60],[20,25,30,35,40],[0.2,0.25,0.3,0.35,0.4],[10,15,20,25,30],[10,10,10,10,10],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/30/40/50/60","20/25/30/35/40","0.2/0.25/0.3/0.35/0.4","10/15/20/25/30","10","400","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.15,"key":"a2"},{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"attackdamage","coeff":[0.2,0.25,0.3,0.35,0.4],"key":"f2"},{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"JudicatorRighteousFury.png","sprite":"spell5.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JudicatorIntervention","name":"Intervention","description":"Shields Kayle or an ally for a short time, causing them to be immune to damage.","tooltip":"Bathes Kayle's target in a holy light, rendering them immune to all damage for {{ e1 }} seconds.","leveltip":{"label":["Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[0,0,0],"costBurn":"0","effect":[null,[2,2.5,3],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"2/2.5/3","0","0","0","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[900,900,900],"rangeBurn":"900","image":{"full":"JudicatorIntervention.png","sprite":"spell5.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Kennen":{"id":85,"key":"Kennen","name":"Kennen","title":"the Heart of the Tempest","spells":[{"id":"KennenShurikenHurlMissile1","name":"Thundering Shuriken","description":"Kennen throws a fast moving shuriken towards a location, causing damage and adding a Mark of the Storm to any opponent that it hits.","tooltip":"Throws a shuriken that deals {{ e1 }} (+{{ a1 }}) magic damage to the first enemy it hits.","leveltip":{"label":["Damage","Energy Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7,6,5,4],"cooldownBurn":"8/7/6/5/4","cost":[60,55,50,45,40],"costBurn":"60/55/50/45/40","effect":[null,[75,115,155,195,235],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/115/155/195/235","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Energy","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"KennenShurikenHurlMissile1.png","sprite":"spell5.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"KennenBringTheLight","name":"Electrical Surge","description":"Kennen passively deals extra damage and adds a Mark of the Storm to his target every few attacks, and he can activate this ability to damage and add another Mark of the Storm to targets who are already marked.","tooltip":"Passive: Every 5 attacks, Kennen deals bonus magic damage equal to {{ e2 }}% of his attack damage and adds a Mark of the Storm to his target.

    Active: Sends a surge of electricity through all nearby enemies affected by Mark of the Storm or Slicing Maelstrom, dealing {{ e1 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Passive %","Cooldown","Active Damage"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[14,12,10,8,6],"cooldownBurn":"14/12/10/8/6","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[65,95,125,155,185],[40,50,60,70,80],[0.4,0.5,0.6,0.7,0.8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/95/125/155/185","40/50/60/70/80","0.4/0.5/0.6/0.7/0.8","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.55,"key":"a1"}],"costType":" Energy","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"KennenBringTheLight.png","sprite":"spell5.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"KennenLightningRush","name":"Lightning Rush","description":"Kennen morphs into a lightning form, enabling him to pass through units. Each enemy unit he touches takes damage and receives a Mark of the Storm.","tooltip":"Kennen doubles his base movement speed and ignores unit collision for 2 seconds, dealing {{ e1 }} (+{{ a1 }}) magic damage to any enemy he passes through. Additionally, Kennen will gain {{ e2 }} Armor and Magic Resist for 4 seconds.

    Kennen gains 40 Energy the first time he passes through an enemy.
    Lightning Rush deals half damage to minions.","leveltip":{"label":["Damage","Bonus Armor and Magic Resist","Cooldown","Energy Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[100,95,90,85,80],"costBurn":"100/95/90/85/80","effect":[null,[85,125,165,205,245],[10,20,30,40,50],[40,40,40,40,40],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"85/125/165/205/245","10/20/30/40/50","40","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Energy","maxammo":"-1","range":[200,200,200,200,200],"rangeBurn":"200","image":{"full":"KennenLightningRush.png","sprite":"spell5.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"KennenShurikenStorm","name":"Slicing Maelstrom","description":"Kennen summons a storm that strikes at nearby enemy champions for magical damage.","tooltip":"Summons a magical storm for {{ e3 }} seconds that deals {{ e1 }} (+{{ a1 }}) magic damage to all enemies near Kennen every {{ e2 }} seconds. Each maelstrom hit applies Mark of the Storm, up to a maximum of 3.

    Successive maelstrom hits against the same enemy deal an additional {{ e4 }}% damage for each hit they've already suffered from this storm.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[120,120,120],"cooldownBurn":"120","cost":[0,0,0],"costBurn":"0","effect":[null,[40,75,110],[0.5,0.5,0.5],[3,3,3],[10,10,10],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"40/75/110","0.5","3","10","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"KennenShurikenStorm.png","sprite":"spell5.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Khazix":{"id":121,"key":"Khazix","name":"Kha'Zix","title":"the Voidreaver","spells":[{"id":"KhazixQ","name":"Taste Their Fear","description":"Deals physical damage to the target. Damage increased on Isolated targets. If he chooses to Evolve Reaper Claws, this refunds a percent of it's cooldown against Isolated targets. Kha'Zix also gains increased range on his basic attacks and Taste Their Fear.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) physical damage. If the target is Isolated, the damage is increased by {{ e2 }}% ({{ f1 }}).

    Evolved Reaper Claws: Increases the range of Taste Their Fear and Kha'Zix's basic attacks by {{ e3 }}. If target is Isolated, refunds {{ e4 }}% of Taste Their Fear's cooldown.","leveltip":{"label":["Base Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[70,95,120,145,170],[50,50,50,50,50],[50,50,50,50,50],[45,45,45,45,45],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/95/120/145/170","50","50","45","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"KhazixQ.png","sprite":"spell5.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KhazixW","name":"Void Spike","description":"Kha'Zix fires exploding spikes that deal physical damage to enemies hit. Kha'Zix is healed if he is also within the explosion radius. If he chooses to Evolve Spike Racks, Void Spike now fires three spikes in a cone, slow enemies hit, and reveals enemy champions hit for 2 seconds. Isolated targets are slowed for extra.","tooltip":"Fires exploding spikes, dealing {{ e1 }} (+{{ a1 }}) physical damage to enemies hit ({{ e5 }}% bonus damage to monsters). Heals Kha'Zix for {{ e2 }} (+{{ a2 }}) if he is within the explosion radius.

    Evolved Spike Racks: Void Spike fires three spikes in a cone and slows targets hit by {{ e3 }}% for {{ e4 }} seconds. Reveals enemy champions hit for 2 seconds. Isolated targets are slowed for {{ e7 }}% instead.","leveltip":{"label":["Damage","Healing","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[55,60,65,70,75],"costBurn":"55/60/65/70/75","effect":[null,[80,110,140,170,200],[60,85,110,135,160],[40,40,40,40,40],[2,2,2,2,2],[20,20,20,20,20],[2,2,2,2,2],[80,80,80,80,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/110/140/170/200","60/85/110/135/160","40","2","20","2","80","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"},{"link":"spelldamage","coeff":0.5,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"KhazixW.png","sprite":"spell5.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KhazixE","name":"Leap","description":"Kha'Zix leaps to an area, dealing physical damage upon landing. If he chooses to Evolve Wings, Leap's range increases by 200 and the cooldown resets on champion kill or assist.","tooltip":"Leaps to target area, dealing {{ e1 }} (+{{ a1 }}) physical damage.

    Evolved Wings: Increases Leap's range by 200, and the cooldown resets on champion kill or assist.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[65,100,135,170,205],[30,35,40,45,50],[10,10,10,10,10],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/100/135/170/205","30/35/40/45/50","10","2.5","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"KhazixE.png","sprite":"spell5.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KhazixR","name":"Void Assault","description":"Each rank allows Kha'Zix to evolve one of his abilities, giving it a unique additional effect. When activated, Kha'Zix becomes Invisible, triggering Unseen Threat and increasing Movement Speed. If he chooses to Evolve Adaptive Cloaking, while out of combat Kha'Zix also passively gains Void Assault upon entering a new brush.","tooltip":"Passive: Ranking up Void Assault allows Kha'Zix to evolve one of his abilities.

    Active: Kha'Zix becomes Invisible for {{ e1 }} seconds and activates Unseen Threat. While Invisible, he gains {{ e3 }}% movement speed and ignores unit collision. Void Assault can be cast a second time within {{ e2 }} seconds.

    Evolved Adaptive Cloaking: While out of combat, Kha'Zix also passively gains Void Assault for {{ e5 }} seconds in brush. This persists for up to {{ e1 }} seconds after exiting the brush ({{ f1 }} second per brush cooldown).

    16","leveltip":{"label":["Evolutions Available","Cooldown"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[1.25,1.25,1.25],[10,10,10],[40,40,40],[1,2,3],[2.5,2.5,2.5],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1.25","10","40","1/2/3","2.5","2","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"KhazixR.png","sprite":"spell5.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Kindred":{"id":203,"key":"Kindred","name":"Kindred","title":"The Eternal Hunters","spells":[{"id":"KindredQ","name":"Dance of Arrows","description":"Kindred tumbles and shoots up to three arrows at nearby targets.","tooltip":"Lamb vaults, firing up to 3 arrows at nearby enemies, dealing {{ e1 }} (+{{ a1 }}) physical damage and gains {{ f2 }}% bonus attack speed for {{ e8 }} seconds.

    Casting Wolf's Frenzy or vaulting inside of its effect reduces the cooldown of this spell to {{ e4 }} seconds.","leveltip":{"label":["Base Damage","Cooldown in Wolf's Frenzy"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[35,35,35,35,35],"costBurn":"35","effect":[null,[55,75,95,115,135],[0,0,0,0,0],[500,500,500,500,500],[4,3.5,3,2.5,2],[100,100,100,100,100],[12,12,12,12,12],[0.1,0.1,0.1,0.1,0.1],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"55/75/95/115/135","0","500","4/3.5/3/2.5/2","100","12","0.1","4","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.65,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[340,340,340,340,340],"rangeBurn":"340","image":{"full":"KindredQ.png","sprite":"spell5.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KindredW","name":"Wolf's Frenzy","description":"Wolf enrages and attacks enemies around him.","tooltip":"Wolf claims a territory, attacking nearby enemies inside it for {{ e4 }} seconds. Lamb can redirect Wolf to new targets by attacking them. If Lamb leaves Wolf's territory he will cease attacking and join her.

    Wolf's attacks deal magic damage equal to {{ e5 }} (+{{ a1 }}) plus {{ f3 }}% of the target's current health.

    Wolf attacks faster based on Kindred's attack speed. His attacks maim monsters for {{ e7 }}% increased damage and reduce their attack and movement speed by {{ e8 }}% for 2 seconds.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[40,45,50,55,60],[1,1,1,1,1],[1.5,1.5,1.5,1.5,1.5],[8.5,8.5,8.5,8.5,8.5],[25,30,35,40,45],[800,800,800,800,800],[50,50,50,50,50],[50,50,50,50,50],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/45/50/55/60","1","1.5","8.5","25/30/35/40/45","800","50","50","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[560,560,560,560,560],"rangeBurn":"560","image":{"full":"KindredW.png","sprite":"spell5.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KindredEWrapper","name":"Mounting Dread","description":"Lamb fires a carefully placed shot, slowing the target. If Lamb attacks the target two more times, her third attack instead directs Wolf to pounce on the enemy, savaging them for massive damage.","tooltip":"Cripple an enemy, slowing their movement speed by {{ e2 }}% for {{ e8 }} second.

    After Lamb attacks the target twice, her third attack instead directs Wolf to pounce on the enemy, dealing {{ e1 }} (+{{ a1 }}) plus {{ f2 }}% of the target's missing health as bonus physical damage.

    Wolf's attack critically strikes targets for {{ e6 }}% increased damage if they are below {{ e5 }}% (+{{ f3 }}%) health (Increased by Critical Strike Chance).

    [Maximum {{ e0 }} vs. monsters]","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[40,60,80,100,120],[50,50,50,50,50],[0.5,0.5,0.5,0.5,0.5],[8,8,8,8,8],[15,15,15,15,15],[50,50,50,50,50],[4,4,4,4,4],[1,1,1,1,1],[4,4,4,4,4],[300,300,300,300,300]],"effectBurn":[null,"40/60/80/100/120","50","0.5","8","15","50","4","1","4","300"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"KindredEWrapper.png","sprite":"spell5.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KindredR","name":"Lamb's Respite","description":"Lamb grants all living things inside a zone a respite from death. Until the effect ends, nothing can die. At the end, units are healed.","tooltip":"Lamb blesses the ground underneath her for {{ e2 }} seconds, creating an area in which no living things, ally or enemy, can die. Upon reaching 10% Health, units become immune to further damage or healing.

    When the blessing ends all living things inside heal for {{ e1 }}.","leveltip":{"label":["Heal","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,130,100],"cooldownBurn":"160/130/100","cost":[0,0,0],"costBurn":"0","effect":[null,[200,250,300],[4,4,4],[530,530,530],[400,600,800],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"200/250/300","4","530","400/600/800","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"KindredR.png","sprite":"spell6.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Kled":{"id":240,"key":"Kled","name":"Kled","title":"the Cantankerous Cavalier","spells":[{"id":"KledQ","name":"Bear Trap on a Rope","description":"Kled throws a bear trap that damages and hooks an enemy champion. If shackled for a short duration, the target takes additional physical damage and is yanked toward Kled.

    When dismounted, this ability is replaced by Pocket Pistol, a ranged gun blast that knocks back Kled and restores courage.","tooltip":"Kled throws a bear trap that deals {{ e1 }} (+{{ a1 }}) physical damage and hooks onto the first enemy champion or large monster hit, granting True Sight. Deals 150% damage to minions passed through.

    If Kled stays near a hooked enemy for {{ e3 }} seconds, he deals {{ e1 }} (+{{ charbonusphysical*2 }}) physical damage, yanks the enemy toward him, and slows the enemy by {{ e5 }}% for 1.5 seconds.

    Dismounted: Becomes Pocket Pistol, a ranged ability that restores courage.","leveltip":{"label":["Throw Damage","Rip Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e1 }} -> {{ e1NL }}","{{ e5 }}% -> {{ e5NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8.5,8,7.5,7],"cooldownBurn":"9/8.5/8/7.5/7","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,100,150,200,250],[0.6,0.6,0.6,0.6,0.6],[1.75,1.75,1.75,1.75,1.75],[2,2,2,2,2],[40,45,50,55,60],[9,8.5,8,7.5,7],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/100/150/200/250","0.6","1.75","2","40/45/50/55/60","9/8.5/8/7.5/7","1.5","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"KledQ.png","sprite":"spell6.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"KledW","name":"Violent Tendencies","description":"Kled gains massive attack speed for four attacks. The fourth attack deals more damage.","tooltip":"Passive: When Kled basic attacks, he frenzies, gaining {{ e2 }}% attack speed for four attacks or {{ e6 }} seconds. Violent Tendencies then goes on cooldown.

    The fourth hit deals bonus physical damage equal to {{ e4 }} plus {{ e1 }}% (+{{ f1 }}%) of the target's maximum health (maximum {{ e9 }} vs monsters).","leveltip":{"label":["Flat Damage","Percentage Damage","Cooldown"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,12.5,11,9.5,8],"cooldownBurn":"14/12.5/11/9.5/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[4,4.5,5,5.5,6],[150,150,150,150,150],[20,20,20,20,20],[20,30,40,50,60],[15,30,45,60,75],[4,4,4,4,4],[200,200,200,200,200],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/4.5/5/5.5/6","150","20","20/30/40/50/60","15/30/45/60/75","4","200","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[0,0,0,0,0],"rangeBurn":"0","image":{"full":"KledW.png","sprite":"spell6.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"KledE","name":"Jousting","description":"Kled dashes, dealing physical damage and gaining a short burst of speed. Kled can then reactivate this ability to dash back through his initial target, dealing the same damage.","tooltip":"Kled dashes, dealing {{ e1 }} (+{{ a1 }}) physical damage to enemies in his path. Cannot cross walls.

    If Jousting hits an enemy champion or large monster, Kled gains {{ e5 }}% movement speed for {{ e4 }} second and True Sight of the target. He can reactivate this ability within {{ e6 }} seconds to dash back through the same target, dealing the same damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,45,70,95,120],[0,0,0,0,0],[350,350,350,350,350],[1,1,1,1,1],[50,50,50,50,50],[3,3,3,3,3],[600,600,600,600,600],[700,700,700,700,700],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/45/70/95/120","0","350","1","50","3","600","700","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"KledE.png","sprite":"spell6.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"KledR","name":"Chaaaaaaaarge!!!","description":"Kled and Skaarl charge to a location, leaving a speed-granting trail behind them and gaining a shield. Skaarl locks onto and rams the first enemy champion encountered.","tooltip":"Kled charges toward a location, leaving a trail that grants allies increasing movement speed. While charging, Kled gains a shield that increases over time to a maximum of {{ e4 }} (+{{ a1 }}) and lasts for 2 seconds after the charge ends.

    Skaarl rams the first enemy champion encountered, dealing up to {{ e1 }}% (+{{ f1*3 }}%) of the target's maximum health as physical damage based on distance traveled.","leveltip":{"label":["Maximum Damage","Maximum Shield","Range","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e4 }} -> {{ e4NL }}","{{ e0 }} -> {{ e0NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,140,120],"cooldownBurn":"160/140/120","cost":[0,0,0],"costBurn":"0","effect":[null,[12,15,18],[4,4,4],[0,0,0],[200,300,400],[3,3,3],[100,150,0],[0,0,0],[150,0,0],[3,3,3],[3500,4000,4500]],"effectBurn":[null,"12/15/18","4","0","200/300/400","3","100/150/0","0","150/0/0","3","3500/4000/4500"],"vars":[{"link":"bonusattackdamage","coeff":3,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[3500,4000,4500],"rangeBurn":"3500/4000/4500","image":{"full":"KledR.png","sprite":"spell6.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"No Cost"}]},"KogMaw":{"id":96,"key":"KogMaw","name":"Kog'Maw","title":"the Mouth of the Abyss","spells":[{"id":"KogMawQ","name":"Caustic Spittle","description":"Kog'Maw launches a corrosive projectile which deals magic damage and corrodes the target's armor and magic resist for a short time. Kog'Maw also gains additional attack speed.","tooltip":"Passive: Kog'Maw gains {{ e2 }}% bonus attack speed.

    Active: Launches a corrosive projectile that deals {{ e1 }} (+{{ a1 }}) Magic Damage to the first enemy hit and shreds its Armor and Magic Resist by {{ e3 }}% for {{ e4 }} seconds.","leveltip":{"label":["Bonus Attack Speed","Damage","Armor and Magic Resist Shred"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[80,130,180,230,280],[15,20,25,30,35],[20,22,24,26,28],[4,4,4,4,4],[1,1,1,1,1],[1,1,1,1,1],[100,100,100,100,100],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/130/180/230/280","15/20/25/30/35","20/22/24/26/28","4","1","1","100","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1175,1175,1175,1175,1175],"rangeBurn":"1175","image":{"full":"KogMawQ.png","sprite":"spell6.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KogMawBioArcaneBarrage","name":"Bio-Arcane Barrage","description":"Kog'Maw's attacks gain range and deal a percent of the target's maximum health as magic damage.","tooltip":"For {{ e3 }} seconds, Kog'Maw's basic attacks gain {{ e1 }} range and deal {{ e2 }}% (+{{ f1 }})% of the target's maximum health as bonus magic damage.","leveltip":{"label":["Range","Bonus Max Health Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[17,17,17,17,17],"cooldownBurn":"17","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[130,150,170,190,210],[3,4,5,6,7],[8,8,8,8,8],[100,100,100,100,100],[100,100,100,100,100],[0,0,0,0,0],[100,100,100,100,100],[15,15,15,15,15],[0.15,0.2,0.25,0.3,0.35],[15,20,25,30,35]],"effectBurn":[null,"130/150/170/190/210","3/4/5/6/7","8","100","100","0","100","15","0.15/0.2/0.25/0.3/0.35","15/20/25/30/35"],"vars":[],"costType":" Mana","maxammo":"-1","range":[530,530,530,530,530],"rangeBurn":"530","image":{"full":"KogMawBioArcaneBarrage.png","sprite":"spell6.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KogMawVoidOoze","name":"Void Ooze","description":"Kog'Maw launches a peculiar ooze which damages all enemies it passes through and leaves a trail which slows enemies who stand on it.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) Magic Damage to enemies hit and leaves a trail on the ground for {{ e3 }} seconds, slowing enemies in it by {{ e2 }}%.","leveltip":{"label":["Damage","Slow","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[60,105,150,195,240],[20,28,36,44,52],[4,4,4,4,4],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","20/28/36/44/52","4","0.25","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200,1200,1200],"rangeBurn":"1200","image":{"full":"KogMawVoidOoze.png","sprite":"spell6.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"KogMawLivingArtillery","name":"Living Artillery","description":"Kog'Maw fires an artillery shell at a great distance dealing magic damage (increased significantly on low health enemies) and revealing non-stealthed targets. Additionally, multiple Living Artilleries in a short period of time cause them to cost additional Mana.","tooltip":"Fires an artillery shot, damaging enemies above 40% health for {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) to {{ effect1amount*1.5 }} (+{{ f3 }}) (+{{ f2 }}) magic damage based on their missing health.

    If enemies are below 40% health, they take {{ f4 }} (+{{ f6 }}) (+{{ f5 }}) magic damage instead.

    Each subsequent shot within {{ e4 }} seconds costs {{ e3 }} more Mana (max {{ e5 }}).","leveltip":{"label":["Damage","Range","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[2,1.5,1],"cooldownBurn":"2/1.5/1","cost":[40,40,40],"costBurn":"40","effect":[null,[100,140,180],[1200,1500,1800],[40,40,40],[8,8,8],[400,400,400],[2,2,2],[1.5,1.5,1.5],[2,2,2],[360,360,360],[400,400,400]],"effectBurn":[null,"100/140/180","1200/1500/1800","40","8","400","2","1.5","2","360","400"],"vars":[{"link":"bonusattackdamage","coeff":0.65,"key":"a2"},{"link":"spelldamage","coeff":0.25,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1500,1800],"rangeBurn":"1200/1500/1800","image":{"full":"KogMawLivingArtillery.png","sprite":"spell6.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Leblanc":{"id":7,"key":"Leblanc","name":"LeBlanc","title":"the Deceiver","spells":[{"id":"LeblancQ","name":"Shatter Orb","description":"LeBlanc projects an orb towards her target, dealing magic damage. When Shatter Orb is empowered by Sigil of Malice, the spell bounces to the nearest ready Sigil of Malice (bounces deal reduced damage to minions).","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage. When Shatter Orb is empowered by Sigil of Malice, the spell bounces to the nearest ready Sigil of Malice (bounces deal {{ e7 }}% damage to minions). ","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[55,90,125,160,195],[25,25,25,25,25],[3.5,3.5,3.5,3.5,3.5],[6,6,6,6,6],[60,90,120,150,180],[0,0,0,0,0],[80,80,80,80,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"55/90/125/160/195","25","3.5","6","60/90/120/150/180","0","80","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"LeblancQ.png","sprite":"spell6.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeblancW","name":"Distortion","description":"LeBlanc rapidly dashes to a target location, dealing magic damage to nearby units. In the following 4 seconds, she can activate Distortion again to return to her starting location.","tooltip":"Dashes to target location, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies in the target area.

    For the next 4 seconds, she can activate Distortion again to return to her starting location.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[40,55,70,85,100],[2,2,2,2,2],[4,4,4,4,4],[600,600,600,600,600],[0.85,0.85,0.85,0.85,0.85],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/55/70/85/100","2","4","600","0.85","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"LeblancW.png","sprite":"spell6.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeblancE","name":"Ethereal Chains","description":"LeBlanc flings illusionary chains towards a target location. If it hits an enemy unit, it will deal initial magic damage and shackle them. If the target remains shackled for 1.5 seconds, the target takes additional magic damage and is unable to move.","tooltip":"Launches a chain that shackles the first unit hit to LeBlanc, granting True Sight of the unit and dealing {{ e1 }} (+{{ a1 }}) magic damage.

    If the target remains shackled for {{ e2 }} seconds they are rooted for {{ e2 }} seconds and take an additional {{ e1 }} (+{{ a1 }}) magic damage. ","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f1 }} -> {{ f2 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[40,60,80,100,120],[1.5,1.5,1.5,1.5,1.5],[1.5,1.5,1.5,1.5,1.5],[25,25,25,25,25],[885,885,885,885,885],[14,13.25,12.5,11.75,11],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/60/80/100/120","1.5","1.5","25","885","14/13.25/12.5/11.75/11","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"LeblancE.png","sprite":"spell6.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeblancR","name":"Mimic","description":"LeBlanc creates a clone and casts a mimicked version of one of her basic spells. The clone casts a fake copy of that spell. LeBlanc can instead send the clone far away, where it casts a fake copy of her most recently cast spell.","tooltip":"LeBlanc copies one of her spells and creates a Mimic decoy to cast it with her. The Mimic lasts {{ e7 }} seconds.

  • Shatter Orb deals {{ e3 }} (+{{ a2 }}) magic damage.
    Distortion deals {{ e5 }} (+{{ f1 }}) magic damage.
    Ethereal Chains deals {{ e4 }} (+{{ a1 }}) magic damage each time.
    Using this spell on itself invokes Shadow of the Rose.

  • Shadow of the Rose
    The Mimic appears at target location and approaches the closest visible enemy Champion, then casts a fake version of LeBlanc's most recent basic ability.

    Has a separate {{ f2 }} second cooldown. This Mimic lasts much longer but cannot be controlled.","leveltip":{"label":["Mimicked Shatter Orb Damage","Mimicked Distortion Damage","Mimicked Ethereal Chains Damage","Shadow of the Rose Cooldown","Mimic Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e5 }} -> {{ e5NL }}","{{ e4 }} -> {{ e4NL }}","{{ f2 }} -> {{ f3 }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[54,42,30],"cooldownBurn":"54/42/30","cost":[0,0,0],"costBurn":"0","effect":[null,[20,40,60],[30,37.5,45],[150,275,400],[100,160,220],[60,120,180],[160,140,120],[2.5,2.5,2.5],[150,150,150],[0.3,0.3,0.3],[0.075,0.075,0.075]],"effectBurn":[null,"20/40/60","30/37.5/45","150/275/400","100/160/220","60/120/180","160/140/120","2.5","150","0.3","0.08"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a2"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":"Toggle","maxammo":"2","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"LeblancR.png","sprite":"spell6.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"Toggle"}]},"LeeSin":{"id":64,"key":"LeeSin","name":"Lee Sin","title":"the Blind Monk","spells":[{"id":"BlindMonkQOne","name":"Sonic Wave / Resonating Strike","description":"Sonic Wave: Lee Sin projects a discordant wave of sound to locate his enemies, dealing physical damage to the first enemy it encounters. If Sonic Wave hits, Lee Sin can cast Resonating Strike for the next 3 seconds.
    Resonating Strike: Lee Sin dashes to the enemy hit by Sonic Wave, dealing physical damage plus 8% of their missing Health.","tooltip":"Sonic Wave: Lee Sin projects a discordant wave of sound to locate his enemies, dealing {{ e1 }} (+{{ a1 }}) physical damage to the first enemy it encounters, granting True Sight of the target. If Sonic Wave hits, Lee Sin can cast Resonating Strike for the next {{ e7 }} seconds.

    Resonating Strike: Lee Sin dashes to the enemy hit by Sonic Wave, dealing {{ e2 }} (+{{ a1 }}) physical damage plus {{ e3 }}% of their missing Health (Max: {{ e6 }} Damage vs. Monsters).","leveltip":{"label":["Sonic Wave Damage","Resonating Strike Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[50,80,110,140,170],[50,80,110,140,170],[8,8,8,8,8],[30,30,30,30,30],[1350,1350,1350,1350,1350],[400,400,400,400,400],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/80/110/140/170","50/80/110/140/170","8","30","1350","400","3","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.9,"key":"a1"},{"link":"bonusattackdamage","coeff":0.9,"key":"a1"}],"costType":" Energy / {{ e4 }} Energy","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"BlindMonkQOne.png","sprite":"spell6.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Energy / {{ e4 }} Energy"},{"id":"BlindMonkWOne","name":"Safeguard / Iron Will","description":"Safeguard: Lee Sin rushes to target ally, shielding himself from damage. If the ally is a champion, they are also shielded. After using Safeguard, Lee Sin can cast Iron Will for the next 3 seconds.
    Iron Will: Lee Sin's intense training allows him to thrive in battle. For 4 seconds, Lee Sin gains Life Steal and Spell Vamp.","tooltip":"Safeguard: Lee Sin rushes to target ally. If the ally is a champion, Lee Sin shields the ally and himself for {{ e1 }} (+{{ a1 }}) damage for {{ e3 }} seconds and Safeguard's cooldown is reduced by {{ e6 }}%. After using Safeguard, Lee Sin can cast Iron Will for the next {{ e9 }} seconds.

    Iron Will: Lee Sin gains {{ e2 }}% Life Steal and Spell Vamp for {{ e8 }} seconds.","leveltip":{"label":["Safeguard Shield Absorption","Iron Will Lifesteal / Spell Vamp %"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[14,14,14,14,14],"cooldownBurn":"14","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[40,100,160,220,280],[10,15,20,25,30],[2,2,2,2,2],[30,30,30,30,30],[5,5,5,5,5],[50,50,50,50,50],[0,0,0,0,0],[4,4,4,4,4],[3,3,3,3,3],[1350,1350,1350,1350,1350]],"effectBurn":[null,"40/100/160/220/280","10/15/20/25/30","2","30","5","50","0","4","3","1350"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Energy / {{ e4 }} Energy","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"BlindMonkWOne.png","sprite":"spell6.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Energy / {{ e4 }} Energy"},{"id":"BlindMonkEOne","name":"Tempest / Cripple","description":"Tempest: Lee Sin smashes the ground, sending out a shockwave that deals magic damage and reveals enemy units hit. If Tempest hits an enemy, Lee Sin can cast cripple for the next 3 seconds.
    Cripple: Lee Sin cripples nearby enemies damaged by Tempest, reducing their Movement Speed for 4 seconds. Movement Speed recovers gradually over the duration.","tooltip":"Tempest: Lee Sin smashes the ground, sending out a shockwave that deals {{ e1 }} (+{{ a1 }}) magic damage. If Tempest hits an enemy, Lee Sin can cast Cripple for the next {{ e6 }} seconds.

    Cripple: Lee Sin cripples nearby enemies struck by Tempest for {{ e5 }} seconds, slowing their Movement Speed by {{ e2 }}%. Movement Speed recovers gradually over the duration.","leveltip":{"label":["Tempest Damage","Cripple Slow"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[60,95,130,165,200],[20,30,40,50,60],[60,95,130,165,200],[30,30,30,30,30],[4,4,4,4,4],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","20/30/40/50/60","60/95/130/165/200","30","4","3","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"}],"costType":" Energy / {{ e4 }} Energy","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"BlindMonkEOne.png","sprite":"spell6.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Energy / {{ e4 }} Energy"},{"id":"BlindMonkRKick","name":"Dragon's Rage","description":"Lee Sin performs a powerful roundhouse kick launching his target back, dealing physical damage to the target and any enemies they collide with. Enemies the target collides with are knocked into the air for a short duration. This technique was taught to him by Jesse Perring, although Lee Sin does not kick players off the map.","tooltip":"Lee Sin performs a powerful roundhouse kick knocking an enemy champion back and dealing {{ e1 }} (+{{ a1 }}) physical damage.

    Enemies the target collides with are knocked into the air briefly and take physical damage equal to {{ e1 }} (+{{ a1 }}) plus {{ e3 }}% of the initial target's bonus health.
    ","leveltip":{"label":["Damage","Bonus Health Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,85,60],"cooldownBurn":"110/85/60","cost":[0,0,0],"costBurn":"0","effect":[null,[150,300,450],[75,125,175],[12,15,18],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/300/450","75/125/175","12/15/18","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":2,"key":"a1"},{"link":"bonusattackdamage","coeff":2,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[375,375,375],"rangeBurn":"375","image":{"full":"BlindMonkRKick.png","sprite":"spell6.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Leona":{"id":89,"key":"Leona","name":"Leona","title":"the Radiant Dawn","spells":[{"id":"LeonaShieldOfDaybreak","name":"Shield of Daybreak","description":"Leona uses her shield to perform her next basic attack, dealing bonus magic damage and stunning the target.","tooltip":"Next basic attack deals {{ e2 }} (+{{ a1 }}) bonus magic damage and stuns for {{ e1 }} second.","leveltip":{"label":["Damage","Mana Cost","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[1,1,1,1,1],[30,55,80,105,130],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1","30/55/80/105/130","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[100,100,100,100,100],"rangeBurn":"100","image":{"full":"LeonaShieldOfDaybreak.png","sprite":"spell6.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeonaSolarBarrier","name":"Eclipse","description":"Leona raises her shield to gain Armor and Magic Resist. When the duration first ends, if there are nearby enemies, she will deal magic damage to them and prolong the duration of the effect.","tooltip":"Grants {{ e2 }} (+{{ f1 }}) bonus Armor and {{ e2 }} (+{{ f2 }}) Magic Resist for {{ e3 }} seconds. When the effect ends, nearby enemies struck take {{ e1 }} (+{{ a1 }}) magic damage and Leona retains her bonus Armor and Magic Resist for {{ e3 }} seconds.","leveltip":{"label":["Damage","Armor and Magic Resist"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[14,14,14,14,14],"cooldownBurn":"14","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[60,100,140,180,220],[20,30,40,50,60],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","20/30/40/50/60","3","0","0","0","0","0","0","0"],"vars":[{"link":"bonusarmor","coeff":0.2,"key":"f1"},{"link":"bonusspellblock","coeff":0.2,"key":"f2"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"LeonaSolarBarrier.png","sprite":"spell6.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeonaZenithBlade","name":"Zenith Blade","description":"Leona projects a solar image of her sword, dealing magic damage to all enemies in a line. When the image fades, the last enemy champion struck will be briefly immobilized and Leona will dash to them.","tooltip":"Strikes all enemies in a line dealing {{ e1 }} (+{{ a1 }}) magic damage. The last enemy Champion struck will be rooted for {{ e2 }} seconds and Leona will dash to them.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[60,100,140,180,220],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","0.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[875,875,875,875,875],"rangeBurn":"875","image":{"full":"LeonaZenithBlade.png","sprite":"spell6.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LeonaSolarFlare","name":"Solar Flare","description":"Leona calls down a beam of solar energy, dealing damage to enemies in an area. Enemies in the center of the area are stunned, while enemies on the outside are slowed. Afterward, Leona's sword is charged with the power of the sun and deals bonus magic damage for a few attacks.","tooltip":"Calls down a radiant beam of solar energy dealing {{ e4 }} (+{{ a1 }}) magic damage and slowing enemies by {{ e1 }}% for {{ e2 }} seconds. Enemies in the center of the flare are stunned instead of slowed.

    Leona's sword remains charged with Incandescence, causing her next {{ e5 }} basic attacks to gain 100 range and deal {{ e3 }} (+{{ a2 }}) bonus magic damage on hit.","leveltip":{"label":["Damage","Incandescence Damage","Incandescent Attacks","Cooldown"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e3 }} -> {{ e3NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[90,75,60],"cooldownBurn":"90/75/60","cost":[100,100,100],"costBurn":"100","effect":[null,[80,80,80],[1.5,1.5,1.5],[30,40,50],[100,175,250],[3,4,5],[5,5,5],[100,100,100],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"80","1.5","30/40/50","100/175/250","3/4/5","5","100","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"},{"link":"spelldamage","coeff":0.15,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200],"rangeBurn":"1200","image":{"full":"LeonaSolarFlare.png","sprite":"spell6.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Lissandra":{"id":127,"key":"Lissandra","name":"Lissandra","title":"the Ice Witch","spells":[{"id":"LissandraQ","name":"Ice Shard","description":"Throws a spear of ice that shatters when it hits an enemy, dealing magic damage and slowing Movement Speed. Shards pass through the target, dealing the same damage to other enemies hit.","tooltip":"Throws a spear of ice that shatters when it hits an enemy, dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing Movement Speed by {{ e3 }}% for {{ e2 }} seconds. Shards then pass through the target, dealing the same damage to other enemies hit.","leveltip":{"label":["Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[6,5.25,4.5,3.75,3],"cooldownBurn":"6/5.25/4.5/3.75/3","cost":[85,85,85,85,85],"costBurn":"85","effect":[null,[70,100,130,160,190],[1.5,1.5,1.5,1.5,1.5],[16,19,22,25,28],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","1.5","16/19/22/25/28","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"LissandraQ.png","sprite":"spell6.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LissandraW","name":"Ring of Frost","description":"Freezes nearby enemies in ice, dealing magic damage and rooting them. ","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and roots them for {{ e2 }} seconds.","leveltip":{"label":["Damage","Root Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[70,110,150,190,230],[1.1,1.2,1.3,1.4,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","1.1/1.2/1.3/1.4/1.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"LissandraW.png","sprite":"spell6.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LissandraE","name":"Glacial Path","description":"Lissandra creates an ice claw that deals magic damage. Reactivating this ability transports Lissandra to the claw's current location.","tooltip":"Casts an ice claw that deals {{ e1 }} (+{{ a1 }}) magic damage to enemies hit.

    Reactivating this ability transports Lissandra to the claw's current location.","leveltip":{"label":["Damage","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[24,21,18,15,12],"cooldownBurn":"24/21/18/15/12","cost":[80,85,90,95,100],"costBurn":"80/85/90/95/100","effect":[null,[70,115,160,205,250],[14,13,12,11,10],[20,20,20,20,20],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","14/13/12/11/10","20","1","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"LissandraE.png","sprite":"spell6.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LissandraR","name":"Frozen Tomb","description":"If cast on an enemy champion, the target is frozen solid, stunning it. If cast on Lissandra, she encases herself in dark ice, healing herself while becoming untargetable and invulnerable. Dark ice then emanates from the target dealing magic damage to enemies and slowing Movement Speed.","tooltip":"On Enemy Cast: Freezes target champion solid, stunning it for {{ e4 }} seconds.

    On Self Cast: Lissandra encases herself in dark ice for {{ e5 }} seconds, healing for {{ e6 }} (+{{ a2 }}), increased by {{ e7 }}% for each {{ e8 }}% Health she is missing. During this time Lissandra is untargetable and invulnerable but is unable to take any actions.

    Dark ice then emanates from the target dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies. The ice lasts for {{ e3 }} seconds and slows enemy Movement Speed by {{ e2 }}%.","leveltip":{"label":["Damage","Slow","Heal","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e6 }} -> {{ e6NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,105,80],"cooldownBurn":"130/105/80","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[30,45,75],[3,3,3],[1.5,1.5,1.5],[2.5,2.5,2.5],[100,150,200],[1,1,1],[1,1,1],[292,292,292],[0,0,0]],"effectBurn":[null,"150/250/350","30/45/75","3","1.5","2.5","100/150/200","1","1","292","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a2"},{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"LissandraR.png","sprite":"spell6.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Lucian":{"id":236,"key":"Lucian","name":"Lucian","title":"the Purifier","spells":[{"id":"LucianQ","name":"Piercing Light","description":"Lucian shoots a bolt of piercing light through a target.","tooltip":"Shoots a bolt of piercing light through an enemy unit, damaging enemies in a line for {{ e1 }} (+{{ f1 }}) ({{ e2 }}% of bonus Attack Damage) physical damage.

    Piercing Light's cast time decreases slightly as Lucian gains levels.","leveltip":{"label":["Damage","Bonus Attack Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[80,115,150,185,220],[60,70,80,90,100],[900,900,900,900,900],[0.41,0.41,0.41,0.41,0.41],[100,100,100,100,100],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/115/150/185/220","60/70/80/90/100","900","0.41","100","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.6,0.75,0.9,1.05,1.2],"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"LucianQ.png","sprite":"spell6.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LucianW","name":"Ardent Blaze","description":"Lucian shoots a missile that explodes in a star shape, marking enemies. Lucian gains Movement Speed for attacking marked enemies.","tooltip":"Fires a shot that explodes upon enemy contact or reaching the end of its path. The explosion deals {{ e1 }} (+{{ a1 }}) magic damage and marks enemies for 6 seconds.

    When Lucian or his allies damage a marked target, Lucian gains {{ e2 }} Movement Speed for 1 second.","leveltip":{"label":["Damage","Movement Speed","Cooldown
    "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[60,100,140,180,220],[60,65,70,75,80],[900,900,900,900,900],[0,0,0,0,0],[1,1,1,1,1],[200,200,200,200,200],[1,1,1,1,1],[6,6,6,6,6],[1,1,1,1,1],[700,700,700,700,700]],"effectBurn":[null,"60/100/140/180/220","60/65/70/75/80","900","0","1","200","1","6","1","700"],"vars":[{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"LucianW.png","sprite":"spell6.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LucianE","name":"Relentless Pursuit","description":"Lucian quickly dashes a short distance. Lightslinger attacks reduce Relentless Pursuit's cooldown.","tooltip":"Quickly dashes a short distance.

    Whenever Lightslinger hits an enemy, Relentless Pursuit's cooldown is reduced by {{ e1 }} second (doubles to {{ e2 }} seconds against champions).","leveltip":{"label":["Cooldown","Mana Cost"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[40,30,20,10,0],"costBurn":"40/30/20/10/0","effect":[null,[1,1,1,1,1],[2,2,2,2,2],[425,425,425,425,425],[200,200,200,200,200],[1350,1350,1350,1350,1350],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1","2","425","200","1350","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[445,445,445,445,445],"rangeBurn":"445","image":{"full":"LucianE.png","sprite":"spell6.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LucianR","name":"The Culling","description":"Lucian unleashes a torrent of shots from his weapons.","tooltip":"Lucian moves freely while firing rapidly in a single direction for {{ e1 }} seconds. His shots collide with the first enemy they hit and each do {{ e2 }} (+{{ a1 }}) (+{{ a2 }}) physical damage. The Culling does {{ e8 }}% damage to minions.

    Lucian may use Relentless Pursuit during The Culling.

    Reactivate The Culling to cancel early.","leveltip":{"label":["Damage","Number of Shots","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,100,90],"cooldownBurn":"110/100/90","cost":[100,100,100],"costBurn":"100","effect":[null,[3,3,3],[20,35,50],[2.5,3,3.5],[2.5,2.5,2.5],[20,25,30],[0.75,0.75,0.75],[1050,1050,1050],[400,400,400],[125,125,125],[0.25,0.25,0.25]],"effectBurn":[null,"3","20/35/50","2.5/3/3.5","2.5","20/25/30","0.75","1050","400","125","0.25"],"vars":[{"link":"spelldamage","coeff":0.1,"key":"a1"},{"link":"attackdamage","coeff":0.2,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1400,1400,1400],"rangeBurn":"1400","image":{"full":"LucianR.png","sprite":"spell6.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Lulu":{"id":117,"key":"Lulu","name":"Lulu","title":"the Fae Sorceress","spells":[{"id":"LuluQ","name":"Glitterlance","description":"Pix and Lulu each fire a bolt of magical energy that heavily slows all enemies it hits. An enemy can only be damaged by one bolt.","tooltip":"Lulu and Pix each fire a piercing bolt dealing {{ e1 }} (+{{ a1 }}) magic damage to the first enemy hit and {{ f4 }} (+{{ f5 }}) to all additional enemies. Enemies hit are slowed by {{ e2 }}%, decaying over the next {{ e3 }} second(s).

    An enemy can only be damaged for up to a total of {{ f6 }} damage per cast.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[80,125,170,215,260],[80,80,80,80,80],[2,2,2,2,2],[0,0,0,0,0],[0.7,0.7,0.7,0.7,0.7],[8,8,8,8,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/125/170/215/260","80","2","0","0.7","8","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"LuluQ.png","sprite":"spell6.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuluW","name":"Whimsy","description":"If cast on an ally, grants them Attack Speed and Movement Speed for a short time. If cast on an enemy, turns them into an adorable critter that can't attack or cast spells.","tooltip":"On Ally Cast: Target ally gains {{ e1 }} (+{{ a2 }})% Movement Speed and {{ e7 }}% Attack Speed for {{ e5 }} seconds.

    On Enemy Cast: Polymorphs an enemy champion for {{ e3 }} seconds, disabling their ability to attack or cast spells and reducing their base Movement Speed by {{ e4 }}.","leveltip":{"label":["Movement and Attack Speed Duration","Attack Speed Bonus","Polymorph Duration","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ e7 }}% -> {{ e7NL }}%","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[30,30,30,30,30],[0,0,0,0,0],[1.25,1.5,1.75,2,2.25],[60,60,60,60,60],[3,3.25,3.5,3.75,4],[0.01,0.01,0.01,0.01,0.01],[25,30,35,40,45],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30","0","1.25/1.5/1.75/2/2.25","60","3/3.25/3.5/3.75/4","0.01","25/30/35/40/45","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.05,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"LuluW.png","sprite":"spell6.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuluE","name":"Help, Pix!","description":"If cast on an ally, commands Pix to jump to an ally and shield them. He then follows them and aids their attacks. If cast on an enemy, commands Pix to jump to an enemy and damage them. He then follows them and grants you vision of that enemy.","tooltip":"On Ally Cast: Commands Pix to jump to an ally and then follow and aid their attacks instead of Lulu's for {{ e1 }} seconds. If the ally is a champion, Pix shields them from {{ e2 }} (+{{ a1 }}) damage for {{ e1 }} seconds.

    On Enemy Cast: Pix deals {{ e4 }} (+{{ a2 }}) magic damage to target enemy unit. Pix then follows and grants True Sight of them for {{ e6 }} seconds.
    ","leveltip":{"label":["Shield Amount","Damage","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[6,6,6,6,6],[70,105,140,175,210],[50,50,50,50,50],[80,110,140,170,200],[25,25,25,25,25],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"6","70/105/140/175/210","50","80/110/140/170/200","25","4","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"LuluE.png","sprite":"spell6.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuluR","name":"Wild Growth","description":"Lulu enlarges an ally, knocking nearby enemies into the air and granting the ally a large amount of bonus health. For the next few seconds, that ally gains an aura that slows nearby enemies.","tooltip":"Lulu enlarges her ally, knocking nearby enemies into the air. For {{ e4 }} seconds, her ally gains {{ e1 }} (+{{ a1 }}) bonus health and slows nearby enemies by {{ e2 }}%.","leveltip":{"label":["Bonus Health","Slow Percent","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,95,80],"cooldownBurn":"110/95/80","cost":[100,100,100],"costBurn":"100","effect":[null,[300,450,600],[30,45,60],[1,1,1],[7,7,7],[50,50,50],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"300/450/600","30/45/60","1","7","50","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900],"rangeBurn":"900","image":{"full":"LuluR.png","sprite":"spell6.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Lux":{"id":99,"key":"Lux","name":"Lux","title":"the Lady of Luminosity","spells":[{"id":"LuxLightBinding","name":"Light Binding","description":"Lux releases a sphere of light that binds and deals damage to up to two enemy units.","tooltip":"Fires a ball of light, rooting up to two enemies for {{ e3 }} seconds and dealing {{ e1 }} (+{{ a1 }}) magic damage to each.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[50,100,150,200,250],[50,50,50,50,50],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/100/150/200/250","50","2","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1175,1175,1175,1175,1175],"rangeBurn":"1175","image":{"full":"LuxLightBinding.png","sprite":"spell6.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuxPrismaticWave","name":"Prismatic Barrier","description":"Lux throws her wand and bends the light around any friendly target it touches, protecting them from enemy damage.","tooltip":"Throws Lux's wand in target direction protecting her and all allied Champions it touches from {{ e2 }} (+{{ a1 }}) damage for 3 seconds.

    Upon reaching its destination it returns to Lux, protecting her and other allied Champions it touches from another {{ e2 }} (+{{ a1 }}) damage for 3 seconds.","leveltip":{"label":["Shield","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[2,4,6,8,10],[50,65,80,95,110],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2/4/6/8/10","50/65/80/95/110","3","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"LuxPrismaticWave.png","sprite":"spell6.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuxLightStrikeKugel","name":"Lucent Singularity","description":"Fires an anomaly of twisted light to an area, which slows nearby enemies. Lux can detonate it to damage enemies in the area of effect.","tooltip":"Creates a zone that slows enemies by {{ e1 }}%. After {{ e3 }} seconds the zone detonates dealing {{ e2 }} (+{{ a1 }}) magic damage.

    Activate again to detonate early.","leveltip":{"label":["Damage","Slow Amount","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[70,85,100,115,130],"costBurn":"70/85/100/115/130","effect":[null,[25,30,35,40,45],[60,105,150,195,240],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/30/35/40/45","60/105/150/195/240","5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"LuxLightStrikeKugel.png","sprite":"spell6.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"LuxMaliceCannon","name":"Final Spark","description":"After gathering energy, Lux fires a beam of light that deals damage to all targets in the area. If Final Spark kills a champion, part of its cooldown is refunded. In addition, triggers Lux's passive ability and refreshes the Illumination debuff duration.","tooltip":"Fires a dazzling ray of light dealing {{ e1 }} (+{{ a1 }}) magic damage to all enemies in a line. If Final Spark kills an enemy champion, {{ e3 }}% of its cooldown is refunded.

    Final Spark ignites and refreshes the Illumination debuff. ","leveltip":{"label":["Damage","Cooldown Refund","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[80,65,50],"cooldownBurn":"80/65/50","cost":[100,100,100],"costBurn":"100","effect":[null,[300,400,500],[1,1,1],[10,30,50],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"300/400/500","1","10/30/50","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[3340,3340,3340],"rangeBurn":"3340","image":{"full":"LuxMaliceCannon.png","sprite":"spell6.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Malphite":{"id":54,"key":"Malphite","name":"Malphite","title":"Shard of the Monolith","spells":[{"id":"SeismicShard","name":"Seismic Shard","description":"Using his primal elemental magic, Malphite sends a shard of the earth through the ground at his foe, dealing damage upon impact and stealing Movement Speed for 4 seconds.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage and steals {{ e2 }}% Movement Speed from the target for {{ e3 }} seconds.","leveltip":{"label":["Damage","Movement Speed","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[70,120,170,220,270],[14,17,20,23,26],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/120/170/220/270","14/17/20/23/26","4","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"SeismicShard.png","sprite":"spell6.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Obduracy","name":"Brutal Strikes","description":"Malphite starts to hit with such force that his attacks deal damage to all units in front of him. Passively increases his Armor.","tooltip":"Passive: Malphite's armor is increased by {{ e1 }}% ({{ f1 }}).

    Active: Basic attacks deal an additional {{ e2 }} (+{{ f2 }}) (+{{ a1 }}) physical damage to the target and nearby enemies. Lasts {{ e3 }} seconds.","leveltip":{"label":["Armor","Damage"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[25,25,25,25,25],"costBurn":"25","effect":[null,[15,20,25,30,35],[15,30,45,60,75],[6,6,6,6,6],[225,225,225,225,225],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","15/30/45/60/75","6","225","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"Obduracy.png","sprite":"spell6.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Landslide","name":"Ground Slam","description":"Malphite slams the ground, sending out a shockwave that deals magic damage based on his Armor as damage and reduces the Attack Speed of enemies for a short duration.","tooltip":"Malphite slams the ground dealing {{ e2 }} (+{{ f1 }}) (+{{ a1 }}) magic damage to surrounding enemies, reducing their Attack Speed by {{ e1 }}% for {{ e4 }} seconds.

    This ability gains damage equal to {{ e3 }}% of Malphite's Armor.","leveltip":{"label":["Damage","Attack Speed Reduction","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[30,35,40,45,50],[60,95,130,165,200],[40,40,40,40,40],[3,3,3,3,3],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/35/40/45/50","60/95/130/165/200","40","3","6","0","0","0","0","0"],"vars":[{"link":"armor","coeff":0.3,"key":"f1"},{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"Landslide.png","sprite":"spell6.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UFSlash","name":"Unstoppable Force","description":"Malphite ferociously charges to a location, damaging enemies and knocking them into the air.","tooltip":"Malphite charges to target area. Upon his arrival he deals {{ e2 }} (+{{ a1 }}) magic damage to nearby enemies and knocks them into the air for {{ e3 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,115,100],"cooldownBurn":"130/115/100","cost":[100,100,100],"costBurn":"100","effect":[null,[1.5,1.75,2],[200,300,400],[1.5,1.5,1.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1.5/1.75/2","200/300/400","1.5","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"UFSlash.png","sprite":"spell7.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Malzahar":{"id":90,"key":"Malzahar","name":"Malzahar","title":"the Prophet of the Void","spells":[{"id":"MalzaharQ","name":"Call of the Void","description":"Malzahar opens up two portals to the Void. After a short delay, they fire projectiles that deal Magic Damage and silence enemy champions.","tooltip":"Malzahar opens two portals to the Void that fire projectiles inward, dealing {{ e1 }} (+{{ a1 }}) magic damage and silencing enemies hit for {{ e2 }} second(s).","leveltip":{"label":["Damage","Silence Duration"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[70,110,150,190,230],[1,1.25,1.5,1.75,2],[0.4,0.4,0.4,0.4,0.4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","1/1.25/1.5/1.75/2","0.4","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"MalzaharQ.png","sprite":"spell7.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MalzaharW","name":"Void Swarm","description":"Malzahar summons Voidlings to attack nearby enemies.","tooltip":"Passive: Casting Malzahar's other spells gives him Gathering Swarm, increasing the number of Voidlings summoned by Void Swarm (max {{ e6 }}).

    Active: Summons one or more Voidlings. Voidlings last {{ e1 }} seconds and deal an additional {{ e2 }} (+{{ a1 }}) (+{{ a2 }}) magic damage each hit.

    Voidlings deal {{ e3 }}% damage to lane minions affected by Malefic Visions.
    Voidlings deal {{ e4 }}% damage to epic monsters.
    ","leveltip":{"label":["Voidling Bonus Damage","Voidling Duration","Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[8,8,9,9,10],[12,14,16,18,20],[300,300,300,300,300],[50,50,50,50,50],[25000,25000,25000,25000,25000],[2,2,2,2,2],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"8/8/9/9/10","12/14/16/18/20","300","50","25000","2","0.5","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"bonusattackdamage","coeff":0.4,"key":"a2"}],"costType":" Mana","maxammo":"0","range":[150,150,150,150,150],"rangeBurn":"150","image":{"full":"MalzaharW.png","sprite":"spell7.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MalzaharE","name":"Malefic Visions","description":"Malzahar infects his target's mind with cruel visions of their demise, dealing damage over time. Using Malzahar's other spells on the target will refresh the visions.

    If the target dies while afflicted by the visions, they pass on to a nearby enemy unit and Malzahar gains Mana. Malzahar's Voidlings are attracted to affected units.","tooltip":"Deal {{ e1 }} (+{{ a1 }}) magic damage to an enemy target over {{ e3 }} seconds. Applying Call of the Void or Nether Grasp to the victim during this time refreshes the visions.

    If the victim is killed, Malzahar gains {{ f1 }} Mana ({{ e5 }}% of max mana) and the visions spread to the nearest enemy.","leveltip":{"label":["Base Damage","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[15,13,11,9,7],"cooldownBurn":"15/13/11/9/7","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[80,115,150,185,220],[8,8,8,8,8],[4,4,4,4,4],[8,8,8,8,8],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/115/150/185/220","8","4","8","2","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"MalzaharE.png","sprite":"spell7.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MalzaharR","name":"Nether Grasp","description":"Malzahar channels the essence of the Void to suppress an enemy champion over a zone of damaging negative energy.","tooltip":"Malzahar suppresses a target champion for {{ e4 }} seconds, dealing {{ e7 }} (+{{ a2 }}) magic damage over the duration. A zone of negative energy is created around his target for {{ e3 }} seconds, dealing {{ e1 }}% (+{{ a1 }}%) of nearby enemies max health as magic damage per second.","leveltip":{"label":["Nether Grasp Damage","Null Zone Damage","Cooldown"],"effect":["{{ e7 }} -> {{ e7NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[2,3,4],[120,100,80],[5,5,5],[2.5,2.5,2.5],[120,120,120],[10,10,10],[125,250,375],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"2/3/4","120/100/80","5","2.5","120","10","125/250/375","0","0","0"],"vars":[{"link":"spelldamage","coeff":1.15,"key":"a2"},{"link":"spelldamage","coeff":0.005,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700],"rangeBurn":"700","image":{"full":"MalzaharR.png","sprite":"spell7.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Maokai":{"id":57,"key":"Maokai","name":"Maokai","title":"the Twisted Treant","spells":[{"id":"MaokaiQ","name":"Bramble Smash","description":"Maokai knocks back nearby enemies with a shockwave, dealing magic damage and slowing them.","tooltip":"Maokai smashes his fist into the ground, releasing a shockwave. Nearby enemies are knocked away from him and all affected enemies take {{ e1 }} (+{{ a1 }}) magic damage and are slowed briefly.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7.25,6.5,5.75,5],"cooldownBurn":"8/7.25/6.5/5.75/5","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[70,115,160,205,250],[325,325,325,325,325],[99,99,99,99,99],[0.25,0.25,0.25,0.25,0.25],[750,750,750,750,750],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","325","99","0.25","750","300","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"MaokaiQ.png","sprite":"spell7.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MaokaiW","name":"Twisted Advance","description":"Maokai contorts into a mass of moving roots, becoming untargetable and dashing to the target. Upon arrival, he roots the target.","tooltip":"Maokai transforms into a moving mass of roots, becoming untargetable and dashing to the target.

    Upon arrival, he deals {{ e1 }} (+{{ a1 }}) magic damage and roots the target for {{ e2 }} second(s).","leveltip":{"label":["Damage","Root Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[50,75,100,125,150],[1,1.1,1.2,1.3,1.4],[0,0,0,0,0],[0,0,0,0,0],[1300,1300,1300,1300,1300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","1/1.1/1.2/1.3/1.4","0","0","1300","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[525,525,525,525,525],"rangeBurn":"525","image":{"full":"MaokaiW.png","sprite":"spell7.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MaokaiE","name":"Sapling Toss","description":"Maokai flings a sapling to the target area to stand guard. More effective in brush.","tooltip":"Maokai flings a sapling, which stands watch for {{ f1 }} seconds. Saplings will chase nearby enemies, detonating on proximity, dealing {{ e1 }} +{{ e8 }}% [+{{ a1 }}%] Target Max Health magic damage and slowing enemies struck by {{ e5 }}% for {{ e6 }} seconds.

    Saplings placed in brush last for {{ f2 }} seconds and cause a larger explosion, dealing double damage over {{ e6 }} seconds to all enemies hit.

    Maximum {{ e4 }} Damage to non-Champions, doubled for brush.","leveltip":{"label":["Base Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,11,11,11,11],"cooldownBurn":"11","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[25,50,75,100,125],[0.5,0.5,0.5,0.5,0.5],[2,2,2,2,2],[300,300,300,300,300],[35,35,35,35,35],[2,2,2,2,2],[2,2,2,2,2],[8,8,8,8,8],[0,0,0,0,0],[550,550,550,550,550]],"effectBurn":[null,"25/50/75/100/125","0.5","2","300","35","2","2","8","0","550"],"vars":[{"link":"spelldamage","coeff":0.01,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"MaokaiE.png","sprite":"spell7.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MaokaiR","name":"Nature's Grasp","description":"Maokai summons a colossal wall of brambles and thorns that slowly advances forwards, damaging and rooting any enemies in the path.","tooltip":"Maokai summons a colossal wall of brambles and thorns that slowly advances forwards, dealing {{ e1 }} (+{{ a1 }}) magic damage and rooting any enemies struck for ({{ e2 }} to {{ e2 }}) seconds, increasing with distance travelled.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,225,300],[2.4,2.4,2.4],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/225/300","2.4","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[2500,2500,2500],"rangeBurn":"2500","image":{"full":"MaokaiR.png","sprite":"spell7.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"MasterYi":{"id":11,"key":"MasterYi","name":"Master Yi","title":"the Wuju Bladesman","spells":[{"id":"AlphaStrike","name":"Alpha Strike","description":"Master Yi teleports across the battlefield with blinding speed, dealing physical damage to multiple units in his path, while simultaneously becoming untargetable. Alpha Strike can critically strike and deals bonus physical damage to minions and monsters. Basic attacks reduce Alpha Strike's cooldown.","tooltip":"Master Yi teleports to strike up to {{ e8 }} enemies, dealing {{ e1 }} (+{{ a1 }}) physical damage, with an additional {{ e3 }} damage to minions and monsters. During Alpha Strike Master Yi is untargetable.

    Alpha Strike can critically strike, dealing an additional {{ f1 }} physical damage. Basic attacks lower the cooldown of Alpha Strike by {{ e7 }} second.","leveltip":{"label":["Damage","Bonus Minion/Monster Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[25,60,95,130,165],[50,50,50,50,50],[75,100,125,150,175],[1,1,1,1,1],[0.25,0.25,0.25,0.25,0.25],[0.6,0.6,0.6,0.6,0.6],[1,1,1,1,1],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/60/95/130/165","50","75/100/125/150/175","1","0.25","0.6","1","4","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"},{"link":"attackdamage","coeff":0.6,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"AlphaStrike.png","sprite":"spell7.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Meditate","name":"Meditate","description":"Master Yi rejuvenates his body by focus of mind, restoring Health and taking reduced damage for a short time. In addition, Master Yi will gain stacks of Double Strike and pause the remaining duration on Wuju Style and Highlander for each second he channels.","tooltip":"Master Yi channels, restoring {{ e1 }} (+{{ a1 }}) Health per second for 4 seconds. This healing is increased by {{ e4 }}% for every {{ e5 }}% of Master Yi's missing Health.

    While channeling, Master Yi reduces incoming damage by {{ e3 }}%. This damage reduction is halved against turrets.

    In addition, Master Yi will gain stacks of Double Strike and pause the remaining duration on Wuju Style and Highlander for each second he channels.","leveltip":{"label":["Damage Reduction","Health Restored"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[35,35,35,35,35],"cooldownBurn":"35","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[30,50,70,90,110],[100,150,200,250,300],[50,55,60,65,70],[1,1,1,1,1],[1,1,1,1,1],[50,50,50,50,50],[4,4,4,4,4],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","100/150/200/250/300","50/55/60/65/70","1","1","50","4","0.5","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"Meditate.png","sprite":"spell7.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"WujuStyle","name":"Wuju Style","description":"Master Yi becomes skilled in the art of Wuju, passively increasing his Attack Damage. Activating Wuju Style grants bonus true damage on basic attacks, but the passive bonus is then lost while on cooldown.","tooltip":"Passive: Grants {{ e1 }}% ({{ f1 }}) Attack Damage.

    Active: Basic attacks deal {{ e3 }} (+{{ f2 }}) bonus true damage for {{ e5 }} seconds. Afterwards the passive bonus is lost while Wuju Style is on cooldown.","leveltip":{"label":["Active Damage","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,10,10,10,10],[25,25,25,25,25],[14,23,32,41,50],[20,30,40,50,60],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10","25","14/23/32/41/50","20/30/40/50/60","5","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.1,0.125,0.15,0.175,0.2],"key":"f2"}],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"WujuStyle.png","sprite":"spell7.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"Highlander","name":"Highlander","description":"Master Yi moves with unparalleled agility, temporarily increasing his Movement and Attack Speeds as well as making him immune to all slowing effects. While active, Champion kills or assists extend Highlander's duration. Passively reduces cooldown for his other abilities on a kill or assist.","tooltip":"Passive: Champion kills and assists reduce the remaining cooldown of Master Yi's basic abilities by {{ e5 }}%.

    Active: Increases Movement Speed by {{ e3 }}%, Attack Speed by {{ e2 }}%, and grants immunity to slows for {{ e1 }} seconds. While active, champion kills and assists extend the duration of Highlander by {{ e4 }} seconds.","leveltip":{"label":["Attack Speed","Movement Speed"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":3,"cooldown":[85,85,85],"cooldownBurn":"85","cost":[100,100,100],"costBurn":"100","effect":[null,[7,7,7],[30,55,80],[25,35,45],[7,7,7],[70,70,70],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"7","30/55/80","25/35/45","7","70","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1,1,1],"rangeBurn":"1","image":{"full":"Highlander.png","sprite":"spell7.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"MissFortune":{"id":21,"key":"MissFortune","name":"Miss Fortune","title":"the Bounty Hunter","spells":[{"id":"MissFortuneRicochetShot","name":"Double Up","description":"Miss Fortune fires a bullet at an enemy, damaging them and a target behind them. Both strikes can also apply Love Tap.","tooltip":"Miss Fortune fires a bouncing shot through an enemy, dealing {{ e2 }} (+{{ a2 }}) (+{{ a1 }}) physical damage to each target hit. Both apply on-hit effects.

    The second shot can critically strike for {{ f1 }}% damage, and it always critically strikes if the first shot kills its target.","leveltip":{"label":["Mana Cost","Cooldown","Damage"],"effect":["{{ cost }} -> {{ costNL }} ","{{ cooldown }} -> {{ cooldownNL }} ","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[7,6,5,4,3],"cooldownBurn":"7/6/5/4/3","cost":[43,46,49,52,55],"costBurn":"43/46/49/52/55","effect":[null,[1,1,1,1,1],[20,40,60,80,100],[1,1,1,1,1],[40,70,100,130,160],[40,40,40,40,40],[20,20,20,20,20],[40,40,40,40,40],[110,110,110,110,110],[160,160,160,160,160],[0,0,0,0,0]],"effectBurn":[null,"1","20/40/60/80/100","1","40/70/100/130/160","40","20","40","110","160","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.35,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"MissFortuneRicochetShot.png","sprite":"spell7.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MissFortuneViciousStrikes","name":"Strut","description":"Miss Fortune passively gains Movement Speed when not attacked. This ability can be activated to grant bonus Attack Speed for a short duration. While it's on cooldown, Love Taps reduce the remaining cooldown of Strut.","tooltip":"Passive: After 5 seconds of not taking direct damage, Miss Fortune gains {{ e5 }} Movement Speed. After another 5 seconds, this bonus increases to {{ e2 }}.

    Active: Fully activates Strut's Movement Speed and grants {{ e1 }}% Attack Speed for {{ e3 }} seconds.

    Love Taps reduce the cooldown of Strut by {{ f2 }} seconds.","leveltip":{"label":["Max Movement Speed","Attack Speed"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[40,55,70,85,100],[60,70,80,90,100],[4,4,4,4,4],[2,2,2,2,2],[25,25,25,25,25],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/55/70/85/100","60/70/80/90/100","4","2","25","1","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"MissFortuneViciousStrikes.png","sprite":"spell7.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MissFortuneScattershot","name":"Make It Rain","description":"Miss Fortune reveals an area with a flurry of bullets, dealing waves of damage to opponents and slowing them.","tooltip":"Miss Fortune reveals an area, raining down bullets that deal {{ e1 }} (+{{ a1 }}) magic damage over 2 seconds and slow enemies hit by {{ e2 }}%.","leveltip":{"label":["Cooldown","Damage","Slow"],"effect":["{{ cooldown }} -> {{ cooldownNL }} ","{{ e1 }} -> {{ e1NL }} ","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[80,115,150,185,220],[28,36,44,52,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/115/150/185/220","28/36/44/52/60","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"MissFortuneScattershot.png","sprite":"spell7.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MissFortuneBulletTime","name":"Bullet Time","description":"Miss Fortune channels a flurry of bullets into a cone in front of her, dealing large amounts of damage to enemies. Each wave of Bullet Time can critically strike.","tooltip":"Miss Fortune channels a barrage of bullets for {{ e3 }} seconds, dealing (+{{ f1 }}) (+{{ a1 }}) physical damage per wave ({{ e2 }} waves total).

    Each wave of Bullet Time can critically strike for {{ f3 }}% damage.

    Total Damage: {{ f2 }}","leveltip":{"label":["Cooldown","Waves Fired"],"effect":["{{ cooldown }} -> {{ cooldownNL }} ","{{ e2 }} -> {{ e2NL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[0,0,0],[12,14,16],[3,3,3],[75,75,75],[120,120,120],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"0","12/14/16","3","75","120","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.35,"key":"f1"},{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400],"rangeBurn":"400","image":{"full":"MissFortuneBulletTime.png","sprite":"spell7.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"MonkeyKing":{"id":62,"key":"MonkeyKing","name":"Wukong","title":"the Monkey King","spells":[{"id":"MonkeyKingDoubleAttack","name":"Crushing Blow","description":"Wukong's next attack deals additional physical damage, gains range, and reduces the enemy's Armor for a short duration.","tooltip":"Wukong's next attack gains {{ e4 }} range, deals {{ e1 }} (+{{ a1 }}) bonus physical damage and reduces the enemy's Armor by {{ e2 }}% for {{ e3 }} seconds.","leveltip":{"label":["Bonus Damage","Armor Reduction","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} ->{{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[30,60,90,120,150],[10,15,20,25,30],[3,3,3,3,3],[125,125,125,125,125],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/60/90/120/150","10/15/20/25/30","3","125","6","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"MonkeyKingDoubleAttack.png","sprite":"spell7.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MonkeyKingDecoy","name":"Decoy","description":"Wukong becomes Invisible for a short duration, leaving behind a decoy that will deal Magic Damage to enemies near it when Wukong's stealth expires.","tooltip":"Wukong becomes Invisible for {{ e2 }} seconds, leaving behind a decoy that will deal {{ e1 }} (+{{ a1 }}) Magic Damage to enemies near it after {{ e2 }} seconds.

    16","leveltip":{"label":["Base Damage","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} ->{{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[70,115,160,205,250],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","1.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"MonkeyKingDecoy.png","sprite":"spell7.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MonkeyKingNimbus","name":"Nimbus Strike","description":"Wukong dashes to target enemy and sends out images to attack up to 2 additional enemies near his target, dealing physical damage to each enemy struck.","tooltip":"Wukong dashes to target enemy and sends out images to attack up to 2 additional enemies nearby. Each enemy struck takes {{ e1 }} (+{{ a1 }}) physical damage. Wukong then gains {{ e2 }}% Attack Speed for {{ e3 }} seconds.","leveltip":{"label":["Base Damage","Attack Speed","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} ->{{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[60,105,150,195,240],[30,35,40,45,50],[4,4,4,4,4],[1050,1050,1050,1050,1050],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","30/35/40/45/50","4","1050","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"MonkeyKingNimbus.png","sprite":"spell7.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MonkeyKingSpinToWin","name":"Cyclone","description":"Wukong's staff grows outward and he spins it around, dealing damage and knocking up enemies. Wukong gains Movement Speed over the duration of the spell.","tooltip":"Wukong's staff grows outward and he spins it around for 4 seconds, dealing {{ e1 }} (+{{ a1 }}) physical damage per second to nearby enemies, knocking them up for {{ e4 }} second the first time they get hit.

    At the start, Wukong gains {{ e2 }}% Movement Speed, and he gains an additional {{ e3 }}% per second over the duration.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} ->{{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[100,100,100],"costBurn":"100","effect":[null,[20,110,200],[15,15,15],[10,10,10],[1,1,1],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"20/110/200","15","10","1","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[315,315,315],"rangeBurn":"315","image":{"full":"MonkeyKingSpinToWin.png","sprite":"spell7.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Mordekaiser":{"id":82,"key":"Mordekaiser","name":"Mordekaiser","title":"the Iron Revenant","spells":[{"id":"MordekaiserMaceOfSpades","name":"Mace of Spades","description":"Mordekaiser's next three attacks deal escalating bonus damage.","tooltip":"Mordekaiser's next three hits are empowered. The first two strikes deal {{ e3 }} (+{{ f1 }}) (+{{ f2 }}) bonus magic damage. The final strike deals {{ e9 }} times the bonus damage of the previous strikes, up to {{ f5 }} (+{{ f3 }}) (+{{ f4 }}).","leveltip":{"label":["Bonus Damage","Attack Damage Ratio","Cooldown","Health Cost"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e0 }} -> {{ e0NL }}"]},"maxrank":5,"cooldown":[10,8.5,7,5.5,4],"cooldownBurn":"10/8.5/7/5.5/4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[132,181.5,231,280.5,330],[10,15,20,25,30],[10,20,30,40,50],[4,4,4,4,4],[30,60,90,120,150],[20,225,250,275,300],[0.5,0.6,0.7,0.8,0.9],[0.6,0.6,0.6,0.6,0.6],[2,2,2,2,2],[20,23,26,29,32]],"effectBurn":[null,"132/181.5/231/280.5/330","10/15/20/25/30","10/20/30/40/50","4","30/60/90/120/150","20/225/250/275/300","0.5/0.6/0.7/0.8/0.9","0.6","2","20/23/26/29/32"],"vars":[{"link":"bonusattackdamage","coeff":1.65,"key":"f1"},{"link":"bonusattackdamage","coeff":1,"key":"f2"}],"costType":" Health","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"MordekaiserMaceOfSpades.png","sprite":"spell7.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ e0 }} Health"},{"id":"MordekaiserCreepingDeathCast","name":"Harvesters of Sorrow","description":"Coats an ally in magnetic metal, increasing each unit's movement speed toward one another. While near one another, the metal swirls violently dealing damage per second to enemies.","tooltip":"18","leveltip":{"label":["Damage","Reactivation Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e6 }} -> {{ e6NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,30,40,50,60],[140,180,220,260,300],[25,35,45,55,65],[1,1,1,1,1],[75,75,75,75,75],[50,85,120,155,190],[25,25,25,25,25],[2,2,2,2,2],[50,50,50,50,50],[0,0,0,0,0]],"effectBurn":[null,"20/30/40/50/60","140/180/220/260/300","25/35/45/55/65","1","75","50/85/120/155/190","25","2","50","0"],"vars":[],"costType":" Health","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"MordekaiserCreepingDeathCast.png","sprite":"spell7.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ e3 }} Health"},{"id":"MordekaiserSyphonOfDestruction","name":"Siphon of Destruction","description":"Mordekaiser deals damage to enemies in a cone in front of him. For each unit hit, Mordekaiser's shield absorbs energy.","tooltip":"Mordekaiser deals {{ e3 }} (+{{ a1 }}) (+{{ a1 }}) magic damage in a cone.

    For each Champion hit, he gains {{ e5 }}% maximum shield.","leveltip":{"label":["Damage","Cooldown","% Max Shield From Champions","Health Cost"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e5 }}% -> {{ e5NL }}%","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[6,5.75,5.5,5.25,5],"cooldownBurn":"6/5.75/5.5/5.25/5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[24,36,48,60,72],[1,2,3,4,5],[35,65,95,125,155],[4,4,4,4,4],[15,17.5,20,22.5,25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"24/36/48/60/72","1/2/3/4/5","35/65/95/125/155","4","15/17.5/20/22.5/25","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.6,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Health","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"MordekaiserSyphonOfDestruction.png","sprite":"spell7.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ e1 }} Health"},{"id":"MordekaiserChildrenOfTheGrave","name":"Children of the Grave","description":"Mordekaiser curses an enemy champion or the Dragon, stealing a percent of their life initially and each second. If the target dies while the spell is active, their soul is enslaved and will follow Mordekaiser as a ghost.","tooltip":"18","leveltip":{"label":["Total Health Stolen","Cooldown","Ghost Lifetime"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ e9 }} -> {{ e9NL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[0,0,0],"costBurn":"0","effect":[null,[25,30,35],[55,65,75],[75,150,225],[2,2,2],[3,3,3],[10,25,50],[30,30,30],[25,25,25],[45,60,75],[2000,2000,2000]],"effectBurn":[null,"25/30/35","55/65/75","75/150/225","2","3","10/25/50","30","25","45/60/75","2000"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"MordekaiserChildrenOfTheGrave.png","sprite":"spell7.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Morgana":{"id":25,"key":"Morgana","name":"Morgana","title":"Fallen Angel","spells":[{"id":"DarkBindingMissile","name":"Dark Binding","description":"Morgana releases a sphere of dark magic. Upon contact with an enemy unit, the sphere will deal magic damage and force the unit to the ground for a period of time.","tooltip":"Fires a bolt of dark energy, rooting the first enemy hit for {{ e2 }} seconds and dealing {{ e1 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Root Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,11,11,11,11],"cooldownBurn":"11","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[80,135,190,245,300],[2,2.25,2.5,2.75,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/135/190/245/300","2/2.25/2.5/2.75/3","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1175,1175,1175,1175,1175],"rangeBurn":"1175","image":{"full":"DarkBindingMissile.png","sprite":"spell7.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TormentedSoil","name":"Tormented Soil","description":"Infects an area with desecrated soil, causing enemy units who stand on the location to take continual damage.","tooltip":"Curses an area for {{ e7 }} seconds. Enemies on the cursed ground are dealt
    from {{ e5 }} (+{{ a1 }}) to {{ e6 }} (+{{ a2 }}) magic damage every second based on the enemy's missing Health.","leveltip":{"label":["Damage ","Mana Cost"],"effect":["{{ e5 }} -> {{ e5NL }} ","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[70,85,100,115,130],"costBurn":"70/85/100/115/130","effect":[null,[8,16,24,32,40],[0.5,0.5,0.5,0.5,0.5],[280,280,280,280,280],[0,0,0,0,0],[16,32,48,64,80],[24,48,72,96,120],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"8/16/24/32/40","0.5","280","0","16/32/48/64/80","24/48/72/96/120","5","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.22,"key":"a1"},{"link":"spelldamage","coeff":0.33,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"TormentedSoil.png","sprite":"spell7.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BlackShield","name":"Black Shield","description":"Places a protective barrier around an allied champion, absorbing magical damage and disables until penetrated or the shield dissipates.","tooltip":"Shields an allied Champion for {{ e2 }} seconds. The shield absorbs {{ e1 }} (+{{ a1 }}) magic damage and prevents disables until it breaks.","leveltip":{"label":["Shield Health","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[23,21,19,17,15],"cooldownBurn":"23/21/19/17/15","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[70,140,210,280,350],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/140/210/280/350","5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"BlackShield.png","sprite":"spell7.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SoulShackles","name":"Soul Shackles","description":"Latches chains of energy onto nearby enemy champions, dealing initial damage to them and slowing their Movement Speed, and then echoing the pain a few seconds later and stunning them if they remain close to Morgana.","tooltip":"Dark chains latch onto nearby enemy Champions dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing them by {{ e4 }}% for {{ e3 }} seconds, granting True Sight. After {{ e3 }} seconds, they are dealt an additional {{ e1 }} (+{{ a1 }}) magic damage and are stunned for {{ e2 }} seconds.

    Enemy Champions can break the chains by moving away from Morgana.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,225,300],[1.5,1.5,1.5],[3,3,3],[20,20,20],[575,575,575],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/225/300","1.5","3","20","575","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625],"rangeBurn":"625","image":{"full":"SoulShackles.png","sprite":"spell7.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Nami":{"id":267,"key":"Nami","name":"Nami","title":"the Tidecaller","spells":[{"id":"NamiQ","name":"Aqua Prison","description":"Sends a bubble to a target area, dealing damage and stunning all enemies on impact.","tooltip":"Sends a bubble to target area, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies, and stunning them for {{ e2 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[75,130,185,240,295],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/130/185/240/295","1.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[875,875,875,875,875],"rangeBurn":"875","image":{"full":"NamiQ.png","sprite":"spell7.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NamiW","name":"Ebb and Flow","description":"Unleashes a stream of water that bounces back and forth between allied and enemy champions, healing allies and damaging enemies.","tooltip":"Unleashes a stream of water that bounces back and forth between allied and enemy champions.

    On Ally Hit: Heals {{ e3 }} (+{{ a2 }}) Health and will bounce to a nearby enemy champion.

    On Enemy Hit: Deals {{ e1 }} (+{{ a1 }}) magic damage and bounces to a nearby allied champion.

    Bounces to each target only once, and hits up to {{ e2 }} targets. The damage and healing value is modified by ({{ f1 }}%) each bounce. ","leveltip":{"label":["Heal","Damage","Mana Cost"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[70,85,100,115,130],"costBurn":"70/85/100/115/130","effect":[null,[70,110,150,190,230],[3,3,3,3,3],[65,95,125,155,185],[15,15,15,15,15],[0.075,0.075,0.075,0.075,0.075],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","3","65/95/125/155/185","15","0.08","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"NamiW.png","sprite":"spell7.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NamiE","name":"Tidecaller's Blessing","description":"Empowers an allied champion for a short duration. The ally's basic attacks deal bonus magic damage and slow the target.","tooltip":"Empowers an allied champion's next {{ e4 }} basic attacks, causing them to slow the target by {{ e2 }}% (+{{ a2 }}%) for {{ e3 }} second and deal {{ e1 }} (+{{ a1 }}) bonus magic damage. Lasts for {{ e5 }} seconds.","leveltip":{"label":["Damage","Slow","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,11,11,11,11],"cooldownBurn":"11","cost":[55,60,65,70,75],"costBurn":"55/60/65/70/75","effect":[null,[25,40,55,70,85],[15,20,25,30,35],[1,1,1,1,1],[3,3,3,3,3],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/40/55/70/85","15/20/25/30/35","1","3","6","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.05,"key":"a2"},{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"NamiE.png","sprite":"spell7.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NamiR","name":"Tidal Wave","description":"Summons a massive Tidal Wave that knocks up, slows, and damages enemies. Allies hit gain double the effect of Surging Tides.","tooltip":"Summons a Tidal Wave from Nami's position. The wave knocks up enemies and slows them by {{ e4 }}%, dealing {{ e1 }} (+{{ a1 }}) magic damage. The duration of the slow increases based on how far the Tidal Wave has traveled, with a minimum duration of {{ e3 }} seconds and a maximum of {{ e5 }} seconds.

    Allies hit by the wave gain double the effect of Surging Tides.","leveltip":{"label":["Damage","Cooldown","Slow"],"effect":[" "," {{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[0.5,0.5,0.5],[2,2,2],[50,60,70],[4,4,4],[0.002,0.002,0.002],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","0.5","2","50/60/70","4","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[2550,2550,2550],"rangeBurn":"2550","image":{"full":"NamiR.png","sprite":"spell7.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Nasus":{"id":75,"key":"Nasus","name":"Nasus","title":"the Curator of the Sands","spells":[{"id":"NasusQ","name":"Siphoning Strike","description":"Nasus strikes his foe, dealing damage and increasing the power of his future Siphoning Strikes if he slays his target.","tooltip":"Nasus's next basic attack will deal {{ e1 }} (+{{ a2 }}) (+{{ f1 }}) physical damage.

    Siphoning Strike permanently gains {{ e2 }} damage if it kills an enemy. This bonus is doubled against Champions, large minions and large monsters.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7,6,5,4],"cooldownBurn":"8/7/6/5/4","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[30,50,70,90,110],[3,3,3,3,3],[25,25,25,25,25],[6,6,6,6,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","3","25","6","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"@stacks","coeff":3,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"NasusQ.png","sprite":"spell7.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NasusW","name":"Wither","description":"Nasus ages an enemy champion, decelerating their Movement and Attack Speeds over time.","tooltip":"Nasus ages target champion over {{ e3 }} seconds, slowing their Movement Speed by {{ e2 }}%, increasing to {{ e1 }}% over the duration. The target's Attack Speed is reduced by half the amount.","leveltip":{"label":["Max Slow","Cooldown"],"effect":[" {{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[47,59,71,83,95],[35,35,35,35,35],[5,5,5,5,5],[17.5,17.5,17.5,17.5,17.5],[1.5,3,4.5,6,7.5],[3,6,9,12,15],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"47/59/71/83/95","35","5","17.5","1.5/3/4.5/6/7.5","3/6/9/12/15","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"NasusW.png","sprite":"spell7.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NasusE","name":"Spirit Fire","description":"Nasus unleashes a spirit flame at a location, dealing damage and reducing the Armor of enemies who stand on it.","tooltip":"Nasus unleashes a spirit flame at target location, dealing {{ e4 }} (+{{ a2 }}) magic damage.

    For the next {{ e3 }} seconds, enemies in the area have their Armor reduced by {{ e2 }} and are dealt an additional {{ e1 }} (+{{ a1 }}) magic damage each second.","leveltip":{"label":["Initial Damage","Damage Per Second","Armor Reduction","Mana Cost"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[70,85,100,115,130],"costBurn":"70/85/100/115/130","effect":[null,[11,19,27,35,43],[20,25,30,35,40],[5,5,5,5,5],[55,95,135,175,215],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"11/19/27/35/43","20/25/30/35/40","5","55/95/135/175/215","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a2"},{"link":"spelldamage","coeff":0.12,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"NasusE.png","sprite":"spell7.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NasusR","name":"Fury of the Sands","description":"Nasus unleashes a mighty sandstorm that batters nearby enemies. While the storm rages, he gains increased Health, Attack Range, damages nearby enemies and gains bonus Armor and Magic Resistance for the duration.","tooltip":"Nasus becomes empowered in the sandstorm for 15 seconds, increasing his maximum Health by {{ e1 }} and Armor and Magic Resistance by {{ e4 }}.

    While the storm rages, he deals {{ e3 }} (+{{ a1 }})% of nearby enemies' maximum Health each second as magic damage ({{ e5 }} damage max per second) and gains an additional {{ e8 }} Armor and Magic Resistance.","leveltip":{"label":["Bonus Health","Max Health %","Initial Armor and Magic Resistance","Incremental Armor and Magic Resistance"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ e4 }} -> {{ e4NL }}","{{ e8 }} -> {{ e8NL }}"]},"maxrank":3,"cooldown":[120,120,120],"cooldownBurn":"120","cost":[100,100,100],"costBurn":"100","effect":[null,[300,450,600],[50,50,50],[3,4,5],[15,35,55],[240,240,240],[15,15,15],[0,0,0],[1,2,3],[0,0,0],[0,0,0]],"effectBurn":[null,"300/450/600","50","3/4/5","15/35/55","240","15","0","1/2/3","0","0"],"vars":[{"link":"spelldamage","coeff":0.01,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20,20,20],"rangeBurn":"20","image":{"full":"NasusR.png","sprite":"spell7.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Nautilus":{"id":111,"key":"Nautilus","name":"Nautilus","title":"the Titan of the Depths","spells":[{"id":"NautilusAnchorDrag","name":"Dredge Line","description":"Nautilus hurls his anchor forward. If it hits a champion, he drags both himself and the opponent close together. If it hits terrain, Nautilus instead pulls himself to the anchor and the cooldown of Dredge Line is reduced by half.","tooltip":"Nautilus hurls his anchor forward. If it hits an enemy unit, Nautilus drags himself and the target together dealing {{ e1 }} (+{{ a1 }}) magic damage and stunning them briefly.

    If the anchor hits terrain, Nautilus will drag himself forward and the cooldown is reduced by {{ e3 }}% ({{ f1 }}).","leveltip":{"label":["Damage","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[60,105,150,195,240],[0,0,0,0,0],[50,50,50,50,50],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","0","50","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"},{"link":"@special.nautilusq","coeff":0.5,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"NautilusAnchorDrag.png","sprite":"spell7.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NautilusPiercingGaze","name":"Titan's Wrath","description":"Nautilus surrounds himself with dark energies, gaining a shield that blocks incoming damage. While the shield persists, his attacks apply a damage over time effect to enemies around his target.","tooltip":"Nautilus surrounds himself with dark energies for {{ e3 }} seconds, shielding him from the next {{ e1 }} (+{{ f1 }}) ({{ e2 }}% of his max Health) damage.

    While the shield persists, Nautilus' basic attacks deal {{ e4 }} (+{{ a1 }}) bonus magic damage over 2 seconds to all enemies around his target.","leveltip":{"label":["Shield Amount","Bonus Magic Damage","Percent Max Health"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[18,18,18,18,18],"cooldownBurn":"18","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[65,70,75,80,85],[9,11,13,15,17],[10,10,10,10,10],[30,40,50,60,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/70/75/80/85","9/11/13/15/17","10","30/40/50/60/70","0","0","0","0","0","0"],"vars":[{"link":"bonushealth","coeff":0.1,"key":"f1"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"NautilusPiercingGaze.png","sprite":"spell7.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NautilusSplashZone","name":"Riptide","description":"Nautilus slams the ground, causing the earth to explode around him in a set of three explosions. Each explosion damages and slows enemies.","tooltip":"Nautilus slams the ground, causing the earth to explode around him. Each explosion deals {{ e1 }} (+{{ a1 }}) magic damage to enemies in the area and slows them by {{ e2 }}% for {{ e4 }} seconds. This slow diminishes over time.

    A unit can be hit by more than one explosion, but they take {{ e3 }}% less damage from additional explosions.","leveltip":{"label":["Damage","Slow","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[55,85,115,145,175],[30,35,40,45,50],[50,50,50,50,50],[1.25,1.25,1.25,1.25,1.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"55/85/115/145/175","30/35/40/45/50","50","1.25","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"NautilusSplashZone.png","sprite":"spell7.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NautilusGrandLine","name":"Depth Charge","description":"Nautilus fires a shockwave into the earth that chases an opponent. This shockwave rips up the earth above it, knocking enemies into the air. When it reaches the opponent, the shockwave erupts, knocking his target into the air and stunning them.","tooltip":"Nautilus fires a shockwave that chases an enemy champion, dealing {{ e1 }} (+{{ a1 }}) magic damage to other enemies it passes through, knocking them into the air, and stunning them for {{ e5 }} seconds.

    The shockwave explodes upon hitting its target dealing {{ e2 }} (+{{ a2 }}) magic damage, launching them into the air and stunning them for {{ e5 }} seconds. ","leveltip":{"label":["Pass Through Damage","Explosion Damage","Stun Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,110,80],"cooldownBurn":"140/110/80","cost":[100,100,100],"costBurn":"100","effect":[null,[125,175,225],[200,325,450],[0,0,0],[0,0,0],[1,1.5,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"125/175/225","200/325/450","0","0","1/1.5/2","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.8,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[825,825,825],"rangeBurn":"825","image":{"full":"NautilusGrandLine.png","sprite":"spell8.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Nidalee":{"id":76,"key":"Nidalee","name":"Nidalee","title":"the Bestial Huntress","spells":[{"id":"JavelinToss","name":"Javelin Toss / Takedown","description":"In human form, Nidalee throws a spiked javelin at her target that gains damage as it flies. As a cougar, her next attack will attempt to fatally wound her target, dealing more damage the less life they have.","tooltip":"18","leveltip":{"label":["Javelin Minimum Damage","Javelin Maximum Damage","Javelin Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[70,85,100,115,130],[210,255,300,345,390],[0,0,0,0,0],[0,0,0,0,0],[0.4,0.4,0.4,0.4,0.4],[1.2,1.2,1.2,1.2,1.2],[525,525,525,525,525],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/85/100/115/130","210/255/300/345/390","0","0","0.4","1.2","525","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1500,1500,1500,1500,1500],"rangeBurn":"1500","image":{"full":"JavelinToss.png","sprite":"spell8.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Bushwhack","name":"Bushwhack / Pounce","description":"In human form, Nidalee lays a trap for unwary opponents that, when sprung, damages and reveals its target. As a cougar, she jumps in a direction, dealing damage in an area where she lands.","tooltip":"18","leveltip":{"label":["Trap Damage","Trap Cooldown","Trap Mana Cost"],"effect":["{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[0,0,0,0,0],[0,0,0,0,0],[4,4,4,4,4],[13,12,11,10,9],[10,20,30,40,50],[120,120,120,120,120],[40,80,120,160,200],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","0","4","13/12/11/10/9","10/20/30/40/50","120","40/80/120/160/200","0","0","0"],"vars":[],"costType":" Mana","maxammo":"0","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"Bushwhack.png","sprite":"spell8.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PrimalSurge","name":"Primal Surge / Swipe","description":"In human form, Nidalee channels the spirit of the cougar to heal her allies and imbue them with Attack Speed for a short duration. As a cougar, she claws in a direction, dealing damage to enemies in front of her.","tooltip":"18","leveltip":{"label":["Primal Surge Heal","Primal Surge Attack Speed","Primal Surge Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[60,75,90,105,120],"costBurn":"60/75/90/105/120","effect":[null,[35,55,75,95,115],[70,110,150,190,230],[0,0,0,0,0],[20,30,40,50,60],[0,0,0,0,0],[0.05,0.05,0.05,0.05,0.05],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/55/75/95/115","70/110/150/190/230","0","20/30/40/50/60","0","0.05","2","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"PrimalSurge.png","sprite":"spell8.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AspectOfTheCougar","name":"Aspect Of The Cougar","description":"Nidalee transforms into a cougar, gaining new abilities.","tooltip":"18","leveltip":{"label":["Takedown Damage","Takedown Damage Amp","Pounce Damage","Swipe Damage","Enhanced Pounce Cooldown"],"effect":["{{ f1 }} -> {{ f6 }}","{{ f4 }}% -> {{ f5 }}%","{{ f7 }} -> {{ f8 }}","{{ f9 }} -> {{ f10 }}","{{ f2 }} -> {{ f3 }}"]},"maxrank":4,"cooldown":[3,3,3,3],"cooldownBurn":"3","cost":[0,0,0,0],"costBurn":"0","effect":[null,[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[6,6,6,6],[0,0,0,0],[10,10,10,10],[0,0,0,0],[0,0,0,0]],"effectBurn":[null,"0","0","0","0","0","6","0","10","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20],"rangeBurn":"20","image":{"full":"AspectOfTheCougar.png","sprite":"spell8.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"No Cost"}]},"Nocturne":{"id":56,"key":"Nocturne","name":"Nocturne","title":"the Eternal Nightmare","spells":[{"id":"NocturneDuskbringer","name":"Duskbringer","description":"Nocturne throws a shadow blade that deals damage, leaves a Dusk Trail, and causes champions to leave a Dusk Trail. While on the trail, Nocturne can move through units and has increased Movement Speed and Attack Damage.","tooltip":"Nocturne throws a shadow blade that deals {{ e2 }} (+{{ f1 }}) physical damage and leaves a Dusk Trail for {{ e3 }} seconds. Enemy champions hit also leave a Dusk Trail.

    While on the trail, Nocturne can move through units and gains {{ e1 }}% Movement Speed and {{ e4 }} Attack Damage.","leveltip":{"label":["Damage","Movement Speed Bonus","Bonus Attack Damage","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}"," {{ e1 }}% -> {{ e1NL }}%","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[15,20,25,30,35],[60,105,150,195,240],[5,5,5,5,5],[15,25,35,45,55],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","60/105/150/195/240","5","15/25/35/45/55","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.75,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[1125,1125,1125,1125,1125],"rangeBurn":"1125","image":{"full":"NocturneDuskbringer.png","sprite":"spell8.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NocturneShroudofDarkness","name":"Shroud of Darkness","description":"Nocturne empowers his blades, passively gaining Attack Speed. Activating Shroud of Darkness allows Nocturne to fade into the shadows, creating a magical barrier which blocks a single enemy ability and doubles his passive Attack Speed if successful.","tooltip":"Passive: Nocturne gains {{ e1 }}% Attack Speed.

    Active: Nocturne creates a magical barrier for 1.5 seconds, which blocks the next enemy ability.

    If an ability is blocked by the shield, Nocturne's passive Attack Speed bonus doubles for 5 seconds.","leveltip":{"label":["Attack Speed Bonus","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[20,25,30,35,40],[0.2,0.05,0.05,0.05,0.05],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/25/30/35/40","0.2/0.05/0.05/0.05/0.05","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"NocturneShroudofDarkness.png","sprite":"spell8.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NocturneUnspeakableHorror","name":"Unspeakable Horror","description":"Nocturne plants a nightmare into his target's mind, dealing damage each second and terrifying the target if they do not get out of range by the end of the duration.","tooltip":"Passive: Nocturne gains massively increased Movement Speed toward terrified enemies.

    Active: Nocturne plants a nightmare into his target's mind, dealing {{ e1 }} (+{{ a1 }}) magic damage over {{ e3 }} seconds. If Nocturne stays within range of the target for the full duration, the target becomes terrified for {{ e2 }} second(s).","leveltip":{"label":["Damage","Terrify Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[80,125,170,215,260],[1.25,1.5,1.75,2,2.25],[2,2,2,2,2],[465,465,465,465,465],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/125/170/215/260","1.25/1.5/1.75/2/2.25","2","465","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"NocturneUnspeakableHorror.png","sprite":"spell8.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"NocturneParanoia","name":"Paranoia","description":"Nocturne reduces the sight radius of all enemy champions and removes their ally vision in the process. He can then launch himself at a nearby enemy champion.","tooltip":"Nocturne reduces the sight radius of all enemy champions and removes their ally vision for 4 seconds.

    While Paranoia is active, Nocturne can launch himself at an enemy champion, dealing {{ e3 }} (+{{ f1 }}) physical damage.","leveltip":{"label":["Damage","Range","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,125,100],"cooldownBurn":"150/125/100","cost":[100,100,100],"costBurn":"100","effect":[null,[250,250,250],[2500,3250,4000],[150,250,350],[3500,4250,5000],[1800,1800,1800],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250","2500/3250/4000","150/250/350","3500/4250/5000","1800","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.2,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[2500,3250,4000],"rangeBurn":"2500/3250/4000","image":{"full":"NocturneParanoia.png","sprite":"spell8.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Nunu":{"id":20,"key":"Nunu","name":"Nunu","title":"the Yeti Rider","spells":[{"id":"Consume","name":"Consume","description":"Nunu commands the yeti to take a bite out of a target minion or monster, dealing heavy damage to it and healing himself.","tooltip":"Nunu commands the yeti to take a bite out of a minion or monster, dealing {{ e1 }} true damage and healing himself for {{ e2 }} (+{{ a1 }}).

    Passive - Feed The Yeti: For every Large or Epic Monster Consumed, Nunu gains increased Size, {{ e0 }}% Maximum Health, and {{ e9 }} out of combat Movement Speed for the next {{ f5 }} seconds (max {{ e6 }} stacks).

    Recently Consumed monsters will not grant additional bonuses ({{ e5 }} second cooldown).","leveltip":{"label":["Damage","Health Restore","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[340,500,660,820,980],[50,100,150,200,250],[120,150,180,210,240],[0,0,0,0,0],[60,60,60,60,60],[5,5,5,5,5],[5,5,5,5,5],[50,50,50,50,50],[10,10,10,10,10],[0.02,0.02,0.02,0.02,0.02]],"effectBurn":[null,"340/500/660/820/980","50/100/150/200/250","120/150/180/210/240","0","60","5","5","50","10","0.02"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[125,125,125,125,125],"rangeBurn":"125","image":{"full":"Consume.png","sprite":"spell8.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"BloodBoil","name":"Blood Boil","description":"Nunu invigorates himself and an allied unit by heating their blood, increasing their Movement and Attack Speeds.","tooltip":"The heat of Nunu and a target ally's blood rises, increasing Movement Speed by {{ e2 }}% and Attack Speed by {{ e1 }}% for {{ e3 }} seconds.

    If Nunu targets himself, Blood Boil will try to target the nearest Ally champion.","leveltip":{"label":["Attack Speed","Movement Speed"],"effect":["{{ e1 }}% -> {{ e1NL }}%"," {{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[25,30,35,40,45],[8,9,10,11,12],[12,12,12,12,12],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/30/35/40/45","8/9/10/11/12","12","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"BloodBoil.png","sprite":"spell8.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"IceBlast","name":"Ice Blast","description":"Nunu launches a ball of ice at an enemy unit, dealing damage and temporarily slowing their Movement and Attack Speeds.","tooltip":"Nunu launches a ball of ice at an enemy unit, dealing {{ e1 }} (+{{ a1 }}) magic damage and slowing their Movement Speed by {{ e2 }}% and Attack Speed by {{ e3 }}% for {{ e4 }} seconds.","leveltip":{"label":["Damage","Movement Speed Slow","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,5.5,5,4.5,4],"cooldownBurn":"6/5.5/5/4.5/4","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[80,120,160,200,240],[40,45,50,55,60],[25,25,25,25,25],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","40/45/50/55/60","25","2","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"IceBlast.png","sprite":"spell8.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"AbsoluteZero","name":"Absolute Zero","description":"Nunu begins to sap the area of heat, slowing all nearby enemies. When Absolute Zero ends, he deals massive damage to all enemies caught in the area.","tooltip":"Nunu channels for {{ e4 }} seconds, sapping the area of heat. Nearby enemies have their Movement Speed slowed by {{ e2 }}% and their Attack Speed slowed by {{ e3 }}%. The Movement Speed slow increases to {{ e6 }}% over the duration of the channel.

    Enemies caught in the area when the channel ends take up to {{ e1 }} (+{{ a1 }}) magic damage, depending on how long Absolute Zero was channeled.
    (Minimum Damage: {{ f2 }})","leveltip":{"label":["Maximum Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,100,90],"cooldownBurn":"110/100/90","cost":[100,100,100],"costBurn":"100","effect":[null,[625,875,1125],[50,50,50],[25,25,25],[3,3,3],[3,3,3],[95,95,95],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"625/875/1125","50","25","3","3","95","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":2.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"AbsoluteZero.png","sprite":"spell8.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Olaf":{"id":2,"key":"Olaf","name":"Olaf","title":"the Berserker","spells":[{"id":"OlafAxeThrowCast","name":"Undertow","description":"Olaf throws an axe into the ground at a target location, dealing damage to enemies it passes through and slowing their Movement Speed. If Olaf picks up the axe, the ability's cooldown is reduced by 4.5 seconds.","tooltip":"Olaf throws an axe to target location, dealing {{ e1 }} (+{{ a1 }}) physical damage to units it passes through and slowing them by {{ e2 }}% for up to {{ e3 }} seconds. The further the Axe flies, the longer the slow lasts, but it is never less than {{ e4 }} seconds.

    If Olaf picks up the axe, the ability's cooldown is reduced by 4.5 seconds.","leveltip":{"label":["Base Damage","Slow %"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[70,115,160,205,250],[29,33,37,41,45],[2.5,2.5,2.5,2.5,2.5],[1.5,1.5,1.5,1.5,1.5],[800,800,800,800,800],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","29/33/37/41/45","2.5","1.5","800","400","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"OlafAxeThrowCast.png","sprite":"spell8.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"OlafFrenziedStrikes","name":"Vicious Strikes","description":"Olaf's Attack Speed is increased, he gains Life Steal and has increased healing from all sources based on how much Health he is missing.","tooltip":"For 6 seconds, Olaf gains {{ e2 }}% Life Steal and his Attack Speed is increased by {{ e1 }}%.

    During this time, Olaf also receives 1% increased healing from all sources for every {{ e3 }}% of Health he is missing.","leveltip":{"label":["Attack Speed","Life Steal"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[40,50,60,70,80],[14,16,18,20,22],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/50/60/70/80","14/16/18/20/22","2","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"OlafFrenziedStrikes.png","sprite":"spell8.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"OlafRecklessStrike","name":"Reckless Swing","description":"Olaf attacks with such force that it deals true damage to his target and himself, refunding the Health cost if he destroys the target.","tooltip":"Olaf ferociously swings his axes, dealing {{ e1 }} (+{{ a1 }}) true damage to his target. This ability's cost is equal to {{ e2 }}% of the total damage dealt, but the cost is refunded if it kills the target.

    Basic attacks lower the cooldown of Reckless Swing by 1 second.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[70,115,160,205,250],[30,30,30,30,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","30","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.4,"key":"a1"}],"costType":"Health","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"OlafRecklessStrike.png","sprite":"spell8.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"Health"},{"id":"OlafRagnarok","name":"Ragnarok","description":"Olaf temporarily becomes immune to disables.","tooltip":"Passive: Olaf gains {{ e1 }} Armor and {{ e1 }} Magic Resist.

    Active: Olaf removes all disables from himself and becomes immune to them for the next {{ e2 }} seconds. Olaf also receives a {{ e5 }}% Movement Speed bonus towards enemy champions for {{ e4 }} second. During this time, Olaf loses the passive portion of Ragnarok and gains {{ e3 }} Attack Damage.","leveltip":{"label":["Armor and Magic Resist","Attack Damage","Movement Speed Bonus","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[0,0,0],"costBurn":"0","effect":[null,[20,30,40],[6,6,6],[40,60,80],[1,1,1],[50,60,70],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"20/30/40","6","40/60/80","1","50/60/70","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[400,400,400],"rangeBurn":"400","image":{"full":"OlafRagnarok.png","sprite":"spell8.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Orianna":{"id":61,"key":"Orianna","name":"Orianna","title":"the Lady of Clockwork","spells":[{"id":"OrianaIzunaCommand","name":"Command: Attack","description":"Orianna commands her Ball to fire toward a target location, dealing magic damage to targets along the way (deals less damage to subsequent targets). Her Ball remains at the target location after.","tooltip":"Orianna commands her Ball to shoot toward a target location, dealing {{ e1 }} (+{{ a1 }}) magic damage to targets along the way. However, it deals {{ e2 }}% less damage for each unit it hits (Minimum {{ e3 }}%).

    Her Ball remains behind at the target location afterwards.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,5.25,4.5,3.75,3],"cooldownBurn":"6/5.25/4.5/3.75/3","cost":[30,35,40,45,50],"costBurn":"30/35/40/45/50","effect":[null,[60,90,120,150,180],[10,10,10,10,10],[40,40,40,40,40],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/90/120/150/180","10","40","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[815,815,815,815,815],"rangeBurn":"815","image":{"full":"OrianaIzunaCommand.png","sprite":"spell8.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"OrianaDissonanceCommand","name":"Command: Dissonance","description":"Orianna commands the Ball to release a pulse of energy, dealing magic damage around it. This leaves a field behind that speeds up allies and slows enemies.","tooltip":"Orianna commands her Ball to release an electric pulse, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies.

    The pulse leaves behind an energy field for {{ e4 }} seconds, lowering enemy Movement Speed by {{ e2 }}% and increasing ally Movement Speed by {{ e3 }}% for {{ e5 }} seconds. This effect diminishes over time.","leveltip":{"label":["Damage","Movement Speed Bonus","Slow Amount","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[70,115,160,205,250],[20,25,30,35,40],[20,25,30,35,40],[3,3,3,3,3],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","20/25/30/35/40","20/25/30/35/40","3","2","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[255,255,255,255,255],"rangeBurn":"255","image":{"full":"OrianaDissonanceCommand.png","sprite":"spell8.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"OrianaRedactCommand","name":"Command: Protect","description":"Orianna commands her Ball to attach to an allied champion, shielding them and dealing magic damage to any enemies it passes through on the way. Additionally, the Ball grants additional Armor and Magic Resist to the champion it is attached to.","tooltip":"Passive: The Ball adds {{ e2 }} Armor and Magic Resist to the allied champion it is attached to.

    Active: Orianna commands her Ball to travel to and attach onto an allied champion, shielding them for {{ e5 }} seconds from the next {{ e1 }} (+{{ a1 }}) damage. Enemies the Ball passes through along the way are damaged for {{ e4 }}% of the shield value: {{ e3 }} (+{{ a2 }}).
    ","leveltip":{"label":["Armor Bonus","Magic Resist Bonus","Damage Absorption"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e2 }} -> {{ e2NL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[80,120,160,200,240],[10,15,20,25,30],[60,90,120,150,180],[75,75,75,75,75],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","10/15/20/25/30","60/90/120/150/180","75","4","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1095,1095,1095,1095,1095],"rangeBurn":"1095","image":{"full":"OrianaRedactCommand.png","sprite":"spell8.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"OrianaDetonateCommand","name":"Command: Shockwave","description":"Orianna commands her Ball to unleash a shockwave, dealing magic damage and launching nearby enemies towards the Ball after a short delay.","tooltip":"Orianna commands her Ball to unleash a shockwave after a brief delay, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and flinging them into the air a set distance in the direction of the Ball.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,95,80],"cooldownBurn":"110/95/80","cost":[100,100,100],"costBurn":"100","effect":[null,[150,225,300],[40,50,60],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/225/300","40/50/60","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[410,410,410],"rangeBurn":"410","image":{"full":"OrianaDetonateCommand.png","sprite":"spell8.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Pantheon":{"id":80,"key":"Pantheon","name":"Pantheon","title":"the Artisan of War","spells":[{"id":"PantheonQ","name":"Spear Shot","description":"Pantheon hurls his spear at an opponent, dealing damage.","tooltip":"Pantheon hurls his spear at an opponent, dealing {{ e2 }} (+{{ f1 }}) physical damage. ","leveltip":{"label":["Base Damage"],"effect":["{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[100,115,130,145,160],[65,105,145,185,225],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100/115/130/145/160","65/105/145/185/225","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.4,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"PantheonQ.png","sprite":"spell8.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PantheonW","name":"Aegis of Zeonia","description":"Pantheon leaps at an enemy and bashes them with his shield, stunning them. After finishing the attack, Pantheon readies himself to block the next attack.","tooltip":"Pantheon leaps at target enemy, dealing {{ e1 }} (+{{ a1 }}) magic damage and stunning them for {{ e2 }} second. Pantheon also instantly refreshes his Aegis Protection.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[55,55,55,55,55],"costBurn":"55","effect":[null,[50,75,100,125,150],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","1","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"PantheonW.png","sprite":"spell8.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PantheonE","name":"Heartseeker Strike","description":"Pantheon focuses and unleashes 3 swift strikes to the area in front of him dealing damage to all enemies. Pantheon also becomes more aware of his enemy's vital spots, allowing him to always crit enemies below 15% Health.","tooltip":"Passive: Pantheon's basic attacks and Spear Shot gain 100% critical strike chance against targets below {{ e3 }}% Health.

    Active: Pantheon focuses and delivers {{ e4 }} swift strikes in front of him for a total of {{ e1 }} (+{{ a1 }}) physical damage (deals {{ e2 }}% damage to minions and monsters).

    Damage per Strike: {{ f1 }}","leveltip":{"label":["Base Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[80,130,180,230,280],[60,60,60,60,60],[15,15,15,15,15],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/130/180/230/280","60","15","3","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":3,"key":"a1"},{"link":"bonusattackdamage","coeff":0.6,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"PantheonE.png","sprite":"spell8.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PantheonRJump","name":"Grand Skyfall","description":"Pantheon composes himself then leaps into the air to a target, striking all enemy units in an area. Enemies closer to the impact point take more damage.","tooltip":"Pantheon gathers his strength and then leaps high into the air, crashing down at target area a few seconds later. Deals up to {{ e1 }} (+{{ a1 }}) magic damage to enemies at the center (down to {{ e6 }}% at the edge) and slows their Movement Speed by {{ e4 }}% for 1 second.

    If Pantheon cancels this channel, Grand Skyfall is put on a {{ e5 }} second cooldown.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,135,120],"cooldownBurn":"150/135/120","cost":[100,100,100],"costBurn":"100","effect":[null,[400,700,1000],[0,0,0],[0,0,0],[35,35,35],[30,30,30],[50,50,50],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"400/700/1000","0","0","35","30","50","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[5500,5500,5500],"rangeBurn":"5500","image":{"full":"PantheonRJump.png","sprite":"spell8.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Poppy":{"id":78,"key":"Poppy","name":"Poppy","title":"Keeper of the Hammer","spells":[{"id":"PoppyQ","name":"Hammer Shock","description":"Poppy swings her hammer, dealing damage and creating a zone that will slow enemies and explode after a delay.","tooltip":"Poppy smashes the ground, dealing {{ e1 }} (+{{ a1 }}) plus {{ e5 }}% of the enemies' maximum Health as physical damage, and leaving an unstable area.

    The area slows enemies inside it by {{ e3 }}% and erupts after {{ e4 }} second, dealing the initial damage again.","leveltip":{"label":["Damage","Slow","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[35,40,45,50,55],"costBurn":"35/40/45/50/55","effect":[null,[35,55,75,95,115],[0.5,0.5,0.5,0.5,0.5],[20,25,30,35,40],[1,1,1,1,1],[7,7,7,7,7],[40,60,80,100,120],[100,100,100,100,100],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/55/75/95/115","0.5","20/25/30/35/40","1","7","40/60/80/100/120","100","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[430,430,430,430,430],"rangeBurn":"430","image":{"full":"PoppyQ.png","sprite":"spell8.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PoppyW","name":"Steadfast Presence","description":"Poppy passively gains Armor and Magic Resist. This bonus increases when she is low on Health. Poppy can activate Steadfast Presence to stop enemy dashes around her and gain movement speed.","tooltip":"Passive: Poppy gains {{ f1 }} Armor and {{ f2 }} Magic Resist ({{ e3 }}% of Armor and Magic Resist). This bonus is doubled if Poppy is below 40% Health.

    Active: For the next {{ e1 }} seconds, Poppy gains {{ e2 }}% Movement Speed. While Steadfast Presence is active, she stops enemy dashes in an area around her, dealing {{ e5 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Movement Speed","Cooldown","Damage"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ e5 }} -> {{ e5NL }}"]},"maxrank":5,"cooldown":[24,22,20,18,16],"cooldownBurn":"24/22/20/18/16","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[2.5,2.5,2.5,2.5,2.5],[32,34,36,38,40],[15,15,15,15,15],[0.5,0.5,0.5,0.5,0.5],[70,110,150,190,230],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2.5","32/34/36/38/40","15","0.5","70/110/150/190/230","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"PoppyW.png","sprite":"spell8.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PoppyE","name":"Heroic Charge","description":"Poppy dashes to the target and pushes it back. If the target is pushed into a wall, it is stunned.","tooltip":"Poppy tackles an enemy, dealing {{ e1 }} (+{{ a1 }}) physical damage and carrying them forward. If Poppy carries the target into terrain, the enemy takes {{ e2 }} (+{{ a1 }}) additional physical damage and is stunned for {{ e3 }} seconds.","leveltip":{"label":["Initial Damage","Wall Damage","Stun Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[50,70,90,110,130],[50,70,90,110,130],[1.6,1.7,1.8,1.9,2],[1800,1800,1800,1800,1800],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/70/90/110/130","50/70/90/110/130","1.6/1.7/1.8/1.9/2","1800","400","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"a1"},{"link":"bonusattackdamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"PoppyE.png","sprite":"spell8.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PoppyR","name":"Keeper's Verdict","description":"Poppy channels a hammer strike that knocks enemies very far away.","tooltip":"First Cast: Poppy channels for up to {{ e3 }} seconds, slowing herself by {{ e7 }}%.

    Second Cast: Poppy smashes the ground, emanating a shockwave that deals {{ e1 }} (+{{ a1 }}) physical damage to enemies around the first champion hit and knocking them a large distance toward their Summoning Platform. The shockwave length and knockback distance increases with channel duration.

    Enemies hit with an uncharged Keeper's Verdict will take half the damage and be knocked straight into the air for half the duration. Enemies are untargetable while they are being knocked away.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[200,300,400],[0.25,0.25,0.25],[4,4,4],[2400,2400,2400],[2,2,2],[1,1,1],[15,15,15],[30,30,30],[1,1,1],[0.5,0.5,0.5]],"effectBurn":[null,"200/300/400","0.25","4","2400","2","1","15","30","1","0.5"],"vars":[{"link":"bonusattackdamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"PoppyR.png","sprite":"spell8.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Quinn":{"id":133,"key":"Quinn","name":"Quinn","title":"Demacia's Wings","spells":[{"id":"QuinnQ","name":"Blinding Assault","description":"Quinn calls Valor to mark an enemy and hinder its vision before damaging all enemies in the immediate area.","tooltip":"Valor flies in a line, marking the first enemy he hits as Vulnerable and reducing its vision radius dramatically for {{ e3 }} seconds. He then deals {{ e1 }} (+{{ f2 }}) (+{{ a1 }}) physical damage to all nearby enemies.

    If the primary target is not a champion, it cannot attack for {{ e3 }} seconds.","leveltip":{"label":["Base Damage","Total Attack Damage Ratio","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,10.5,10,9.5,9],"cooldownBurn":"11/10.5/10/9.5/9","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[20,45,70,95,120],[-1000,-1000,-1000,-1000,-1000],[1.5,1.5,1.5,1.5,1.5],[0.8,0.9,1,1.1,1.2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/45/70/95/120","-1000","1.5","0.8/0.9/1/1.1/1.2","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1025,1025,1025,1025,1025],"rangeBurn":"1025","image":{"full":"QuinnQ.png","sprite":"spell8.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"QuinnW","name":"Heightened Senses","description":"Passively grants Quinn Attack Speed and Movement Speed after she attacks a Vulnerable target. Activate to have Valor reveal a large area nearby.","tooltip":"Passive: Attacking a Vulnerable target increases Quinn's Attack Speed {{ e2 }}% and Movement Speed by {{ e3 }}% for {{ e1 }} seconds.

    Active: Valor reveals a large area nearby for {{ e5 }} seconds.","leveltip":{"label":["Attack Speed Bonus","Movement Speed Bonus","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[50,45,40,35,30],"cooldownBurn":"50/45/40/35/30","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[2,2,2,2,2],[20,35,50,65,80],[20,25,30,35,40],[25,45,65,85,105],[2,2,2,2,2],[20,30,40,50,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2","20/35/50/65/80","20/25/30/35/40","25/45/65/85/105","2","20/30/40/50/60","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[2100,2100,2100,2100,2100],"rangeBurn":"2100","image":{"full":"QuinnW.png","sprite":"spell8.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"QuinnE","name":"Vault","description":"Quinn dashes to an enemy, dealing physical damage and slowing the target's Movement Speed. Upon reaching the target, she leaps off the target, briefly interrupting it, and lands near her maximum Attack Range away from the target.","tooltip":"Quinn dashes to an enemy, dealing {{ e2 }} (+{{ a1 }}) physical damage while Valor marks it as Vulnerable.

    Upon reaching the target, Quinn leaps off, briefly displacing and slowing it by {{ e1 }}% (diminishing over {{ e3 }} seconds).","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[50,50,50,50,50],[40,70,100,130,160],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50","40/70/100/130/160","1.5","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[675,675,675,675,675],"rangeBurn":"675","image":{"full":"QuinnE.png","sprite":"spell8.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"QuinnR","name":"Behind Enemy Lines","description":"Quinn and Valor team up to fly around at great speed.","tooltip":"Quinn calls down Valor to assist her. After a 2 second channel, they unite, gaining {{ e3 }}% Total Movement Speed and the ability to cast Skystrike by recasting this ability or taking offensive action. Skystrike damages nearby enemies.

    Taking champion or turret damage puts Behind Enemy Lines on a 3 second cooldown.","leveltip":{"label":["Movement Speed Bonus","Mana Cost"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ e7 }} -> {{ e7NL }}"]},"maxrank":3,"cooldown":[0,0,0],"cooldownBurn":"0","cost":[100,50,0],"costBurn":"100/50/0","effect":[null,[15,20,25],[0,0,0],[70,100,130],[25000,25000,25000],[7,7,7],[8,8,8],[100,50,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"15/20/25","0","70/100/130","25000","7","8","100/50/0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[700,700,700],"rangeBurn":"700","image":{"full":"QuinnR.png","sprite":"spell8.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Rakan":{"id":497,"key":"Rakan","name":"Rakan","title":"The Charmer","spells":[{"id":"RakanQ","name":"Gleaming Quill","description":"Flings a magical feather that deals magic damage. Striking a champion or epic monster enables Rakan to heal his allies.","tooltip":"Flings a magical feather that deals {{ e1 }} (+{{ a1 }}) magic damage to the first enemy hit.

    If the feather hits a champion or epic monster, Rakan restores {{ f1 }} (+{{ a2 }}) health to himself and nearby allies after {{ e4 }} seconds or when he touches an ally.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f3 }} -> {{ f2 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[70,115,160,205,250],[15,15,15,15,15],[7.5,7.5,7.5,7.5,7.5],[3,3,3,3,3],[12,10.5,9,7.5,6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","15","7.5","3","12/10.5/9/7.5/6","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.7,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"RakanQ.png","sprite":"spell8.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RakanW","name":"Grand Entrance","description":"Dashes to a location, knocking up nearby enemies on arrival.","tooltip":"Dashes to a location. Upon arrival Rakan spirals into the air, dealing {{ e1 }} (+{{ a1 }}) magic damage and knocking up enemies for {{ e3 }} second.","leveltip":{"label":["Damage","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,16.5,15,13.5,12],"cooldownBurn":"18/16.5/15/13.5/12","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[70,115,160,205,250],[1800,1800,1800,1800,1800],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","1800","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"RakanW.png","sprite":"spell8.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RakanE","name":"Battle Dance","description":"Flies to an allied champion granting them a shield. Can be re-cast for free for a short duration.","tooltip":"Flies to an allied champion, granting them a {{ e1 }} (+{{ a1 }}) health shield for {{ e3 }} seconds.

    Can be re-cast at no cost once within {{ e2 }} seconds.

    Battle Dance's cast range is increased when cast on Xayah.","leveltip":{"label":["Shield Health","Cooldown","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f2 }} -> {{ f6 }}","{{ e5 }} -> {{ e5NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[50,75,100,125,150],[5,5,5,5,5],[3,3,3,3,3],[20,18,16,14,12],[60,70,80,90,100],[600,600,600,600,600],[1000,1000,1000,1000,1000],[1150,1150,1150,1150,1150],[1250,1250,1250,1250,1250],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","5","3","20/18/16/14/12","60/70/80/90/100","600","1000","1150","1250","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"RakanE.png","sprite":"spell8.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RakanR","name":"The Quickness","description":"Gains movement speed, charming and dealing magic damage to enemies touched.","tooltip":"Gain {{ e5 }}% movement speed for {{ e2 }} seconds.

    Touching enemies deals {{ e1 }} (+{{ a1 }}) magic damage and charms them for {{ e3 }} second(s) the first time they're hit. The first champion charmed grants Rakan {{ e6 }}% decaying movement speed.","leveltip":{"label":["Charm Duration","Damage","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[100,200,300],[4,4,4],[1,1.25,1.5],[0.25,0.25,0.25],[50,50,50],[150,150,150],[200,200,200],[120,110,100],[0,0,0],[0,0,0]],"effectBurn":[null,"100/200/300","4","1/1.25/1.5","0.25","50","150","200","120/110/100","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[200,200,200],"rangeBurn":"200","image":{"full":"RakanR.png","sprite":"spell8.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Rammus":{"id":33,"key":"Rammus","name":"Rammus","title":"the Armordillo","spells":[{"id":"PowerBall","name":"Powerball","description":"Rammus accelerates in a ball towards his enemies, dealing damage and slowing targets affected by the impact.","tooltip":"Rammus accelerates in a ball, gaining up to {{ f2 }}% Movement Speed over {{ e4 }} seconds. On enemy collision, he stops, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies, knocking them back, and slowing them by {{ e2 }}% for {{ e3 }} second.

    Can be reactivated to end the effect early and put Powerball on cooldown.
    Powerball is a channeled spell and is subject to interruption by spells that prevent casting.
    ","leveltip":{"label":["Damage ","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,13.5,11,8.5,6],"cooldownBurn":"16/13.5/11/8.5/6","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[100,135,170,205,240],[40,50,60,70,80],[1,1,1,1,1],[6,6,6,6,6],[125,125,125,125,125],[0.35,0.35,0.35,0.35,0.35],[25,25,25,25,25],[0.25,0.25,0.25,0.25,0.25],[50,50,50,50,50],[1000,1000,1000,1000,1000]],"effectBurn":[null,"100/135/170/205/240","40/50/60/70/80","1","6","125","0.35","25","0.25","50","1000"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"PowerBall.png","sprite":"spell8.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"DefensiveBallCurl","name":"Defensive Ball Curl","description":"Rammus goes into a defensive formation, vastly increasing his Armor and Magic Resistance, amplifying Spiked Shells' damage, and returning damage to enemies that basic attack him, but he is also slowed during this time.","tooltip":"Rammus enters a defensive formation for up to {{ e3 }} seconds, increasing Armor and Magic Resist by {{ e1 }} plus {{ e6 }}% ({{ f1 }}/{{ f2 }}) but becoming slowed by {{ e5 }}%.

    During this time, Spiked Shell deals {{ e4 }}% damage and also deals its damage to enemies that basic attack Rammus.

    Can be reactivated to end the effect early and put Defensive Ball Curl on cooldown.","leveltip":{"label":["Percent Armor Bonus","Percent Magic Resist Bonus"],"effect":["{{ e6 }}% -> {{ e6NL }}%","{{ e6 }}% -> {{ e6NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[20,20,20,20,20],[25,35,45,55,65],[6,6,6,6,6],[150,150,150,150,150],[60,60,60,60,60],[50,55,60,65,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20","25/35/45/55/65","6","150","60","50/55/60/65/70","0","0","0","0"],"vars":[{"link":"armor","coeff":0.1,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"DefensiveBallCurl.png","sprite":"spell8.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PuncturingTaunt","name":"Frenzying Taunt","description":"Rammus taunts an enemy champion or monster into a reckless assault against him. Additionally, he gains increased Attack Speed for a short time, but this bonus is extended by having any of his other spells active.","tooltip":"Taunt an enemy champion or monster for {{ e1 }} seconds and gain {{ e2 }}% Attack Speed for the same duration.

    While any of Rammus' other spells are active, Frenzying Taunt's Attack Speed bonus is refreshed.","leveltip":{"label":["Duration","Attack Speed Bonus"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[1.25,1.5,1.75,2,2.25],[20,25,30,35,40],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.25/1.5/1.75/2/2.25","20/25/30/35/40","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"PuncturingTaunt.png","sprite":"spell8.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Tremors2","name":"Tremors","description":"Rammus creates waves of destruction pulsing through the ground, causing damage to nearby enemies and slowing them. Turrets take double damage from Tremors.","tooltip":"Shake the earth for {{ e2 }} seconds, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and slowing them by {{ e4 }}% for {{ e3 }} seconds, stacking up to {{ e6 }} times.

    Tremors deals {{ e5 }}% damage to turrets.","leveltip":{"label":["Damage per Tremor","Slow per Tremor","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,80,60],"cooldownBurn":"100/80/60","cost":[100,100,100],"costBurn":"100","effect":[null,[40,80,120],[7,7,7],[1.5,1.5,1.5],[8,10,12],[200,200,200],[8,8,8],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"40/80/120","7","1.5","8/10/12","200","8","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[375,375,375],"rangeBurn":"375","image":{"full":"Tremors2.png","sprite":"spell9.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"RekSai":{"id":421,"key":"RekSai","name":"Rek'Sai","title":"the Void Burrower","spells":[{"id":"RekSaiQ","name":"Queen's Wrath / Prey Seeker","description":"Rek'Sai's next 3 basic attacks deal bonus Physical Damage to nearby enemies.

    While Burrowed, Rek'Sai launches a burst of void-charged earth that deals Physical Damage and reveals enemies hit.","tooltip":"Un-Burrowed: Rek'Sai's next 3 basic attacks within 5 seconds deal {{ e1 }} (+{{ a1 }}) bonus Physical Damage to nearby enemies.

    Burrowed: Rek'Sai launches a burst of void-charged earth that explodes on first enemy hit, dealing {{ e4 }} (+{{ a2 }}) (+{{ a1 }}) Physical Damage and revealing non-stealthed enemies for {{ e6 }} seconds.","leveltip":{"label":["Queen's Wrath Damage","Prey Seeker Damage","Prey Seeker Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ e5 }} -> {{ e5NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[15,20,25,30,35],[0,0,0,0,0],[0,0,0,0,0],[60,90,120,150,180],[11,10,9,8,7],[2.5,2.5,2.5,2.5,2.5],[5,5,5,5,5],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","0","0","60/90/120/150/180","11/10/9/8/7","2.5","5","300","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.7,"key":"a2"},{"link":"bonusattackdamage","coeff":0.4,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"RekSaiQ.png","sprite":"spell9.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"RekSaiW","name":"Burrow / Un-burrow","description":"Rek'Sai burrows into the ground, gaining new abilities and increased Movement Speed. Her vision range is reduced and she cannot use basic attacks.

    While Burrowed, Rek'Sai may cast Un-burrow to knock up and damage nearby enemies.","tooltip":"Un-Burrowed: Burrow into the ground.

    Burrowed: Un-burrow, dealing {{ e2 }} (+{{ a1 }}) Physical Damage and knocking up the closest enemy for {{ e5 }} second. All other nearby enemies will be knocked back. An enemy who has been knocked up will be immune from further Un-burrow effects for {{ e6 }} seconds.

    Burrowed Effects: New abilities, +{{ f1 }} Movement Speed, reduced vision range, disabled basic attacks, and gains Tremor Sense: Nearby enemies that move in Fog of War have their position revealed to Rek'Sai and her allies.","leveltip":{"label":["Un-burrow Damage"],"effect":["{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[1500,1500,1500,1500,1500],[50,65,80,95,110],[0,0,0,0,0],[0,0,0,0,0],[1,1,1,1,1],[10,10,10,10,10],[250,250,250,250,250],[175,175,175,175,175],[3,3,3,3,3],[0,0,0,0,0]],"effectBurn":[null,"1500","50/65/80/95/110","0","0","1","10","250","175","3","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[1650,1650,1650,1650,1650],"rangeBurn":"1650","image":{"full":"RekSaiW.png","sprite":"spell9.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"RekSaiE","name":"Furious Bite / Tunnel","description":"Rek'Sai bites her target, dealing double and True Damage if she has max Fury.

    While Burrowed, Rek'Sai creates a re-usable, long lasting tunnel. Enemies can destroy it by standing on top of either entrance.","tooltip":"Un-Burrowed: Rek'Sai bites a target dealing {{ e1 }} (+{{ a1 }}) Physical Damage. At max Fury, the bite does 100% more and deals True Damage.

    Maximum Damage: {{ f2 }}

    Burrowed: Rek'Sai tunnels forward leaving two connected Tunnel Entrances. Clicking a Tunnel Entrance will make Rek'Sai dive to the other entrance.

    Tunnel Entrances last for {{ e5 }} minutes and can be destroyed by enemies. Rek'Sai may have {{ e6 }} tunnels at one time. Tunnels have a {{ e8 }} second cooldown on use.","leveltip":{"label":["Furious Bite Damage","Tunnel Cast Cooldown","Tunnel Use Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f4 }} -> {{ f5 }}","{{ e8 }} -> {{ e8NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,60,70,80,90],[0,0,0,0,0],[12,12,12,12,12],[0,0,0,0,0],[10,10,10,10,10],[8,8,8,8,8],[0,0,0,0,0],[10,8,6,4,2],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/60/70/80/90","0","12","0","10","8","0","10/8/6/4/2","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.85,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[250,250,250,250,250],"rangeBurn":"250","image":{"full":"RekSaiE.png","sprite":"spell9.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"No Cost"},{"id":"RekSaiRWrapper","name":"Void Rush","description":"Rek'Sai dives underground and leaps towards her target, dealing massive damage.","tooltip":"Passive: Dealing damage to enemy Champions marks them for the next {{ e6 }} seconds.

    Active: Rek'Sai lets out a harrowing scream before burrowing underground seeking out a marked target. Moments later she leaps from underground, dealing {{ e8 }} (+{{ a2 }}) physical damage plus {{ e9 }}% of their missing Health.","leveltip":{"label":["Damage","Cooldown","Missing Health Damage"],"effect":["{{ e8 }} -> {{ e8NL }}","{{ f7 }} -> {{ f8 }}","{{ e9 }}% -> {{ e9NL }}%"]},"maxrank":3,"cooldown":[0,0,0],"cooldownBurn":"0","cost":[0,0,0],"costBurn":"0","effect":[null,[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[5,5,5],[0,0,0],[100,250,400],[20,25,30],[0,0,0]],"effectBurn":[null,"0","0","0","0","0","5","0","100/250/400","20/25/30","0"],"vars":[{"link":"bonusattackdamage","coeff":1.6,"key":"a2"}],"costType":"No cost","maxammo":"-1","range":[1500,1500,1500],"rangeBurn":"1500","image":{"full":"RekSaiRWrapper.png","sprite":"spell9.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"No cost"}]},"Renekton":{"id":58,"key":"Renekton","name":"Renekton","title":"the Butcher of the Sands","spells":[{"id":"RenektonCleave","name":"Cull the Meek","description":"Renekton swings his blade, dealing moderate physical damage to all targets around him, and heals for a small portion of the damage dealt. If he has more than 50 Fury, his damage and heal are increased.","tooltip":"Renekton swings his blade, dealing {{ e1 }} (+{{ a1 }}) physical damage to nearby enemies and healing himself for {{ e2 }} (+{{ f1 }}) Health for each non-champion hit and {{ f2 }} (+{{ f3 }}) Health for each champion hit, up to {{ e4 }} Health.

    He generates 2.5 Fury for each non-champion hit and 10 Fury for each champion hit, up to a maximum of 30 Fury.

    50 Fury Bonus: Damage increased to {{ e6 }} (+{{ a2 }}). Heal increased to {{ e7 }} (+{{ f4 }}) per non-champion and {{ f5 }} (+{{ f6 }}) per champion, up to {{ e9 }}. No longer generates Fury.","leveltip":{"label":["Damage","Max Health Gain","Fury Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} / {{ e9 }} -> {{ e4NL }} / {{ e9NL }}","{{ e6 }} -> {{ e6NL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[60,90,120,150,180],[3,4.5,6,7.5,9],[0.04,0.04,0.04,0.04,0.04],[50,75,100,125,150],[300,300,300,300,300],[90,135,180,225,270],[9,13.5,18,22.5,27],[0.12,0.12,0.12,0.12,0.12],[150,225,300,375,450],[0,0,0,0,0]],"effectBurn":[null,"60/90/120/150/180","3/4.5/6/7.5/9","0.04","50/75/100/125/150","300","90/135/180/225/270","9/13.5/18/22.5/27","0.12","150/225/300/375/450","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"},{"link":"bonusattackdamage","coeff":0.8,"key":"f1"},{"link":"bonusattackdamage","coeff":1.2,"key":"f2"},{"link":"bonusattackdamage","coeff":1.2,"key":"a2"}],"costType":"No Cost or 50 Fury","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"RenektonCleave.png","sprite":"spell9.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"No Cost or 50 Fury"},{"id":"RenektonPreExecute","name":"Ruthless Predator","description":"Renekton slashes his target twice, dealing moderate physical damage and stuns them for 0.75 seconds. If Renekton has more than 50 Fury, he slashes his target three times, dealing high physical damage and stuns them for 1.5 seconds.","tooltip":"Renekton's next attack strikes twice, stunning his target for {{ e3 }} seconds and dealing {{ e1 }} (+{{ f1 }}) physical damage per hit ({{ e5 }} (+{{ f2 }}) total).

    Each strike applies on-hit effects and generates Fury. Hitting a champion generates an additional {{ e7 }} Fury.

    50 Fury Bonus: Renekton attacks three times, stunning his target for {{ e4 }} seconds and dealing a total of {{ e6 }} (+{{ f3 }}) damage. No longer generates Fury.","leveltip":{"label":["Bonus Damage","Cooldown"],"effect":["{{ e5 }} / {{ e6 }} -> {{ e5NL }} / {{ e6NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[5,15,25,35,45],[0.75,0.75,0.75,0.75,0.75],[0.75,0.75,0.75,0.75,0.75],[1.5,1.5,1.5,1.5,1.5],[10,30,50,70,90],[15,45,75,105,135],[10,10,10,10,10],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/15/25/35/45","0.75","0.75","1.5","10/30/50/70/90","15/45/75/105/135","10","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.5,"key":"f1"},{"link":"attackdamage","coeff":2.25,"key":"f2"}],"costType":"No Cost or 50 Fury","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"RenektonPreExecute.png","sprite":"spell9.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"No Cost or 50 Fury"},{"id":"RenektonSliceAndDice","name":"Slice and Dice","description":"Renekton dashes, dealing damage to units along the way. Empowered, Renekton deals bonus damage and reduces the Armor of units hit.","tooltip":"Slice: Renekton dashes, dealing {{ e1 }} (+{{ f1 }}) physical damage to enemies he passes through. Hitting an enemy grants the ability to use Dice for {{ e8 }} seconds.

    Dice: Renekton dashes, dealing {{ e1 }} (+{{ f1 }}) physical damage to enemies he passes through.

    He generates {{ e4 }} Fury for each non-champion hit and {{ e5 }} Fury for each champion hit, up to a maximum of {{ e0 }} Fury.

    Dice - 50 Fury Bonus: Damage increased to {{ e3 }} (+{{ f2 }}). Enemies hit have {{ e2 }}% reduced Armor for {{ e9 }} seconds. No longer generates Fury.","leveltip":{"label":["Bonus Damage","Armor Reduction %","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,60,90,120,150],[15,20,25,30,35],[45,90,135,180,225],[2,2,2,2,2],[10,10,10,10,10],[0.9,0.9,0.9,0.9,0.9],[1.35,1.35,1.35,1.35,1.35],[4,4,4,4,4],[4,4,4,4,4],[30,30,30,30,30]],"effectBurn":[null,"30/60/90/120/150","15/20/25/30/35","45/90/135/180/225","2","10","0.9","1.35","4","4","30"],"vars":[{"link":"bonusattackdamage","coeff":0.9,"key":"f1"},{"link":"bonusattackdamage","coeff":0.9,"key":"f1"},{"link":"bonusattackdamage","coeff":1.35,"key":"f2"}],"costType":"No Cost or 50 Fury","maxammo":"-1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"RenektonSliceAndDice.png","sprite":"spell9.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"No Cost or 50 Fury"},{"id":"RenektonReignOfTheTyrant","name":"Dominus","description":"Renekton transforms into the Tyrant form, gaining bonus Health and dealing damage to enemies around him. While in this form, Renekton gains Fury periodically.","tooltip":"Renekton surrounds himself with dark energies for {{ e5 }} seconds, gaining {{ e1 }} Health and {{ e6 }} Fury. While active, he deals {{ e2 }} (+{{ a1 }}) magic damage to nearby enemies and gains {{ e4 }} Fury per second.","leveltip":{"label":["Bonus Health","Periodic Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":3,"cooldown":[120,120,120],"cooldownBurn":"120","cost":[0,0,0],"costBurn":"0","effect":[null,[250,500,750],[40,80,120],[7.5,7.5,7.5],[5,5,5],[15,15,15],[20,20,20],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250/500/750","40/80/120","7.5","5","15","20","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[20,20,20],"rangeBurn":"20","image":{"full":"RenektonReignOfTheTyrant.png","sprite":"spell9.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"No Cost"}]},"Rengar":{"id":107,"key":"Rengar","name":"Rengar","title":"the Pridestalker","spells":[{"id":"RengarQ","name":"Savagery","description":"Rengar slashes all enemies in an arc before piercing all enemies in a line. Casting this outside the max range moves Rengar a short distance in that direction.

    Ferocity effect: deals increased damage.","tooltip":"Slash all enemies in an arc before piercing all enemies in a line for {{ e1 }} (+{{ f2 }}) physical damage per strike.

    Ferocity effect:
    Each hit deals {{ f1 }} (+{{ a2 }}) damage.

    If cast outside max range, Rengar lunges forward to extend the range of the piercing attack.
    If used during leap, Savagery is cast immediately following the leap.
    ","leveltip":{"label":["Base Damage","Bonus Attack Damage Ratio"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e7 }} -> {{ e7NL }}"]},"maxrank":5,"cooldown":[0.25,0.25,0.25,0.25,0.25],"cooldownBurn":"0.25","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[25,45,65,85,105],[0.3,0.3,0.3,0.3,0.3],[350,350,350,350,350],[90,90,90,90,90],[150,150,150,150,150],[0.15,0.15,0.15,0.15,0.15],[0.2,0.3,0.4,0.5,0.6],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/45/65/85/105","0.3","350","90","150","0.15","0.2/0.3/0.4/0.5/0.6","0","0","0"],"vars":[{"link":"@dynamic.attackdamage","coeff":1,"key":"f1"},{"link":"bonusattackdamage","coeff":1.1,"key":"a2"}],"costType":"Generates 1 Ferocity","maxammo":"1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"RengarQ.png","sprite":"spell9.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"Generates 1 Ferocity"},{"id":"RengarW","name":"Battle Roar","description":"Rengar lets out a battle roar, damaging enemies and healing for some of the recent damage he has taken.

    Ferocity effect: additionally breaks crowd control effects.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and heals Rengar for {{ e2 }}% of the damage he took in the last {{ e3 }} seconds.

    Ferocity effect:
    Deals {{ f1 }} (+{{ a1 }}) magic damage and removes existing crowd control effects, in addition to its normal effect.

    Damage healed from monster attacks is increased by {{ e5 }}%.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}"]},"maxrank":5,"cooldown":[0.25,0.25,0.25,0.25,0.25],"cooldownBurn":"0.25","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,80,110,140,170],[50,50,50,50,50],[1.5,1.5,1.5,1.5,1.5],[1.5,1.5,1.5,1.5,1.5],[50,50,50,50,50],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/80/110/140/170","50","1.5","1.5","50","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"},{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":"Generates 1 Ferocity","maxammo":"1","range":[450,450,450,450,450],"rangeBurn":"450","image":{"full":"RengarW.png","sprite":"spell9.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"Generates 1 Ferocity"},{"id":"RengarE","name":"Bola Strike","description":"Rengar throws a bola, slowing the first target hit for a short duration.

    Ferocity effect: roots the target.","tooltip":"Throw a bola, dealing {{ e1 }} (+{{ a1 }}) physical damage and slowing the first enemy hit by {{ e2 }}% for {{ e3 }} seconds.

    Ferocity effect:
    Deals {{ f1 }} (+{{ a1 }}) physical damage and roots the target for {{ e4 }} seconds.","leveltip":{"label":["Damage","Slow Amount"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[0.25,0.25,0.25,0.25,0.25],"cooldownBurn":"0.25","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,95,140,185,230],[30,45,60,75,90],[1.75,1.75,1.75,1.75,1.75],[1.75,1.75,1.75,1.75,1.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/95/140/185/230","30/45/60/75/90","1.75","1.75","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.7,"key":"a1"},{"link":"bonusattackdamage","coeff":0.7,"key":"a1"}],"costType":"Generates 1 Ferocity","maxammo":"1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"RengarE.png","sprite":"spell9.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"Generates 1 Ferocity"},{"id":"RengarR","name":"Thrill of the Hunt","description":"Rengar's predatory instincts take over, Camouflaging him and revealing the nearest enemy champion in a large radius around him. During Thrill of the Hunt, Rengar gains Movement Speed and he can leap to the tracked enemy for a guaranteed critical strike, even without being in brush.","tooltip":"Rengar gains {{ e1 }}% Movement Speed and True Sight of the nearest enemy champion (within {{ e3 }} range) for {{ e2 }} seconds.

    After the first {{ e6 }} seconds, Rengar becomes Camouflaged and can leap to an enemy without being in brush. Leaping to the nearest champion results in a critical strike.

    Attacking or casting most spells ends Thrill of the Hunt.
    Stealth - Camouflaged: This character is hidden from view. It is revealed by nearby enemy champions and turrets.
    ","leveltip":{"label":["Duration","Detection Range","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,100,70],"cooldownBurn":"130/100/70","cost":[0,0,0],"costBurn":"0","effect":[null,[40,40,40],[12,16,20],[2000,3000,4000],[750,750,750],[725,725,725],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"40","12/16/20","2000/3000/4000","750","725","2","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[2000,3000,4000],"rangeBurn":"2000/3000/4000","image":{"full":"RengarR.png","sprite":"spell9.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Riven":{"id":92,"key":"Riven","name":"Riven","title":"the Exile","spells":[{"id":"RivenTriCleave","name":"Broken Wings","description":"Riven lashes out in a series of strikes. This ability can be reactivated three times in a short time frame with the third hit knocking back nearby enemies.","tooltip":"Riven strikes out in front of her in a short line. This ability may be re-cast {{ e4 }} additional times.

    1st and 2nd Use: Slashes forward, dealing {{ e1 }} (+{{ f1 }}) physical damage to all units she comes in contact with.
    3rd Use: Leaps forward and slams the ground dealing {{ e1 }} (+{{ f1 }}) physical damage and knocking nearby enemies up. This leap can cross unpathable terrain.

    Riven will target the unit your cursor is hovering over, or if no targets are present, Riven will simply strike the direction she is currently facing.","leveltip":{"label":["Base Damage","Attack Damage Scaling"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }}% -> {{ e5NL }}%"]},"maxrank":5,"cooldown":[13,13,13,13,13],"cooldownBurn":"13","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,30,50,70,90],[30,55,80,105,130],[150,225,300,375,450],[2,2,2,2,2],[40,45,50,55,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/30/50/70/90","30/55/80/105/130","150/225/300/375/450","2","40/45/50/55/60","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.4,0.45,0.5,0.55,0.6],"key":"f1"},{"link":"attackdamage","coeff":[0.4,0.45,0.5,0.55,0.6],"key":"f1"}],"costType":"No Cost","maxammo":"-1","range":[275,275,275,275,275],"rangeBurn":"275","image":{"full":"RivenTriCleave.png","sprite":"spell9.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"RivenMartyr","name":"Ki Burst","description":"Riven emits a Ki Burst, damaging and stunning nearby enemies.","tooltip":"Riven's sword emits a burst of runic energy that shocks nearby enemies, dealing {{ e1 }} (+{{ f1 }}) physical damage and stunning them for {{ e5 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[11,10,9,8,7],"cooldownBurn":"11/10/9/8/7","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,80,110,140,170],[40,70,100,130,160],[3,3,3,3,3],[0.75,0.75,0.75,0.75,0.75],[0.75,0.75,0.75,0.75,0.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/80/110/140/170","40/70/100/130/160","3","0.75","0.75","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"f1"}],"costType":"No Cost","maxammo":"-1","range":[260,260,260,260,260],"rangeBurn":"260","image":{"full":"RivenMartyr.png","sprite":"spell9.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"RivenFeint","name":"Valor","description":"Riven steps forward a short distance and blocks incoming damage.","tooltip":"Riven does a quick dash in the direction of your cursor and becomes shielded, blocking up to {{ e2 }} (+{{ f1 }}) incoming damage for 1.5 seconds.","leveltip":{"label":["Damage Blocked","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[90,120,150,180,210],[90,120,150,180,210],[4,4,4,4,4],[800,800,800,800,800],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"90/120/150/180/210","90/120/150/180/210","4","800","1.5","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"f1"}],"costType":"No Cost","maxammo":"-1","range":[250,250,250,250,250],"rangeBurn":"250","image":{"full":"RivenFeint.png","sprite":"spell9.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"RivenFengShuiEngine","name":"Blade of the Exile","description":"Riven empowers her keepsake weapon with energy, and gains Attack Damage and Range. During this time, she also gains the ability to use Wind Slash, a powerful ranged attack, once. ","tooltip":"Riven's weapon surges with spiritual energy for {{ e4 }} seconds, granting her {{ e5 }}% extra Attack Damage (+{{ f3 }}), increased Range on her damaging spells and attacks, and the ability to use Wind Slash once.

    Wind Slash: Riven fires a shockwave that deals from {{ e1 }} (+{{ f1 }}) to {{ e2 }} (+{{ f2 }}) physical damage to all enemies hit, increasing based on how much Health they are missing.","leveltip":{"label":["Shockwave Damage","Cooldown"],"effect":["{{ e1 }} / {{ e2 }} -> {{ e1NL }} / {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,95,60],"cooldownBurn":"130/95/60","cost":[0,0,0],"costBurn":"0","effect":[null,[100,150,200],[300,450,600],[20,20,20],[15,15,15],[20,20,20],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/150/200","300/450/600","20","15","20","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"f3"},{"link":"bonusattackdamage","coeff":0.6,"key":"f1"},{"link":"bonusattackdamage","coeff":1.8,"key":"f2"}],"costType":"No Cost","maxammo":"-1","range":[200,200,200],"rangeBurn":"200","image":{"full":"RivenFengShuiEngine.png","sprite":"spell9.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Rumble":{"id":68,"key":"Rumble","name":"Rumble","title":"the Mechanized Menace","spells":[{"id":"RumbleFlameThrower","name":"Flamespitter","description":"Rumble torches opponents in front of him, dealing magic damage in a cone for 3 seconds. While in Danger Zone this damage is increased. ","tooltip":"Rumble torches his opponents, dealing {{ e1 }} (+{{ a1 }}) magic damage in a cone over {{ e9 }} seconds.

    Danger Zone Bonus: Deals {{ e5 }}% damage.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[75,135,195,255,315],[20,20,20,20,20],[0,0,0,0,0],[1,1,1,1,1],[150,150,150,150,150],[12,12,12,12,12],[0.25,0.25,0.25,0.25,0.25],[3,3,3,3,3],[3,3,3,3,3],[0,0,0,0,0]],"effectBurn":[null,"75/135/195/255/315","20","0","1","150","12","0.25","3","3","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Heat","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"RumbleFlameThrower.png","sprite":"spell9.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ e2 }} Heat"},{"id":"RumbleShield","name":"Scrap Shield","description":"Rumble pulls up a shield, protecting him from damage and granting him a quick burst of speed. While in Danger Zone, the shield strength and speed bonus is increased. ","tooltip":"Rumble tosses up a shield for {{ e5 }} seconds that absorbs {{ e1 }} (+{{ a1 }}) damage. Rumble also gains an additional {{ e2 }}% Movement Speed for {{ e6 }} second.


    Danger Zone Bonus: 50% increase in shield health and Movement Speed.","leveltip":{"label":["Damage Absorption","Movement Speed Bonus"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[50,80,110,140,170],[10,15,20,25,30],[20,20,20,20,20],[0,0,0,0,0],[2,2,2,2,2],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/80/110/140/170","10/15/20/25/30","20","0","2","1","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Heat","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"RumbleShield.png","sprite":"spell9.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ e3 }} Heat"},{"id":"RumbleGrenade","name":"Electro Harpoon","description":"Rumble launches a taser, electrocuting his target with magic damage and slowing their Movement Speed. Rumble can carry 2 harpoons at a time. While in Danger Zone the damage and slow percentage is increased. ","tooltip":"Rumble shoots his opponent with up to 2 tasers, dealing {{ e1 }} (+{{ a1 }}) magic damage and applying a stacking slow of {{ e2 }}% for {{ e7 }} seconds.

    Danger Zone Bonus: Damage and slow percentage increased by 50%.","leveltip":{"label":["Damage","Slow Amount"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[0.5,0.5,0.5,0.5,0.5],"cooldownBurn":"0.5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[45,70,95,120,145],[15,20,25,30,35],[10,10,10,10,10],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[2,2,2,2,2],[30,40,50,60,66],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"45/70/95/120/145","15/20/25/30/35","10","0","0","0","2","30/40/50/60/66","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Heat","maxammo":"2","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"RumbleGrenade.png","sprite":"spell9.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ e3 }} Heat"},{"id":"RumbleCarpetBomb","name":"The Equalizer","description":"Rumble fires off a group of rockets, creating a wall of flames that damages and slows enemies. ","tooltip":"Rumble launches a line of rockets that creates a burning trail for {{ e5 }} seconds. Enemies in the area have their Movement Speed slowed by {{ e3 }}% and take {{ e1 }} (+{{ a2 }}) magic damage each second.

    You can control the placement of this attack by clicking and dragging your mouse in a line.","leveltip":{"label":["Damage Per Second","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,100,90],"cooldownBurn":"110/100/90","cost":[0,0,0],"costBurn":"0","effect":[null,[130,185,240],[0,0,0],[35,35,35],[0.5,0.5,0.5],[5,5,5],[6,6,6],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"130/185/240","0","35","0.5","5","6","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[1750,1750,1750],"rangeBurn":"1750","image":{"full":"RumbleCarpetBomb.png","sprite":"spell9.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Ryze":{"id":13,"key":"Ryze","name":"Ryze","title":"the Rune Mage","spells":[{"id":"RyzeQWrapper","name":"Overload","description":"Passively, Ryze's other damaging spells reset Overload and begin to charge a Rune that can be used to empower Overload.

    On cast, Ryze throws a charge of pure energy in a line, dealing damage to the first enemy struck. If a Rune is fully charged, Ryze also gains a shield and Movement Speed.","tooltip":"Passive: Rune Prison and Spell Flux reset Overload's cooldown and charge a Rune for {{ e2 }} seconds, up to {{ e9 }} Runes.

    Active: Unleash a runic blast, dealing {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) magic damage to the first enemy struck. Any active Runes are discharged.

    If {{ e9 }} Runes are discharged, they Overload, shielding Ryze from {{ f3 }} (+{{ a2 }}) (+{{ f2 }}) damage and increasing his Movement Speed by {{ e4 }}% for {{ e5 }} seconds. ","leveltip":{"label":["Base Damage","Movement Speed"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":6,"cooldown":[6,6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0,0],"costBurn":"0","effect":[null,[60,85,110,135,160,185],[4,4,4,4,4,4],[55,55,55,55,55,55],[25,28,31,34,37,40],[2,2,2,2,2,2],[2,2,2,2,2,2],[3,3,3,3,3,3],[3,3,3,3,3,3],[2,2,2,2,2,2],[40,40,40,40,40,40]],"effectBurn":[null,"60/85/110/135/160/185","4","55","25/28/31/34/37/40","2","2","3","3","2","40"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"RyzeQWrapper.png","sprite":"spell9.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ e0 }} Mana"},{"id":"RyzeW","name":"Rune Prison","description":"Ryze traps a target enemy unit in a cage of runes, damaging them and preventing them from moving.","tooltip":"Instantly root an enemy for {{ e1 }} second and deal {{ e2 }} (+{{ a1 }}) (+{{ f1 }}) magic damage.","leveltip":{"label":["Base Damage","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":6,"cooldown":[13,12,11,10,9,9],"cooldownBurn":"13/12/11/10/9/9","cost":[50,60,70,80,90,100],"costBurn":"50/60/70/80/90/100","effect":[null,[0.75,0.75,0.75,0.75,0.75,0.75],[80,100,120,140,160,180],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]],"effectBurn":[null,"0.75","80/100/120/140/160/180","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[615,615,615,615,615,615],"rangeBurn":"615","image":{"full":"RyzeW.png","sprite":"spell9.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RyzeE","name":"Spell Flux","description":"Ryze releases an orb of pure magical power that damages an enemy and debuffs them. Ryze's spells have additional effects against the debuffed enemy.","tooltip":"Apply Flux to an enemy, dealing {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) magic damage. Spells consume Flux for bonus effects:

    Overload: Deals {{ e2 }}% more damage and spreads to nearby enemies with Flux.
    Rune Prison: Root duration is increased to {{ e6 }} seconds.
    Spell Flux: Spreads Spell Flux to nearby enemies.

    Spells that kill enemies affected by Flux spread Spell Flux to nearby enemies.","leveltip":{"label":["Base Damage","Overload Damage Increase","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":6,"cooldown":[3.25,3,2.75,2.5,2.25,2.25],"cooldownBurn":"3.25/3/2.75/2.5/2.25/2.25","cost":[40,55,70,85,100,115],"costBurn":"40/55/70/85/100/115","effect":[null,[50,75,100,125,150,175],[40,50,60,70,80,90],[167,167,167,167,167,167],[0.1,0.1,0.1,0.1,0.1,0.1],[1.5,1.5,1.5,1.5,1.5,1.5],[2,2,2,2,2,2],[4,4,4,4,4,4],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150/175","40/50/60/70/80/90","167","0.1","1.5","2","4","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[615,615,615,615,615,615],"rangeBurn":"615","image":{"full":"RyzeE.png","sprite":"spell9.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"RyzeR","name":"Realm Warp","description":"Ryze creates a portal to a nearby location. After a few seconds, allies standing near the portal are teleported to the target location.","tooltip":"Ryze opens a portal to a location up to {{ e7 }} range away. After {{ e4 }} seconds, all allies near the portal are teleported to that location.

    If Ryze becomes unable to cast or move, Realm Warp is cancelled.","leveltip":{"label":["Range"],"effect":["{{ e7 }} -> {{ e7NL }}"]},"maxrank":2,"cooldown":[120,120],"cooldownBurn":"120","cost":[100,100],"costBurn":"100","effect":[null,[2.1,2.1],[0.65,0.65],[0.75,0.75],[2,2],[150,250],[6,6],[1750,3000],[0,0],[0,0],[0,0]],"effectBurn":[null,"2.1","0.65","0.75","2","150/250","6","1750/3000","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1750,3000],"rangeBurn":"1750/3000","image":{"full":"RyzeR.png","sprite":"spell9.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Sejuani":{"id":113,"key":"Sejuani","name":"Sejuani","title":"Fury of the North","spells":[{"id":"SejuaniQ","name":"Arctic Assault","description":"Sejuani charges forward, knocking enemies into the air. The charge stops after hitting an enemy champion.","tooltip":"Sejuani charges, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies and knocking them up. The charge ends after hitting an enemy champion.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12.5,12,11.5,11],"cooldownBurn":"13/12.5/12/11.5/11","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[60,90,120,150,180],[625,625,625,625,625],[1000,1000,1000,1000,1000],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/90/120/150/180","625","1000","0.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"SejuaniQ.png","sprite":"spell9.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SejuaniW","name":"Winter's Wrath","description":"Sejuani swings her mace twice, dealing damage and applying Frost stacks.","tooltip":"Sejuani swings her flail, dealing {{ e1 }} (+{{ f1 }}) physical damage and applying Frost to enemies hit and knocking minions and monsters back.

    She then lashes out with her flail, dealing {{ e3 }} (+{{ f2 }}) physical damage, applying Frost and slowing enemies hit briefly.","leveltip":{"label":["First Hit Damage","Second Hit Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[20,25,30,35,40],[0.75,0.75,0.75,0.75,0.75],[30,65,100,135,170],[0,0,0,0,0],[75,75,75,75,75],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/25/30/35/40","0.75","30/65/100/135/170","0","75","0.25","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"SejuaniW.png","sprite":"spell9.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SejuaniE","name":"Permafrost","description":"Sejuani freezes and stuns an enemy champion that has maximum Frost stacks.","tooltip":"Passive: Nearby melee allied champions' basic attacks apply Frost to enemy champions and large monsters.

    Active: Target enemy with 4 stacks of Frost takes {{ e3 }} (+{{ a1 }}) magic damage and is stunned for {{ e1 }} second(s).

    16","leveltip":{"label":["Damage","Stun Duration"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[1.5,1.5,1.5,1.5,1.5],"cooldownBurn":"1.5","cost":[20,20,20,20,20],"costBurn":"20","effect":[null,[1,1.25,1.5,1.75,2],[0,0,0,0,0],[40,60,80,100,120],[4,4,4,4,4],[5,5,5,5,5],[1100,1100,1100,1100,1100],[0,0,0,0,0],[0,0,0,0,0],[250,250,250,250,250],[0,0,0,0,0]],"effectBurn":[null,"1/1.25/1.5/1.75/2","0","40/60/80/100/120","4","5","1100","0","0","250","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"SejuaniE.png","sprite":"spell9.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SejuaniR","name":"Glacial Prison","description":"Sejuani throws her bola that freezes and stuns the first champion hit and creates an ice storm that slows other enemies.","tooltip":"Sejuani throws her True Ice bola that deals {{ e1 }} (+{{ a1 }}) magic damage to the first enemy champion hit and stuns them for {{ e2 }} second.

    The bola becomes more powerful as it travels, dealing {{ e7 }} (+{{ a2 }}) magic damage, stunning for {{ e6 }} seconds and creating a storm that slows other enemies by {{ e8 }}%. After {{ e3 }} seconds, the storm deals {{ e7 }} (+{{ a2 }}) magic damage and slows by {{ e4 }}% for {{ e5 }} seconds.","leveltip":{"label":["Minimum Damage","Maximum Damage","Cooldown"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[100,125,150],[1,1,1],[2,2,2],[80,80,80],[3,3,3],[2,2,2],[150,250,350],[30,30,30],[0,0,0],[0,0,0]],"effectBurn":[null,"100/125/150","1","2","80","3","2","150/250/350","30","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.8,"key":"a2"},{"link":"spelldamage","coeff":0.8,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1300,1300,1300],"rangeBurn":"1300","image":{"full":"SejuaniR.png","sprite":"spell9.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Shaco":{"id":35,"key":"Shaco","name":"Shaco","title":"the Demon Jester","spells":[{"id":"Deceive","name":"Deceive","description":"Shaco becomes Invisible and teleports to target location.","tooltip":"Shaco teleports nearby and becomes Invisible for {{ e3 }} seconds. Shaco remains Invisible even if he uses Jack in the Box or Hallucinate.

    His next basic attack during or shortly after Invisibility will reduce Deceive's cooldown by {{ f3 }} seconds.

    16","leveltip":{"label":["Stealth Duration","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15.5,15,14.5,14],"cooldownBurn":"16/15.5/15/14.5/14","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[0,0,0,0,0],[200,200,200,200,200],[1.5,2.25,3,3.75,4.5],[6,6,6,6,6],[400,400,400,400,400],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","200","1.5/2.25/3/3.75/4.5","6","400","2.5","0","2.5","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"Deceive.png","sprite":"spell9.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"JackInTheBox","name":"Jack In The Box","description":"Shaco creates a hidden animated Jack-in-the-Box. When triggered, it will fear and attack nearby enemies.","tooltip":"Shaco creates a Jack in the Box that hides from view after {{ e5 }} seconds. It pops out when an enemy comes near or when uncovered by a ward or trinket, making nearby enemies flee very slowly for {{ e3 }} seconds.

    Its attacks deal {{ e1 }} (+{{ a1 }}) magic damage. It lasts for {{ e4 }} (+{{ a2 }}) seconds while hidden or 5 seconds while firing. ","leveltip":{"label":["Damage","Fear Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[35,50,65,80,95],[200,300,400,500,600],[0.5,0.75,1,1.25,1.5],[60,60,60,60,60],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/50/65/80/95","200/300/400/500/600","0.5/0.75/1/1.25/1.5","60","2","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.05,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"JackInTheBox.png","sprite":"spell9.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TwoShivPoison","name":"Two-Shiv Poison","description":"Shaco's Shivs passively poison targets on hit, slowing their movement speed. He can throw his Shivs to deal damage and poison the target. The thrown Shiv deals bonus damage based on the target's missing Health.","tooltip":"Passive: While Two-Shiv Poison is ready to cast, Shaco's basic attacks apply Two-Shiv Poison for {{ e4 }} seconds. It reduces Movement Speed by {{ e2 }}%.

    Active: Shaco throws a shiv that applies Two-Shiv Poison for {{ e5 }} seconds and deals magic damage equal to {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) + {{ f2 }}% of the target's missing Health.","leveltip":{"label":["Base Damage","Movement Speed Slow","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[5,35,65,95,125],[20,22.5,25,27.5,30],[20,22.5,25,27.5,30],[2,2,2,2,2],[3,3,3,3,3],[30,35,40,45,50],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/35/65/95/125","20/22.5/25/27.5/30","20/22.5/25/27.5/30","2","3","30/35/40/45/50","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"f1"},{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"TwoShivPoison.png","sprite":"spell9.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"HallucinateFull","name":"Hallucinate","description":"Shaco creates an illusion of himself near him, which can attack nearby enemies (Deals reduced damage to turrets). Upon death, it explodes, spawning three mini Jack in the Boxes and dealing damage to nearby enemies. ","tooltip":"Shaco vanishes briefly and reappears with a clone. The clone lasts up to 18 seconds and detonates when it dies, dealing {{ e1 }} (+{{ a1 }}) magic damage to nearby enemies and spawning three mini Jack in the Boxes.

  • Mini boxes deal {{ e6 }} (+{{ a2 }}) damage, make enemies flee for {{ e7 }} second(s), and trigger together.

    Clone deals {{ e8 }}% of Shaco's damage and receives {{ e2 }}% increased damage.

  • The clone can be controlled by holding the alt key and using the right mouse button or by reactivating this ability.","leveltip":{"label":["On Death Damage","Mini Box Damage","Mini Box Flee Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e6 }} -> {{ e6NL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[200,300,400],[50,50,50],[15,15,15],[2,2,2],[6,6,6],[25,50,75],[0.75,1,1.25],[75,75,75],[0,0,0],[0,0,0]],"effectBurn":[null,"200/300/400","50","15","2","6","25/50/75","0.75/1/1.25","75","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"},{"link":"spelldamage","coeff":0.15,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"HallucinateFull.png","sprite":"spell9.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Shen":{"id":98,"key":"Shen","name":"Shen","title":"the Eye of Twilight","spells":[{"id":"ShenQ","name":"Twilight Assault","description":"Shen recalls his spirit blade to attack with it, dealing damage based on the target's max health. The attacks are greatly empowered if it collides with an enemy champion, and all collided enemies are slowed while running away from Shen.","tooltip":"Shen recalls his spirit blade and draws it. Enemies it collides with are slowed by {{ e4 }}% when moving away from Shen for the next {{ e5 }} seconds.

    Shen's next {{ e3 }} attacks deal {{ f1 }} plus {{ e2 }} (+{{ charabilitypower*.01 }})% of their target's max health as bonus magic damage. If the spirit blade collided with an enemy champion, those attacks deal {{ f2 }} plus {{ e6 }} (+{{ charabilitypower2*.01 }})% instead and have +{{ e9 }}% Attack Speed.

    Each attack deals {{ e1 }}% increased damage to monsters (capped at {{ e7 }}).","leveltip":{"label":["Base Percent Damage","Enhanced Percent Damage","Energy Cost","Monster Damage Cap","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e6 }}% -> {{ e6NL }}%","{{ cost }} -> {{ costNL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[8,7.25,6.5,5.75,5],"cooldownBurn":"8/7.25/6.5/5.75/5","cost":[140,130,120,110,100],"costBurn":"140/130/120/110/100","effect":[null,[100,100,100,100,100],[2,2.5,3,3.5,4],[3,3,3,3,3],[35,35,35,35,35],[2,2,2,2,2],[4,4.5,5,5.5,6],[75,100,125,150,175],[8,8,8,8,8],[50,50,50,50,50],[75,75,75,75,75]],"effectBurn":[null,"100","2/2.5/3/3.5/4","3","35","2","4/4.5/5/5.5/6","75/100/125/150/175","8","50","75"],"vars":[],"costType":" Energy","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"ShenQ.png","sprite":"spell9.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ShenW","name":"Spirit's Refuge","description":"Attacks that would hit Shen or his allies near his spirit blade are blocked.","tooltip":"Shen's spirit blade creates a defensive zone for {{ e1 }} seconds. Basic attacks that would hit Shen or an allied champion in the zone are blocked.

    If there are no champions to protect in the zone when it starts, the spirit blade will not activate until one enters or {{ e2 }} seconds pass.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,16.5,15,13.5,11],"cooldownBurn":"18/16.5/15/13.5/11","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[1.75,1.75,1.75,1.75,1.75],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.75","2","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Energy","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"ShenW.png","sprite":"spell9.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ShenE","name":"Shadow Dash","description":"Shen dashes in a direction, taunting enemies in his path.","tooltip":"Passive: Dealing damage with Shadow Dash or Twilight Assault recovers {{ f1 }} Energy.

    Active: Shen dashes in a direction, dealing {{ e2 }} [+{{ f2 }}] physical damage to enemy champions and neutral monsters in his path and taunting them for {{ e1 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[150,150,150,150,150],"costBurn":"150","effect":[null,[1.5,1.5,1.5,1.5,1.5],[50,75,100,125,150],[25,25,25,25,25],[300,300,300,300,300],[125,125,125,125,125],[150,150,150,150,150],[800,800,800,800,800],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.5","50/75/100/125/150","25","300","125","150","800","0","0","0"],"vars":[],"costType":" Energy","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"ShenE.png","sprite":"spell9.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ShenR","name":"Stand United","description":"Shen shields target allied champion from incoming damage, and soon after teleports to their location.","tooltip":"Shen grants a shield to a target allied champion that lasts {{ e2 }} seconds and absorbs up to {{ e1 }} (+{{ a1 }}) to {{ e8 }} (+{{ f2 }}) damage based on their missing health. After channeling for {{ f1 }} seconds, Shen and his spirit blade teleport to the ally's location.","leveltip":{"label":["Minimum Shield Health","Maximum Shield Health","Cooldown "],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e8 }} -> {{ e8NL }} "," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[200,180,160],"cooldownBurn":"200/180/160","cost":[0,0,0],"costBurn":"0","effect":[null,[175,350,525],[5,5,5],[175,175,175],[2000,2000,2000],[600,600,600],[0.4,0.4,0.4],[1.6,1.6,1.6],[280,560,840],[0,0,0],[0,0,0]],"effectBurn":[null,"175/350/525","5","175","2000","600","0.4","1.6","280/560/840","0","0"],"vars":[{"link":"spelldamage","coeff":1.35,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[35000,35000,35000],"rangeBurn":"35000","image":{"full":"ShenR.png","sprite":"spell9.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Shyvana":{"id":102,"key":"Shyvana","name":"Shyvana","title":"the Half-Dragon","spells":[{"id":"ShyvanaDoubleAttack","name":"Twin Bite","description":"Shyvana strikes twice on her next attack. Basic attacks reduce the cooldown of Twin Bite by 0.5 seconds.

    Dragon Form: Twin Bite cleaves all units in front Shyvana. ","tooltip":"Shyvana strikes twice on her next attack, dealing {{ f2 }} and {{ f1 }} physical damage on the first and second hits respectively.

    While Twin Bite is on cooldown, basic attacks reduce the remaining cooldown duration by {{ e3 }} seconds.

    Dragon Form: Twin Bite cleaves all units in front Shyvana.

    Twin Bite's second hit damage is equal to {{ e1 }}% of Shyvana's attack damage.","leveltip":{"label":["Bonus Damage","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} ->{{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[40,55,70,85,100],[250,250,250,250,250],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/55/70/85/100","250","0.5","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.8,0.85,0.9,0.95,1],"key":"f1"}],"costType":"No Cost","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"ShyvanaDoubleAttack.png","sprite":"spell9.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"ShyvanaImmolationAura","name":"Burnout","description":"Shyvana surrounds herself in fire, dealing magic damage per second to nearby enemies and moving faster for 3 seconds, part of this damage is applied again when Shyvana basic attacks an enemy with Burnout active. The Movement Speed reduces over the duration of the spell. Basic attacks extend the duration of Burnout.

    Dragon Form: Burnout grows in size.","tooltip":"Shyvana deals {{ e1 }} (+{{ a1 }}) (+{{ a2 }}) magic damage per second to nearby enemies and gains a bonus {{ e2 }}% movement speed that decays over {{ e7 }} seconds.

    While Burnout is active, basic attacks deal {{ effect1amount*0.25 }} (+{{ charbonusphysical*0.25 }}) (+{{ charabilitypower2*.25 }}) magic damage to nearby enemies and extend its duration by {{ e8 }} second.

    Dragon Form: Burnout grows in size.

    Burnout has a maximum duration of {{ e9 }} seconds.","leveltip":{"label":["Damage (aura)","Damage (on hit)","Movement Speed Bonus"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ effect1amount*0.25 }} -> {{ effect1amountnl*0.25 }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,32,45,57,70],[30,35,40,45,50],[0.85,0.85,0.85,0.85,0.85],[0,0,0,0,0],[25,25,25,25,25],[325,325,325,325,325],[3,3,3,3,3],[1,1,1,1,1],[7,7,7,7,7],[350,365,380,380,380]],"effectBurn":[null,"20/32/45/57/70","30/35/40/45/50","0.85","0","25","325","3","1","7","350/365/380/380/380"],"vars":[{"link":"bonusattackdamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.1,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"ShyvanaImmolationAura.png","sprite":"spell9.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"ShyvanaFireball","name":"Flame Breath","description":"Shyvana unleashes a fireball that deals damage to all enemies it encounters and leaves cinders on the target, marking them for 5 seconds. Shyvana's basic attacks on marked targets deal a percentage of their maximum Health as damage on-hit.

    Dragon Form: Flame Breath explodes on impact or at target location, dealing bonus damage and scorching the earth for a short duration.","tooltip":"Shyvana unleashes a fireball that stops after hitting a champion. All enemies hit take {{ e1 }} (+{{ a1 }}) magic damage and are marked for {{ e2 }} seconds.

    Shyvana's basic attacks against marked targets deal {{ e4 }}% of their maximum health as magic damage on hit.

    Dragon Form: Flame Breath explodes on impact or at target location, dealing {{ f1 }} (+{{ a1 }}) bonus magic damage and scorching the earth for 4 seconds. Enemies on scorched earth take {{ f3 }} (+{{ f2 }}) magic damage per second.

    Flame Breath's on-hit damage against monsters is capped at {{ e3 }} damage per hit.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[60,100,140,180,220],[5,5,5,5,5],[100,100,100,100,100],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[60,100,140,180,220],[4.5,4.5,4.5,4.5,4.5],[220,240,260,260,260],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","5","100","2.5","0","60/100/140/180/220","4.5","220/240/260/260/260","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"ShyvanaFireball.png","sprite":"spell9.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"No Cost"},{"id":"ShyvanaTransformCast","name":"Dragon's Descent","description":"Shyvana transforms into a dragon and takes flight to a target location. Enemies along her path take damage and are knocked toward her target location.

    Shyvana passively gains Fury per second and gains 2 Fury on basic attack.","tooltip":"Active: Shyvana transforms into a dragon, gaining {{ e0 }} Health and flying to target location. Enemies along her path take {{ e1 }} (+{{ a1 }}) magic damage and are knocked toward her target location.

    Passive: Shyvana generates {{ e3 }} Fury every second. Basic Attacks generate {{ e2 }} Fury.","leveltip":{"label":["Passive Fury Gain","Dragon Size","Bonus Health","Flight Damage"],"effect":["{{ e3 }} -> {{ e3NL }}","Big -> Bigger","{{ e0 }} -> {{ e0NL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[0,0,0],"cooldownBurn":"0","cost":[0,0,0],"costBurn":"0","effect":[null,[150,250,350],[2,2,2],[1,1.5,2],[5,5,5],[1100,1100,1100],[275,275,275],[100,100,100],[200,230,260],[0,0.085,0.16],[150,250,350]],"effectBurn":[null,"150/250/350","2","1/1.5/2","5","1100","275","100","200/230/260","0/0.085/0.16","150/250/350"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Fury a Second","maxammo":"-1","range":[850,850,850],"rangeBurn":"850","image":{"full":"ShyvanaTransformCast.png","sprite":"spell10.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ e4 }} Fury a Second"}]},"Singed":{"id":27,"key":"Singed","name":"Singed","title":"the Mad Chemist","spells":[{"id":"PoisonTrail","name":"Poison Trail","description":"Leaves a trail of poison behind Singed, dealing damage to enemies caught in the path.","tooltip":"Toggle: Singed lays a poisonous trail that deals {{ e1 }} (+{{ charabilitypower*4 }}) magic damage per second.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[13,13,13,13,13],"costBurn":"13","effect":[null,[22,34,46,58,70],[1,1,1,1,1],[3.25,3.25,3.25,3.25,3.25],[2.1,2.1,2.1,2.1,2.1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"22/34/46/58/70","1","3.25","2.1","0","0","0","0","0","0"],"vars":[],"costType":" Mana per Second","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"PoisonTrail.png","sprite":"spell10.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana per Second"},{"id":"MegaAdhesive","name":"Mega Adhesive","description":"Throws a vial of mega adhesive on the ground, slowing enemies who walk on it.","tooltip":"Singed leaves a sticky area on the ground for {{ e2 }} seconds, slowing enemies in the area by {{ e1 }}%.

    If Singed flings a target into the zone, it will root them.","leveltip":{"label":["Slow Amount","Mana Cost "],"effect":["{{ e1 }}% -> {{ e1NL }}%"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[14,14,14,14,14],"cooldownBurn":"14","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[35,45,55,65,75],[5,5,5,5,5],[0.25,0.25,0.25,0.25,0.25],[265,265,265,265,265],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/45/55/65/75","5","0.25","265","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"MegaAdhesive.png","sprite":"spell10.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"Fling","name":"Fling","description":"Damages target enemy unit and flings them into the air behind Singed. If the target Singed flings lands in his Mega Adhesive, they are also rooted.","tooltip":"Singed flings an enemy over his shoulder, dealing {{ e1 }} (+{{ a1 }}) plus {{ e3 }}% of the target's maximum Health as magic damage. Max 300 bonus damage against minions and monsters.

    If the target Singed flings lands in his Mega Adhesive, they are also rooted for {{ e2 }} seconds.","leveltip":{"label":["Damage","Max Health Damage","Root Duration","Mana Cost "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e2 }} -> {{ e2NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[100,110,120,130,140],"costBurn":"100/110/120/130/140","effect":[null,[50,65,80,95,110],[1,1.25,1.5,1.75,2],[6,6.5,7,7.5,8],[420,420,420,420,420],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/65/80/95/110","1/1.25/1.5/1.75/2","6/6.5/7/7.5/8","420","300","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[125,125,125,125,125],"rangeBurn":"125","image":{"full":"Fling.png","sprite":"spell10.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"InsanityPotion","name":"Insanity Potion","description":"Singed drinks a potent brew of chemicals, granting him increased combat stats.","tooltip":"Singed drinks a potent brew of chemicals, granting him {{ e1 }} Ability Power, Armor, Magic Resist, Movement Speed, Health Regen, and Mana Regen for {{ e2 }} seconds.","leveltip":{"label":["Bonus Stats"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[100,100,100],"cooldownBurn":"100","cost":[150,150,150],"costBurn":"150","effect":[null,[35,50,80],[25,25,25],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"35/50/80","25","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[20,20,20],"rangeBurn":"20","image":{"full":"InsanityPotion.png","sprite":"spell10.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Sion":{"id":14,"key":"Sion","name":"Sion","title":"The Undead Juggernaut","spells":[{"id":"SionQ","name":"Decimating Smash","description":"Sion charges a powerful swing in an area in front of himself that will deal damage to enemies when released. If he charges for enough time, enemies hit by the swing will also be knocked up and stunned.","tooltip":"Sion charges up a heavy blow for up to 2 seconds. When released, he deals {{ e1 }} (+{{ f1 }}) to {{ e4 }} (+{{ f2 }}) physical damage to enemies hit ({{ e8 }}% damage to minions) and briefly slows them.

    If Sion charges for at least 1 second, enemies are knocked up and stunned for 1.25 to 2.25 seconds.","leveltip":{"label":["Minimum Damage","Maximum Damage","Cooldown "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[20,40,60,80,100],[1.25,1.25,1.25,1.25,1.25],[0.65,0.65,0.65,0.65,0.65],[60,120,180,240,300],[2.5,2.5,2.5,2.5,2.5],[1.95,1.95,1.95,1.95,1.95],[7.5,7.5,7.5,7.5,7.5],[60,60,60,60,60],[80,80,80,80,80],[-0.8,-0.8,-0.8,-0.8,-0.8]],"effectBurn":[null,"20/40/60/80/100","1.25","0.65","60/120/180/240/300","2.5","1.95","7.5","60","80","-0.8"],"vars":[{"link":"attackdamage","coeff":0.6,"key":"f1"},{"link":"attackdamage","coeff":1.8,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[10000,10000,10000,10000,10000],"rangeBurn":"10000","image":{"full":"SionQ.png","sprite":"spell10.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SionW","name":"Soul Furnace","description":"Sion shields himself and can reactivate after 3 seconds to deal Magic Damage to enemies nearby. When Sion kills enemies, he passively gains maximum Health.","tooltip":"Passive: Sion gains {{ e5 }} maximum Health when he kills a unit ({{ e6 }} for large monsters and champion kills or assists). Current Bonus: {{ f1 }}

    Active: Sion shields himself for {{ e1 }} (+{{ a1 }}) (+{{ f2 }}) ({{ e3 }}% of maximum Health) for 6 seconds. After {{ e7 }} seconds, while the shield holds, Sion can reactivate to deal {{ e2 }} (+{{ a1 }}) plus {{ e4 }}% of the target's maximum Health as magic damage to nearby enemies. Max 400 bonus damage to minions and monsters.","leveltip":{"label":["Shield","Damage","Max Health Damage","Max Health Shield Ratio","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ e3 }}% -> {{ e3NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,11,11,11,11],"cooldownBurn":"11","cost":[65,70,75,80,85],"costBurn":"65/70/75/80/85","effect":[null,[30,55,80,105,130],[40,65,90,115,140],[8,9,10,11,12],[10,11,12,13,14],[2,2,2,2,2],[10,10,10,10,10],[3,3,3,3,3],[10,10,10,10,10],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/55/80/105/130","40/65/90/115/140","8/9/10/11/12","10/11/12/13/14","2","10","3","10","0","0"],"vars":[{"link":"health","coeff":0.1,"key":"f1"},{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"SionW.png","sprite":"spell10.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SionE","name":"Roar of the Slayer","description":"Sion fires a short range shockwave that damages and slows and reduces the Armor of the first enemy hit. If the shockwave hits a minion or monster, it will be knocked back, damaging and slowing all enemies that it passes through.","tooltip":"Sion fires a shockwave, dealing {{ e1 }} (+{{ a1 }}) magic damage to the first enemy hit, slowing it by {{ e2 }}%, and reducing its Armor by {{ e5 }}% for 2.5 seconds.

    If the target is not a champion, it will be knocked back. Enemies that the knocked back unit collides with take {{ e6 }}% bonus damage and are slowed by {{ e2 }}%.","leveltip":{"label":["Damage","Slow","Cooldown","Mana Cost "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[35,40,45,50,55],"costBurn":"35/40/45/50/55","effect":[null,[70,105,140,175,210],[40,45,50,55,60],[5,5,5,5,5],[40,45,50,55,60],[20,20,20,20,20],[30,30,30,30,30],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/105/140/175/210","40/45/50/55/60","5","40/45/50/55/60","20","30","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"SionE.png","sprite":"spell10.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SionR","name":"Unstoppable Onslaught","description":"Sion charges in a direction, ramping up speed over time. He can steer his charge slightly with the mouse cursor location. When he collides with an enemy he deals damage and knocks them up based on the distance he has charged.","tooltip":"Sion charges in a direction for 8 seconds and can steer slowly towards the mouse cursor. While charging, Sion is immune to all Crowd Control. Reactivating will cancel Sion's charge early.

    When Sion collides with an enemy champion or wall, he deals {{ e1 }} (+{{ f1 }}) physical damage and knocks up enemies in a small area for {{ e5 }} seconds. Enemies in a larger area take the same damage and are slowed by {{ e3 }}% for 3 seconds.

    The damage increases to {{ e2 }} (+{{ f2 }}) and the stun increases to {{ e6 }} seconds as Sion charges farther.","leveltip":{"label":["Minimum Damage","Maximum Damage","Slow","Cooldown "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,100,60],"cooldownBurn":"140/100/60","cost":[100,100,100],"costBurn":"100","effect":[null,[150,300,450],[300,600,900],[40,45,50],[500,500,500],[0.75,0.75,0.75],[1.75,1.75,1.75],[950,950,950],[0,0,0],[2,2,2],[0,0,0]],"effectBurn":[null,"150/300/450","300/600/900","40/45/50","500","0.75","1.75","950","0","2","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"f1"},{"link":"bonusattackdamage","coeff":0.8,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[7500,7500,7500],"rangeBurn":"7500","image":{"full":"SionR.png","sprite":"spell10.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Sivir":{"id":15,"key":"Sivir","name":"Sivir","title":"the Battle Mistress","spells":[{"id":"SivirQ","name":"Boomerang Blade","description":"Sivir hurls her crossblade like a boomerang, dealing damage each way.","tooltip":"Sivir hurls her crossblade like a boomerang, which deals {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) physical damage to the first target hit and {{ e2 }}% reduced damage to each subsequent target down to a minimum of {{ e3 }}%.","leveltip":{"label":["Base Damage","Attack Damage Scaling","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }}% -> {{ e5NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[25,45,65,85,105],[15,15,15,15,15],[40,40,40,40,40],[0.7,0.8,0.9,1,1.1],[70,80,90,100,110],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/45/65/85/105","15","40","0.7/0.8/0.9/1/1.1","70/80/90/100/110","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0,"key":"f1"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200,1200,1200],"rangeBurn":"1200","image":{"full":"SivirQ.png","sprite":"spell10.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SivirW","name":"Ricochet","description":"Sivir's next few basic attacks will bounce to nearby targets, dealing reduced damage to secondary targets.","tooltip":"Sivir's next {{ e4 }} basic attacks bounce to nearby targets, dealing {{ f1 }} physical damage to the first target and {{ f2 }} physical damage to each subsequent target.

    If a given Ricochet attack critically strikes its first target, all subsequent bounces from that attack will also critically strike.

    Targets cannot be hit more than once by a given Ricochet attack and on-hit effects are applied only to the first target of each attack.","leveltip":{"label":["Cooldown","Bounce Damage (total attack damage)"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[12,10.5,9,7.5,6],"cooldownBurn":"12/10.5/9/7.5/6","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[5,6,7,8,9],[10,20,30,40,50],[50,55,60,65,70],[3,3,3,3,3],[60,70,80,90,100],[1,1,1,1,1],[0,0,0,0,0],[4.5,4.25,4,3.75,3.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/6/7/8/9","10/20/30/40/50","50/55/60/65/70","3","60/70/80/90/100","1","0","4.5/4.25/4/3.75/3.5","0","0"],"vars":[{"link":"attackdamage","coeff":0,"key":"f1"},{"link":"attackdamage","coeff":0,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"SivirW.png","sprite":"spell10.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SivirE","name":"Spell Shield","description":"Creates a magical barrier that blocks a single enemy ability cast on Sivir. She receives Mana back if a spell is blocked.","tooltip":"Sivir creates a magical barrier for {{ e1 }} seconds that blocks the next incoming enemy ability.

    If an ability is blocked by the shield, Sivir regains {{ e2 }} Mana.","leveltip":{"label":["Cooldown","Mana Gain"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[22,19,16,13,10],"cooldownBurn":"22/19/16/13/10","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[1.5,1.5,1.5,1.5,1.5],[80,95,110,125,140],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.5","80/95/110/125/140","0","0","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"SivirE.png","sprite":"spell10.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"SivirR","name":"On The Hunt","description":"Sivir leads her allies in battle, granting them a surge Movement Speed for a period of time. Additionally passively grants Sivir bonus Attack Speed while Ricochet is active.","tooltip":"Passive: Sivir gains {{ e1 }}% Attack Speed while Ricochet is active.

    Active: Sivir rallies her allies for {{ e3 }} seconds, granting all nearby allies an initial {{ e4 }}% Movement Speed bonus that reduces to {{ e2 }}% after the first {{ e5 }} seconds of On The Hunt elapse.","leveltip":{"label":["Attack Speed","Enhanced Movement Speed Bonus","Enhanced Movement Speed Duration","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e4 }}% -> {{ e4NL }}%","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[30,45,60],[20,20,20],[8,8,8],[40,50,60],[2,3,4],[1000,1000,1000],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"30/45/60","20","8","40/50/60","2/3/4","1000","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000],"rangeBurn":"1000","image":{"full":"SivirR.png","sprite":"spell10.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Skarner":{"id":72,"key":"Skarner","name":"Skarner","title":"the Crystal Vanguard","spells":[{"id":"SkarnerVirulentSlash","name":"Crystal Slash","description":"Skarner lashes out with his claws, dealing physical damage to all nearby enemies and charging himself with Crystal Energy for several seconds if a unit is struck. If he casts Crystal Slash again while powered by Crystal Energy, he deals bonus magic damage.","tooltip":"Skarner deals {{ f1 }} physical damage to all nearby enemies. If a unit is struck, he charges himself with Crystal Energy for {{ e2 }} seconds.

    While Skarner is charged, Crystal Slash deals {{ f1 }} (+{{ a1 }}) bonus magic damage.

    Basic attacks against non-structures lower Crystal Slash's cooldown by 0.25 seconds (quadrupled against champions).","leveltip":{"label":["Total Attack Damage Ratio","Cooldown","Mana Cost"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[3.5,3.25,3,2.75,2.5],"cooldownBurn":"3.5/3.25/3/2.75/2.5","cost":[10,11,12,13,14],"costBurn":"10/11/12/13/14","effect":[null,[33,36,39,42,45],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"33/36/39/42/45","4","0","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"f1"},{"link":"bonusattackdamage","coeff":0.8,"key":"f1"},{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"SkarnerVirulentSlash.png","sprite":"spell10.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SkarnerExoskeleton","name":"Crystalline Exoskeleton","description":"Skarner gains a shield and has increased Movement Speed while the shield persists.","tooltip":"Skarner is shielded for {{ f1 }} ({{ e1 }}% of his maximum health) (+{{ a1 }}) damage for {{ e4 }} seconds. While the shield persists, Skarner gains movement speed that ramps up to {{ e5 }}% over 3 seconds.","leveltip":{"label":["Max Health Damage Absorption","Movement Speed Bonus","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e5 }}% -> {{ e5NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12.5,12,11.5,11],"cooldownBurn":"13/12.5/12/11.5/11","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[10,11,12,13,14],[30,35,40,45,50],[8,10,12,14,16],[6,6,6,6,6],[16,20,24,28,32],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/11/12/13/14","30/35/40/45/50","8/10/12/14/16","6","16/20/24/28/32","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"SkarnerExoskeleton.png","sprite":"spell10.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SkarnerFracture","name":"Fracture","description":"Skarner summons a blast of crystalline energy which deals damage to enemies struck and slows them. Basic attacking these enemies within a short window will stun them.","tooltip":"Passive: Crystallizing enemies with Fracture and Impale grants Crystal Charge for the disable duration and reduces the cooldown of Fracture by the same amount.

    Active: Skarner summons a blast of crystalline energy, dealing {{ e1 }} (+{{ a1 }}) magic damage, slowing targets hit by {{ e8 }}% for {{ e7 }} seconds and reducing the blast's speed.

    Enemies hit by Fracture are afflicted with Crystal Venom for {{ e6 }} seconds, causing Skarner's next basic attack against them to deal {{ e2 }} additional physical damage and stun the target for {{ e3 }} second.","leveltip":{"label":["Blast Damage / Attack Damage","Slow","Cooldown"],"effect":["{{ e1 }} / {{ e2 }} -> {{ e1NL }} / {{ e2NL }}","{{ e8 }}% -> {{ e8NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13.5,13,12.5,12],"cooldownBurn":"14/13.5/13/12.5/12","cost":[55,55,55,55,55],"costBurn":"55","effect":[null,[40,65,90,115,140],[25,35,45,55,65],[1,1,1,1,1],[50,50,50,50,50],[6,6,6,6,6],[5,5,5,5,5],[2.5,2.5,2.5,2.5,2.5],[30,35,40,45,50],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/65/90/115/140","25/35/45/55/65","1","50","6","5","2.5","30/35/40/45/50","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[980,980,980,980,980],"rangeBurn":"980","image":{"full":"SkarnerFracture.png","sprite":"spell10.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SkarnerImpale","name":"Impale","description":"Skarner suppresses an enemy champion and deals damage to it. During this time, Skarner can move freely and will drag his helpless victim around with him. When the effect ends, the target will be dealt additional damage.","tooltip":"Skarner suppresses an enemy champion for {{ e1 }} seconds, dealing {{ a2 }} physical damage plus {{ e2 }} (+{{ a1 }}) magic damage. Skarner can move freely during this time, and will drag his helpless victim around with him. When the effect ends, Skarner's target will be dealt the same damage again.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[1.75,1.75,1.75],[20,60,100],[50,75,100],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1.75","20/60/100","50/75/100","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.6,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350],"rangeBurn":"350","image":{"full":"SkarnerImpale.png","sprite":"spell10.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Sona":{"id":37,"key":"Sona","name":"Sona","title":"Maven of the Strings","spells":[{"id":"SonaQ","name":"Hymn of Valor","description":"Sona plays the Hymn of Valor, sends out bolts of sound, dealing magic damage to two nearby enemies, prioritizing champions and monsters. Sona gains a temporary aura that grants allies tagged by the zone bonus damage on their next attack against enemies.","tooltip":"Active: Deals {{ e1 }} (+{{ a1 }}) magic damage to the nearest two enemies (prioritizes champions) and changes her Power Chord bonus to Staccato.

    Melody: Sona gains an aura for {{ e3 }} seconds. Allied champions that enter the aura will gain an additional {{ e4 }} (+{{ a2 }}) magic damage on their next attack.","leveltip":{"label":["Damage (active)","Damage (melody)","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[40,70,100,130,160],[400,400,400,400,400],[3,3,3,3,3],[20,30,40,50,60],[5,5,5,5,5],[0.4,0.4,0.4,0.4,0.4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/70/100/130/160","400","3","20/30/40/50/60","5","0.4","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.2,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[825,825,825,825,825],"rangeBurn":"825","image":{"full":"SonaQ.png","sprite":"spell10.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SonaW","name":"Aria of Perseverance","description":"Sona plays the Aria of Perseverance, sending out protective melodies, healing Sona and a nearby wounded ally. Sona gains a temporary aura that grants allies tagged by the zone a temporary shield.","tooltip":"Active: Restores {{ e1 }} (+{{ a1 }}) health to Sona and a nearby allied champion (prioritizes most wounded) and changes her Power Chord bonus to Diminuendo.

    Melody: Sona gains an aura for {{ e3 }} seconds. Allied champions that enter the aura will gain a shield that prevents up to {{ e4 }} (+{{ a2 }}) damage within the next {{ e5 }} seconds.","leveltip":{"label":["Heal (active)","Shield (melody)","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[80,85,90,95,100],"costBurn":"80/85/90/95/100","effect":[null,[35,55,75,95,115],[400,400,400,400,400],[3,3,3,3,3],[30,55,80,105,130],[1.5,1.5,1.5,1.5,1.5],[0.25,0.25,0.25,0.25,0.25],[0.04,0.04,0.04,0.04,0.04],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/55/75/95/115","400","3","30/55/80/105/130","1.5","0.25","0.04","3","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"SonaW.png","sprite":"spell10.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SonaE","name":"Song of Celerity","description":"Sona plays the Song of Celerity, granting nearby allies bonus Movement Speed. Sona gains a temporary aura that grants allied champions tagged by the zone bonus Movement Speed on their next attack.","tooltip":"Active: Grants Sona {{ f1*100 }}% movement speed [{{ e1 }}% + {{ f2*100 }}% per 100 ability power] for {{ e9 }} seconds (or until damaged) and changes her Power Chord bonus to Tempo.

    Melody: Sona gains an aura for {{ e3 }} seconds. Allied champions that enter the aura will gain {{ f3*100 }}% movement speed for {{ e5 }} seconds.

    Sona's personal movement speed increase will always last at least {{ e5 }} seconds.","leveltip":{"label":["Movement Speed"],"effect":["{{ e1 }}% -> {{ e1NL }}%"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[10,11,12,13,14],[400,400,400,400,400],[3,3,3,3,3],[0.1,0.11,0.12,0.13,0.14],[3,3,3,3,3],[0.4,0.4,0.4,0.4,0.4],[0.04,0.04,0.04,0.04,0.04],[2,2,2,2,2],[7,7,7,7,7],[0,0,0,0,0]],"effectBurn":[null,"10/11/12/13/14","400","3","0.1/0.11/0.12/0.13/0.14","3","0.4","0.04","2","7","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[430,430,430,430,430],"rangeBurn":"430","image":{"full":"SonaE.png","sprite":"spell10.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SonaR","name":"Crescendo","description":"Sona plays her ultimate chord, stunning enemy champions and forcing them to dance and dealing magic damage to them. Each rank reduces the base cooldown of Sona's basic abilities.","tooltip":"Active: Strikes an irresistible chord, stunning enemy Champions and forcing them to dance for {{ e2 }} seconds and take {{ e1 }} (+{{ a1 }}) magic damage.

    Passive: Reduces the base cooldown of Sona's basic abilities by {{ e3 }}%.","leveltip":{"label":["Damage","Cooldown","Hymn of Valor Cooldown","Aria of Perseverance Cooldown","Song of Celerity Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ f1 }} -> {{ f2 }}","{{ f3 }} -> {{ f4 }}","{{ f5 }} -> {{ f6 }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[1.5,1.5,1.5],[10,25,40],[-0.1,-0.25,-0.4],[-0.1,-0.25,-0.4],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","1.5","10/25/40","-0.1/-0.25/-0.4","-0.1/-0.25/-0.4","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900],"rangeBurn":"900","image":{"full":"SonaR.png","sprite":"spell10.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Soraka":{"id":16,"key":"Soraka","name":"Soraka","title":"the Starchild","spells":[{"id":"SorakaQ","name":"Starcall","description":"A star falls from the sky at the target location dealing magic damage and slowing enemies. If an enemy champion is hit by Starcall, Soraka recovers health and gains movement speed when moving away from enemy champions.","tooltip":"Calls down a star from Soraka to a target location. Enemies standing in the explosion radius take {{ e1 }} (+{{ a1 }}) magic damage and are slowed by {{ e2 }}% for 2 seconds.

    If Starcall hits a champion Soraka gains Rejuvenation for {{ e5 }} seconds, which restores {{ e3 }} (+{{ f1 }}) health per second and grants {{ e9 }}% movement speed when not moving toward enemy champions.","leveltip":{"label":["Damage","Health Restore (Rejuvenation)","Rejuvenation Duration (for allies)","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e6 }} -> {{ e6NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[5,5,5,5,5],"cooldownBurn":"5","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[70,110,150,190,230],[30,30,30,30,30],[14,16,18,20,22],[7,9,11,13,15],[4,4,4,4,4],[3,3.5,4,4.5,5],[0.1,0.1,0.1,0.1,0.1],[0.05,0.05,0.05,0.05,0.05],[10,10,10,10,10],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","30","14/16/18/20/22","7/9/11/13/15","4","3/3.5/4/4.5/5","0.1","0.05","10","0"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[810,810,810,810,810],"rangeBurn":"810","image":{"full":"SorakaQ.png","sprite":"spell10.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SorakaW","name":"Astral Infusion","description":"Soraka sacrifices a portion of her own health to heal another friendly champion.","tooltip":"Restores {{ e1 }} (+{{ a1 }}) health to another champion ally.

    If cast while affected by Rejuvenation, Soraka will grant her target its benefits for {{ f1 }} seconds.

    Cannot be cast if Soraka is below 5% Health.","leveltip":{"label":["Heal Amount","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,6.5,5,3.5,2],"cooldownBurn":"8/6.5/5/3.5/2","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[80,110,140,170,200],[0.1,0.1,0.1,0.1,0.1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/110/140/170/200","0.1","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"SorakaW.png","sprite":"spell10.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"10% Max Health, {{ cost }} Mana"},{"id":"SorakaE","name":"Equinox","description":"Creates a zone at a location that silences all enemies inside. When the zone expires, all enemies still inside are rooted.","tooltip":"Creates a zone at target location for 1.5 seconds, dealing {{ e2 }} (+{{ a1 }}) magic damage to enemy Champions in the cast radius. Enemy Champions standing in the zone are silenced until they leave.

    When the zone disappears, all enemy Champions still standing in the zone are rooted for {{ e1 }} second(s) and are dealt {{ e2 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Cooldown","Root Duration"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[24,22,20,18,16],"cooldownBurn":"24/22/20/18/16","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[1,1.25,1.5,1.75,2],[70,110,150,190,230],[40,70,100,130,160],[5,5,5,5,5],[20,40,60,80,100],[60,90,120,150,180],[1.5,1.5,1.5,1.5,1.5],[260,260,260,260,260],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1/1.25/1.5/1.75/2","70/110/150/190/230","40/70/100/130/160","5","20/40/60/80/100","60/90/120/150/180","1.5","260","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"SorakaE.png","sprite":"spell10.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SorakaR","name":"Wish","description":"Soraka fills her allies with hope, instantly restoring health to herself and all friendly champions.","tooltip":"Calls upon divine powers to restore {{ e1 }} (+{{ a1 }}) Health to all allied Champions. Wish's power is increased by 50% on each Champion below 40% Health.","leveltip":{"label":["Health Restored","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,145,130],"cooldownBurn":"160/145/130","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[120,90,60],[250,350,450],[1.5,1.5,1.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","120/90/60","250/350/450","1.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.55,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"SorakaR.png","sprite":"spell10.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Swain":{"id":50,"key":"Swain","name":"Swain","title":"the Master Tactician","spells":[{"id":"SwainDecrepify","name":"Decrepify","description":"Swain sends Beatrice to cripple enemies within the target area. Crippled targets are slowed and damaged over time.","tooltip":"Beatrice lands on the target area for {{ e3 }} seconds and attacks the closest enemy unit prioritizing champions, dealing {{ e1 }} (+{{ a1 }}) magic damage per second and slowing it down by {{ e2 }}%. Damage is doubled to {{ f1 }} (+{{ a2 }}) magic damage against minions.

    Beatrice will look for a new target if the current one moves away or dies. If any nearby enemy unit is afflicted by Torment then it will become Beatrice's target.

    Max damage: {{ f2 }} (+{{ f3 }}) to non-minions and {{ f4 }} (+{{ f5 }}) to enemy minions.","leveltip":{"label":["Damage Per Second","Slow","Mana Cost","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[30,48,65,83,100],[20,25,30,35,40],[4,4,4,4,4],[350,350,350,350,350],[0.5,0.5,0.5,0.5,0.5],[0.25,0.25,0.25,0.25,0.25],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/48/65/83/100","20/25/30/35/40","4","350","0.5","0.25","2","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"SwainDecrepify.png","sprite":"spell10.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SwainShadowGrasp","name":"Nevermove","description":"Swain marks a target area. After a short delay, mighty talons grab hold of enemy units, dealing damage and rooting them.","tooltip":"Swain marks the target area that triggers after a brief delay dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies within range and rooting them for {{ e2 }} second(s).","leveltip":{"label":["Damage","Root Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[80,85,90,95,100],"costBurn":"80/85/90/95/100","effect":[null,[80,120,160,200,240],[1,1.25,1.5,1.75,2],[0.875,0.875,0.875,0.875,0.875],[0,0,0,0,0],[0.875,0.875,0.875,0.875,0.875],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","1/1.25/1.5/1.75/2","0.88","0","0.88","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"SwainShadowGrasp.png","sprite":"spell10.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SwainTorment","name":"Torment","description":"Swain afflicts his target, dealing damage to them over time and causing them to take increased damage from Swain. Enemies afflicted by Torment are focused by Beatrice while Decrepify is active.","tooltip":"Swain afflicts his target with a curse that deals {{ e1 }} (+{{ a1 }}) magic damage over 4 seconds and amplifies Swain's damage against it by {{ e2 }}%.","leveltip":{"label":["Base Damage Over Time","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,10,10,10,10],"cooldownBurn":"10","cost":[65,70,75,80,85],"costBurn":"65/70/75/80/85","effect":[null,[50,80,110,140,170],[20,20,20,20,20],[6,7,8,9,10],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/80/110/140/170","20","6/7/8/9/10","4","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[625,625,625,625,625],"rangeBurn":"625","image":{"full":"SwainTorment.png","sprite":"spell10.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SwainMetamorphism","name":"Ravenous Flock","description":"Swain inspires dread in his enemies by temporarily taking the form of a raven. During this time ravens strike out at up to 5 nearby enemies. Each raven deals damage and heals Swain by a flat amount.","tooltip":"Toggle: Swain transforms into a monstrous raven, spawning up to {{ e7 }} lesser ravens per second that strike out at nearby enemies, prioritizing champions. Each raven deals {{ e1 }} (+{{ a1 }}) magic damage and heals Swain for {{ e2 }} (+{{ f9 }}) against champions and {{ e9 }} (+{{ f10 }}) against minions and monsters.

    Ravens can't attack the same target for 1 second and the mana cost to sustain Ravenous Flock increases by {{ e8 }} Mana each second.","leveltip":{"label":["Damage Amount","Heal From Champions","Heal From Minions and Monsters"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e9 }} -> {{ e9NL }}"]},"maxrank":3,"cooldown":[20,20,20],"cooldownBurn":"20","cost":[25,25,25],"costBurn":"25","effect":[null,[50,70,90],[20,30,40],[0.12,0.12,0.12],[25,25,25],[2,2,2],[25000,25000,25000],[5,5,5],[5,5,5],[8,11,14],[0.03,0.03,0.03]],"effectBurn":[null,"50/70/90","20/30/40","0.12","25","2","25000","5","5","8/11/14","0.03"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Initial Mana Cost per Second","maxammo":"-1","range":[625,625,625],"rangeBurn":"625","image":{"full":"SwainMetamorphism.png","sprite":"spell10.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Initial Mana Cost per Second"}]},"Syndra":{"id":134,"key":"Syndra","name":"Syndra","title":"the Dark Sovereign","spells":[{"id":"SyndraQ","name":"Dark Sphere","description":"Syndra conjures a Dark Sphere dealing magic damage. The sphere remains and can be manipulated by her other powers.","tooltip":"Conjures a Dark Sphere dealing {{ e1 }} (+{{ a1 }}) magic damage. The sphere remains for {{ f1 }} seconds and can be manipulated by Syndra's other abilities.

    This spell can be cast while moving.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[40,50,60,70,80],"costBurn":"40/50/60/70/80","effect":[null,[50,95,140,185,230],[6,6,6,6,6],[2,2.2,2.4,2.6,2.8],[8,8,8,8,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/95/140/185/230","6","2/2.2/2.4/2.6/2.8","8","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"SyndraQ.png","sprite":"spell10.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SyndraW","name":"Force of Will","description":"Syndra picks up and throws a Dark Sphere or enemy minion dealing magic damage and slowing the Movement Speed of enemies. ","tooltip":"First Cast: Grabs target Dark Sphere or enemy minion. If no target is selected, grabs nearest Dark Sphere.

    Second Cast: Throws the grabbed unit. Enemies hit take {{ e2 }} (+{{ a1 }}) magic damage and are slowed by {{ e1 }}% for {{ f2 }} seconds.

    This spell can be cast while moving.","leveltip":{"label":["Damage","Mana Cost","Slow","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }} ","{{ cost }} -> {{ costNL }} ","{{ e1 }}% -> {{ e1NL }}%","{{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[25,30,35,40,45],[70,110,150,190,230],[12,11,10,9,8],[1.5,1.5,1.5,1.5,1.5],[400,400,400,400,400],[0.2,0.2,0.2,0.2,0.2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"25/30/35/40/45","70/110/150/190/230","12/11/10/9/8","1.5","400","0.2","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"@text","coeff":1.5,"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"SyndraW.png","sprite":"spell10.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SyndraE","name":"Scatter the Weak","description":"Syndra knocks enemies and Dark Spheres back dealing magic damage. Enemies hit by Dark Spheres become stunned.","tooltip":"Knocks enemies and Dark Spheres back dealing {{ e1 }} (+{{ a1 }}) magic damage to them and any enemies they collide with.

    Dark Spheres that are knocked back stun all enemies in their path for {{ e2 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ cooldown }} -> {{ cooldownNL }} "]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[70,115,160,205,250],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/115/160/205/250","1.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"SyndraE.png","sprite":"spell10.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"SyndraR","name":"Unleashed Power","description":"Syndra bombards an enemy Champion with all of her Dark Spheres.","tooltip":"Draws upon Syndra's full cataclysmic power, harnessing Dark Spheres to deal magic damage to target enemy Champion. Unleashed Power manipulates the three Dark Spheres orbiting Syndra, in addition to up to 4 Dark Spheres previously created.

    Damage per sphere: {{ e1 }} (+{{ a1 }}).
    Minimum total damage: {{ e2 }} (+{{ a2 }}).","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }} ","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[90,135,180],[270,405,540],[7,7,7],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"90/135/180","270/405/540","7","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[675,675,675],"rangeBurn":"675","image":{"full":"SyndraR.png","sprite":"spell10.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"TahmKench":{"id":223,"key":"TahmKench","name":"Tahm Kench","title":"the River King","spells":[{"id":"TahmKenchQ","name":"Tongue Lash","description":"Tahm Kench lashes out with his tongue, damaging and slowing the first unit hit. This ability gains a stun after three stacks of An Acquired Taste.","tooltip":"Damage the first enemy hit for {{ e3 }} (+{{ a1 }}) magic damage and slow them by {{ e4 }}% for {{ e5 }}s. Champions with 3 stacks of An Acquired Taste will additionally be stunned for {{ e6 }}s.

    Activate Devour while your tongue is in midair to devour monsters/minions from a distance.","leveltip":{"label":["Base Damage","Slow Amount"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[875,875,875,875,875],[1000,1000,1000,1000,1000],[80,130,180,230,280],[30,40,50,60,70],[2,2,2,2,2],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"875","1000","80/130/180/230/280","30/40/50/60/70","2","1.5","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"TahmKenchQ.png","sprite":"spell10.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TahmKenchW","name":"Devour","description":"Tahm Kench devours a target, dealing a percentage of their maximum health as magic damage. He can spit devoured minions and monsters out as a skillshot that deals magic damage in an area upon impact.","tooltip":"Devour a target for {{ e1 }}s (half that for enemy champions.) Enemies are dealt {{ e3 }}% (+{{ a1 }}%) of their maximum health as magic damage (maximum of {{ e2 }} against neutral monsters).

    Enemy Champions: Requires 3 stacks of An Acquired Taste to be devoured. While holding an enemy champion, Tahm Kench is slowed by 95%.

    Allied Champions: While holding an allied champion, Tahm Kench gains {{ e4 }}% movement speed toward enemy champions.

    Minions and Monsters: Reactivate to spit them, dealing {{ e7 }} (+{{ a2 }}) magic damage to targets hit.","leveltip":{"label":["Devour Duration","Max Health Damage","Max Damage to Monsters","Minion Spit Damage","Cooldown"],"effect":["{{ e1 }}s -> {{ e1NL }}s","{{ e3 }}% -> {{ e3NL }}%","{{ e2 }} -> {{ e2NL }}","{{ e7 }} -> {{ e7NL }}","{{ cooldown }}s -> {{ cooldownNL }}s"]},"maxrank":5,"cooldown":[14,13,12,11,10],"cooldownBurn":"14/13/12/11/10","cost":[90,90,90,90,90],"costBurn":"90","effect":[null,[4,4.5,5,5.5,6],[400,450,500,550,600],[20,23,26,29,32],[30,35,40,45,50],[0,0,0,0,0],[0,0,0,0,0],[100,150,200,250,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/4.5/5/5.5/6","400/450/500/550/600","20/23/26/29/32","30/35/40/45/50","0","0","100/150/200/250/300","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.02,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[250,250,250,250,250],"rangeBurn":"250","image":{"full":"TahmKenchW.png","sprite":"spell10.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TahmKenchE","name":"Thick Skin","description":"Tahm Kench turns incoming damage into gray health. As gray health decays, Tahm Kench is healed for a percentage of the gray health amount. When activated, this ability turns all gray health into a shield.","tooltip":"Passive: {{ e3 }}% of the Damage taken while this ability is not cooling down is converted to gray health. If allowed to decay, {{ e2 }}% of gray health will turn back into health.

    Active: Convert all of your gray health into a shield that lasts {{ e1 }} seconds.","leveltip":{"label":["Damage to Gray Health Conversion","Gray Health to Healing Conversion"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[3,3,3,3,3],[25,30,35,40,45],[70,75,80,85,90],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"3","25/30/35/40/45","70/75/80/85/90","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1,1,1,1,1],"rangeBurn":"1","image":{"full":"TahmKenchE.png","sprite":"spell10.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TahmKenchNewR","name":"Abyssal Voyage","description":"Tahm Kench teleports across the map, and he may bring one ally champion with him.","tooltip":"Begin to channel for up to 6 seconds. During this time, one ally champion can right click Tahm Kench to join in. Alternatively, reactivate this ability to travel alone. On reactivation or once an ally has opted in, Tahm travels to the target location. Incoming champion damage breaks the channel.

    'Boy, the world's one river, and I'm its king. Ain't no place I ain't been; ain't no place I can't go again.'","leveltip":{"label":["Cooldown","Range"],"effect":["{{ cooldown }}s -> {{ cooldownNL }}s","{{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[4500,5500,6500],[0.02,0.04,0.06],[20,20,20],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"4500/5500/6500","0.02/0.04/0.06","20","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[4500,5500,6500],"rangeBurn":"4500/5500/6500","image":{"full":"TahmKenchNewR.png","sprite":"spell10.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Taliyah":{"id":163,"key":"Taliyah","name":"Taliyah","title":"the Stoneweaver","spells":[{"id":"TaliyahQ","name":"Threaded Volley","description":"Taliyah throws a volley of missiles in a target direction, moving freely as she does. This works the ground below her. If Taliyah casts Threaded Volley on worked ground, she only throws one missile.","tooltip":"Hurls 5 rocks in a direction, dealing {{ e1 }} (+{{ a1 }}) magic damage in a small area around the first enemy hit. Subsequent hits on the same unit deal {{ e2 }}% reduced damage. Creates Worked Ground for {{ f1 }} seconds.

    Taliyah gains {{ f2 }}% movement speed on Worked Ground. Casting Threaded Volley on Worked Ground only hurls one rock but refunds {{ e7 }}% of its mana cost.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }}s -> {{ cooldownNL }}s","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,7.5,6,4.5,3],"cooldownBurn":"9/7.5/6/4.5/3","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[70,95,120,145,170],[60,60,60,60,60],[0,0,0,0,0],[0,0,0,0,0],[160,160,160,160,160],[120,120,120,120,120],[50,50,50,50,50],[450,450,450,450,450],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/95/120/145/170","60","0","0","160","120","50","450","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"TaliyahQ.png","sprite":"spell10.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TaliyahWVC","name":"Seismic Shove","description":"Taliyah causes an area of ground to erupt and throws enemies within in a direction of her choosing.","tooltip":"Taliyah targets an area. After a short delay, enemies caught within the area will be pushed and dealt {{ e1 }} (+{{ a1 }}) magic damage.

    You can control the direction enemies will be pushed by clicking and dragging your mouse in a line.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }}s -> {{ cooldownNL }}s","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[60,80,100,120,140],[0.55,0.55,0.55,0.55,0.55],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/80/100/120/140","0.55","400","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"TaliyahWVC.png","sprite":"spell10.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TaliyahE","name":"Unraveled Earth","description":"Taliyah creates a slowing minefield that explodes if enemies dash through it or are pushed/pulled through it.","tooltip":"Places a field of dash-sensitive traps that deal {{ e1 }} (+{{ a1 }}) magic damage and slow enemies in the area by {{ f2 }}%. After {{ e3 }} seconds, the traps explode, dealing damage again.

    Enemies dashing, being pushed, or being pulled through Unraveled Earth will trigger traps, taking {{ f3 }} (+{{ f1 }}) magic damage from each trap (maximum 4, each hit after the first deals {{ f4 }}% less damage).","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }}s -> {{ cooldownNL }}s","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,14,12,10,8],"cooldownBurn":"16/14/12/10/8","cost":[90,95,100,105,110],"costBurn":"90/95/100/105/110","effect":[null,[70,90,110,130,150],[-0.2,-0.2,-0.2,-0.2,-0.2],[4,4,4,4,4],[85,85,85,85,85],[0.5,0.5,0.5,0.5,0.5],[4,4,4,4,4],[0.15,0.15,0.15,0.15,0.15],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/90/110/130/150","-0.2","4","85","0.5","4","0.15","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"TaliyahE.png","sprite":"spell10.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TaliyahR","name":"Weaver's Wall","description":"Stone Weaver creates a very long wall and then surfs it.","tooltip":"Cast once to create a wall. Recast immediately to ride ahead of the wall. Moving or receiving damage will cause Taliyah to stop.

    Weaver's Wall lasts for {{ e1 }} seconds. Deactivate this spell to break the wall early.","leveltip":{"label":["Duration","Wall length","Cooldown"],"effect":["{{ e1 }}s -> {{ e1NL }}s","{{ e2 }} -> {{ e2NL }}","{{ cooldown }}s -> {{ cooldownNL }}s"]},"maxrank":3,"cooldown":[180,150,120],"cooldownBurn":"180/150/120","cost":[100,100,100],"costBurn":"100","effect":[null,[6,7,8],[3000,4500,6000],[0.1,0.1,0.1],[2500,2500,2500],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"6/7/8","3000/4500/6000","0.1","2500","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[3000,4500,6000],"rangeBurn":"3000/4500/6000","image":{"full":"TaliyahR.png","sprite":"spell11.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Talon":{"id":91,"key":"Talon","name":"Talon","title":"the Blade's Shadow","spells":[{"id":"TalonQ","name":"Noxian Diplomacy","description":"Talon stabs the target unit. If they are within melee range, this attack deals critical damage. If they are outside melee range, Talon will leap at his target before stabbing them. Talon refunds some health and cooldown if this ability kills the target.","tooltip":"Talon leaps to target and deals {{ e1 }} (+{{ a1 }}) physical damage. If cast from melee range, Talon does not leap but instead critically strikes, dealing {{ f2 }}% damage ({{ f3 }}).

    When Noxian Diplomacy kills a unit, Talon regains {{ f4 }} health and refunds {{ e5 }}% of its cooldown.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f5 }} -> {{ f6 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[60,85,110,135,160],[50,50,50,50,50],[17,17,17,17,17],[3,3,3,3,3],[50,50,50,50,50],[8,7.5,7,6.5,6],[0.8,0.8,0.8,0.8,0.8],[1.5,1.5,1.5,1.5,1.5],[30,30,30,30,30],[0,0,0,0,0]],"effectBurn":[null,"60/85/110/135/160","50","17","3","50","8/7.5/7/6.5/6","0.8","1.5","30","0"],"vars":[{"link":"bonusattackdamage","coeff":1.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[10000,10000,10000,10000,10000],"rangeBurn":"10000","image":{"full":"TalonQ.png","sprite":"spell11.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ e9 }} Mana"},{"id":"TalonW","name":"Rake","description":"Talon sends out a volley of daggers that then return back to him, dealing physical damage every time it passes through an enemy. The returning blades deal bonus damage and slow units hit.","tooltip":"Talon tosses a volley of blades, dealing {{ e1 }} (+{{ f1 }}) physical damage to units hit and returning to him after a delay.

    On their way back to Talon, the blades deal {{ e5 }} (+{{ a2 }}) additional damage and slow the enemy by {{ e2 }}% for 1 second.","leveltip":{"label":["Initial Damage","Return Damage","Slow Amount","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[55,60,65,70,75],"costBurn":"55/60/65/70/75","effect":[null,[50,65,80,95,110],[40,45,50,55,60],[14,13,12,11,10],[2,2,2,2,2],[60,85,110,135,160],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/65/80/95/110","40/45/50/55/60","14/13/12/11/10","2","60/85/110/135/160","1","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"TalonW.png","sprite":"spell11.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TalonE","name":"Assassin's Path","description":"Talon vaults over any terrain or structure, up to a max distance. This ability has a low cooldown, but puts the used terrain on a long cooldown.","tooltip":"Talon vaults up to {{ f2 }} units over the nearest structure or terrain in the target direction. The vault's speed is affected by Talon's movement speed.

    Talon cannot dash over the same section of terrain more than once every {{ f3 }} seconds.","leveltip":{"label":["Terrain Cooldown"],"effect":["{{ f3 }} -> {{ f4 }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[0,0,0,0,0],[0,0,0,0,0],[625,625,625,625,625],[1250,1250,1250,1250,1250],[2,2,2,2,2],[160,135,110,85,60],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"0","0","625","1250","2","160/135/110/85/60","0","0","0","0"],"vars":[],"costType":"No cost","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"TalonE.png","sprite":"spell11.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"No cost"},{"id":"TalonR","name":"Shadow Assault","description":"Talon disperses a ring of blades and becomes Invisible while gaining additional Movement Speed. When Talon emerges from Invisibility, the blades converge on his location. Each time the blades move, Shadow Assault deals physical damage to enemies hit by at least one blade. ","tooltip":"Talon disperses a ring of blades that deal {{ e1 }} (+{{ a1 }}) physical damage to all units they hit, gains {{ e3 }}% increased movement speed, and becomes Invisible for up to {{ e5 }} seconds. When the Invisibility ends, the blades converge, dealing the same damage again to enemies they pass through.

    If Talon cancels Invisibility with an attack or Noxian Diplomacy, the blades converge on his target's location instead.

    16","leveltip":{"label":["Damage","Movement Speed","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,80,60],"cooldownBurn":"100/80/60","cost":[100,100,100],"costBurn":"100","effect":[null,[90,135,180],[200,300,400],[40,55,70],[70,85,100],[2.5,2.5,2.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"90/135/180","200/300/400","40/55/70","70/85/100","2.5","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"TalonR.png","sprite":"spell11.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Taric":{"id":44,"key":"Taric","name":"Taric","title":"the Shield of Valoran","spells":[{"id":"TaricQ","name":"Starlight's Touch","description":"Heals nearby allied champions based on charges stored.

    Bravado-empowered attacks reduce this recharge time more than usual.","tooltip":"Spends all charges to heal nearby allied champions for {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) per charge, up to {{ e5 }} (+{{ a2 }}) (+{{ f2 }}) at 3 charges.

    Bravado-empowered attacks reduce this recharge time by {{ f3 }} additional seconds.","leveltip":{"label":["Base Healing","Max Healing","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[60,80,100,120,140],"costBurn":"60/80/100/120/140","effect":[null,[20,30,40,50,60],[1.5,1.5,1.5,1.5,1.5],[5,5,5,5,5],[6,6,6,6,6],[60,90,120,150,180],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/30/40/50/60","1.5","5","6","60/90/120/150/180","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana, 1-3 Charges","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"TaricQ.png","sprite":"spell11.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana, 1-3 Charges"},{"id":"TaricW","name":"Bastion","description":"Passively increase the Armor of Taric and any allied champion with Bastion.

    Actively shields an ally and grants them Bastion for as long as they remain near Taric. Taric's spells also cast off the ally with Bastion.","tooltip":"Taric's spells will also cast from a nearby ally protected by Bastion.

    Passive: Bastion increases Armor by {{ f1 }} ({{ e1 }}% of Taric's Armor).

    Active: Blesses an ally with Bastion and shields them for {{ e2 }}% of their maximum Health for {{ e3 }} seconds. Bastion lasts on the target until a new one is chosen.","leveltip":{"label":["Passive Armor","Shield Ratio"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[10,12.5,15,17.5,20],[8,9,10,11,12],[2.5,2.5,2.5,2.5,2.5],[1000,1000,1000,1000,1000],[1300,1300,1300,1300,1300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/12.5/15/17.5/20","8/9/10/11/12","2.5","1000","1300","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"TaricW.png","sprite":"spell11.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TaricE","name":"Dazzle","description":"Taric readies a beam of starlight that, after a brief delay, deals magic damage and stuns enemies.","tooltip":"Readies a beam of starlight that, after {{ e3 }} second, deals {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) magic damage and stuns enemies for {{ e2 }} second(s).","leveltip":{"label":["Damage","Stun Duration"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[60,105,150,195,240],[1,1.125,1.25,1.375,1.5],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","1/1.125/1.25/1.375/1.5","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[610,610,610,610,610],"rangeBurn":"610","image":{"full":"TaricE.png","sprite":"spell11.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TaricR","name":"Cosmic Radiance","description":"Pulses cosmic energy onto nearby allied champions after a delay, making them invulnerable for a short duration.","tooltip":"After a {{ e4 }} second delay, pulses cosmic energy onto nearby allied champions, making them invulnerable for {{ e5 }} seconds.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,130,100],"cooldownBurn":"160/130/100","cost":[100,100,100],"costBurn":"100","effect":[null,[150,275,400],[25,35,45],[2.5,2.5,2.5],[2.5,2.5,2.5],[2.5,2.5,2.5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/275/400","25/35/45","2.5","2.5","2.5","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[400,400,400],"rangeBurn":"400","image":{"full":"TaricR.png","sprite":"spell11.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Teemo":{"id":17,"key":"Teemo","name":"Teemo","title":"the Swift Scout","spells":[{"id":"BlindingDart","name":"Blinding Dart","description":"Obscures an enemy's vision with a powerful venom, dealing damage to the target unit and blinding it for the duration.","tooltip":"Deals {{ e1 }} (+{{ a1 }}) magic damage and blinds the target for {{ e2 }} seconds. ","leveltip":{"label":["Damage","Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[80,125,170,215,260],[1.5,1.75,2,2.25,2.5],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/125/170/215/260","1.5/1.75/2/2.25/2.5","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[680,680,680,680,680],"rangeBurn":"680","image":{"full":"BlindingDart.png","sprite":"spell11.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"MoveQuick","name":"Move Quick","description":"Teemo scampers around, passively increasing his Movement Speed until he is struck by an enemy champion or turret. Teemo can sprint to gain bonus Movement Speed that isn't stopped by being struck for a short time.","tooltip":"Passive: Teemo's Movement Speed is increased by {{ e1 }}% unless he has been damaged by an enemy champion or turret in the last 5 seconds.

    Active: Teemo sprints, gaining twice his normal bonus for {{ e2 }} seconds. This bonus is not lost when struck.","leveltip":{"label":["Base Movement Speed Bonus"],"effect":["{{ e1 }}% -> {{ e1NL }}%"]},"maxrank":5,"cooldown":[17,17,17,17,17],"cooldownBurn":"17","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[10,14,18,22,26],[3,3,3,3,3],[0.2,0.28,0.36,0.44,0.52],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/14/18/22/26","3","0.2/0.28/0.36/0.44/0.52","5","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"MoveQuick.png","sprite":"spell11.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ToxicShot","name":"Toxic Shot","description":"Each of Teemo's attacks will poison the target, dealing damage on impact and each second after for 4 seconds.","tooltip":"Teemo's basic attacks poison their target, dealing {{ e2 }} (+{{ a1 }}) magical damage upon impact and {{ e1 }} (+{{ a2 }}) magical damage each second for {{ e3 }} seconds.","leveltip":{"label":["Impact Damage","Damage per Second "],"effect":[" {{ e2 }} -> {{ e2NL }}"," {{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[8,8,8,8,8],"cooldownBurn":"8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[6,12,18,24,30],[10,20,30,40,50],[4,4,4,4,4],[4.5,4.5,4.5,4.5,4.5],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"6/12/18/24/30","10/20/30/40/50","4","4.5","1","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.1,"key":"a2"}],"costType":"Passive","maxammo":"-1","range":[680,680,680,680,680],"rangeBurn":"680","image":{"full":"ToxicShot.png","sprite":"spell11.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"Passive"},{"id":"TeemoRCast","name":"Noxious Trap","description":"Teemo throws an explosive poisonous trap using one of the mushrooms stored in his pack. If an enemy steps on the trap, it will release a poisonous cloud, slowing enemies and damaging them over time. If Teemo throws a mushroom onto another mushroom it will bounce, gaining additional range.","tooltip":"Tosses a stored mushroom as a trap that detonates if an enemy steps on it, spreading poison to nearby enemies that slows Movement Speed by {{ e2 }}%, reveals them and deals {{ e1 }} (+{{ a1 }}) magic damage over {{ e9 }} seconds.

    Traps last {{ e5 }} minutes and take {{ e4 }} second to arm and stealth. If a thrown trap lands on another trap, it will bounce up to {{ e6 }} Teemos further before planting.

    Teemo forages for a mushroom every {{ f1 }} seconds, but he is only big enough to carry 3 at once.","leveltip":{"label":["Damage ","Slow Percent","Max Bounce Range","Forage Timer","Cast Range"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }}% -> {{ e2NL }}%","{{ e6 }} Teemos -> {{ e6NL }} Teemos"," {{ f1 }} -> {{ f2 }}"," {{ e8 }} -> {{ e8NL }}"]},"maxrank":3,"cooldown":[0.25,0.25,0.25],"cooldownBurn":"0.25","cost":[75,75,75],"costBurn":"75","effect":[null,[200,325,450],[30,40,50],[75,75,75],[1,1,1],[5,5,5],[3,4,5],[30,25,20],[400,650,900],[4,4,4],[450,450,450]],"effectBurn":[null,"200/325/450","30/40/50","75","1","5","3/4/5","30/25/20","400/650/900","4","450"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"3","range":[400,650,900],"rangeBurn":"400/650/900","image":{"full":"TeemoRCast.png","sprite":"spell11.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Thresh":{"id":412,"key":"Thresh","name":"Thresh","title":"the Chain Warden","spells":[{"id":"ThreshQ","name":"Death Sentence","description":"Thresh binds an enemy in chains and pulls them toward him. Activating this ability a second time pulls Thresh to the enemy.","tooltip":"Thresh throws out his scythe, dealing {{ e1 }} (+{{ a1 }}) magic damage, granting True Sight and stunning the first unit hit, pulling them in for {{ e2 }} seconds.

    Reactivating this ability will pull Thresh to the bound enemy.

    Death Sentence's cooldown is reduced by {{ e4 }} seconds if it hits an enemy.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[80,120,160,200,240],[1.5,1.5,1.5,1.5,1.5],[75,75,75,75,75],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","1.5","75","3","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1075,1075,1075,1075,1075],"rangeBurn":"1075","image":{"full":"ThreshQ.png","sprite":"spell11.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ThreshW","name":"Dark Passage","description":"Thresh throws out a lantern that shields nearby allied Champions from damage. Allies can click the lantern to dash to Thresh.","tooltip":"Thresh throws the Lantern to a target location. If an ally clicks on it Thresh will pull both the Lantern and his ally to him.

    The Lantern grants a shield lasting {{ e5 }} seconds that absorbs up to {{ e1 }} (+{{ f6 }}) damage to Thresh and up to one ally if they come near it. The shield amount scales with the number of souls Thresh has collected.","leveltip":{"label":["Shield Absorption","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[22,20.5,19,17.5,16],"cooldownBurn":"22/20.5/19/17.5/16","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[60,100,140,180,220],[1,1,1,1,1],[0,0,0,0,0],[6,6,6,6,6],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","1","0","6","4","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"ThreshW.png","sprite":"spell11.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ThreshE","name":"Flay","description":"Thresh's attacks wind up, dealing more damage the longer he waits between attacks. When activated, Thresh sweeps his chain, knocking all enemies hit in the direction of the blow.","tooltip":"Passive: Thresh's basic attacks deal from {{ f3 }} to {{ f3 }} (+{{ f2 }}) additional magic damage, which builds up while not attacking (total souls collected plus up to {{ e3 }}% total Attack Damage).

    Active: Deals {{ e1 }} (+{{ a1 }}) magic damage in a line beginning behind Thresh. Enemies hit are pushed in the direction of the swing, then slowed by {{ e2 }}% for {{ e4 }} second.

    Cast forward to push; cast backward to pull.","leveltip":{"label":["Passive Damage","Active Damage","Slow","Mana Cost"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,9,9,9,9],"cooldownBurn":"9","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[65,95,125,155,185],[20,25,30,35,40],[80,110,140,170,200],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/95/125/155/185","20/25/30/35/40","80/110/140/170/200","1","1","1","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500,500,500],"rangeBurn":"500","image":{"full":"ThreshE.png","sprite":"spell11.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ThreshRPenta","name":"The Box","description":"A prison of walls that slow and deal damage if broken.","tooltip":"Thresh creates a prison of spectral walls around himself. Enemy champions who walk through a wall suffer {{ e1 }} (+{{ a1 }}) magic damage and are slowed by {{ e3 }}% for {{ e2 }} seconds, but break that wall.

    Once one wall is broken, the remaining walls deal no damage and apply half slow duration. An enemy cannot be affected by multiple walls simultaneously.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,120,100],"cooldownBurn":"140/120/100","cost":[100,100,100],"costBurn":"100","effect":[null,[250,400,550],[2,2,2],[99,99,99],[4,4,4],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"250/400/550","2","99","4","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450],"rangeBurn":"450","image":{"full":"ThreshRPenta.png","sprite":"spell11.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Tristana":{"id":18,"key":"Tristana","name":"Tristana","title":"the Yordle Gunner","spells":[{"id":"TristanaQ","name":"Rapid Fire","description":"Tristana fires her weapon rapidly, increasing her Attack Speed for a short time.","tooltip":"Active: Increases Tristana's Attack Speed by {{ e1 }}% for {{ e2 }} seconds.","leveltip":{"label":["Attack Speed %","Cooldown"],"effect":[" {{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,19,18,17,16],"cooldownBurn":"20/19/18/17/16","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,50,70,90,110],[7,7,7,7,7],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","7","0","0","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"TristanaQ.png","sprite":"spell11.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"TristanaW","name":"Rocket Jump","description":"Tristana fires at the ground to propel her to a distant location, dealing damage and slowing surrounding units for a brief period where she lands.","tooltip":"Tristana launches herself to target location, dealing {{ e1 }} (+{{ a1 }}) Magic Damage and slowing surrounding enemies by {{ e3 }}% for {{ e2 }} seconds.

    Kills, Assists, and max stack Explosive Charge detonations on Champions reset Rocket Jump's cooldown.","leveltip":{"label":["Damage","Slow Duration","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[22,20,18,16,14],"cooldownBurn":"22/20/18/16/14","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[60,110,160,210,260],[1,1.5,2,2.5,3],[60,60,60,60,60],[350,350,350,350,350],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/110/160/210/260","1/1.5/2/2.5/3","60","350","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"TristanaW.png","sprite":"spell11.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TristanaE","name":"Explosive Charge","description":"When Tristana kills a unit, her cannonballs burst into shrapnel, dealing damage to surrounding enemies. Can be activated to place a bomb on a target enemy that explodes after a short duration dealing damage to surrounding units.","tooltip":"Passive: Enemies explode when slain by Tristana's basic attacks, dealing {{ e1 }} (+{{ a2 }}) Magic Damage to nearby enemies.

    Active: Places a bomb on an enemy or turret that explodes after 4 seconds, dealing {{ e4 }} (+{{ f1 }}) (+{{ a1 }}) Physical Damage. Each attack and ability charges the bomb's damage by +{{ e6 }}%.

    At 4 charges, the bomb explodes immediately. The detonation radius is twice as large if used on a turret.","leveltip":{"label":["Passive Explosion Damage","Base Charge Damage","Mana Cost","Bonus Attack Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}","{{ e5 }}% -> {{ e5NL }}%"]},"maxrank":5,"cooldown":[16,15.5,15,14.5,14],"cooldownBurn":"16/15.5/15/14.5/14","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[50,75,100,125,150],[4,4,4,4,4],[300,300,300,300,300],[60,70,80,90,100],[50,65,80,95,110],[30,30,30,30,30],[500,500,500,500,500],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/75/100/125/150","4","300","60/70/80/90/100","50/65/80/95/110","30","500","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550,550,550],"rangeBurn":"550","image":{"full":"TristanaE.png","sprite":"spell11.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TristanaR","name":"Buster Shot","description":"Tristana loads a massive cannonball into her weapon and fires it at an enemy unit. This deals Magic Damage and knocks the target back. If the target is carrying the Explosive Charge bomb, the bomb detonation radius is doubled.","tooltip":"Tristana fires a massive cannonball at an enemy unit. This deals {{ e1 }} (+{{ a1 }}) Magic Damage and knocks surrounding units back {{ e2 }} distance.","leveltip":{"label":["Damage","Knockback Distance","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}"," {{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[100,100,100],"costBurn":"100","effect":[null,[300,400,500],[600,800,1000],[200,200,200],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"300/400/500","600/800/1000","200","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[550,550,550],"rangeBurn":"550","image":{"full":"TristanaR.png","sprite":"spell11.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Trundle":{"id":48,"key":"Trundle","name":"Trundle","title":"the Troll King","spells":[{"id":"TrundleTrollSmash","name":"Chomp","description":"Trundle bites his opponent, dealing damage, briefly slowing and sapping some of their Attack Damage.","tooltip":"Trundle lunges at his opponent with his next basic attack, dealing {{ e1 }} (+{{ f1 }}) physical damage and briefly slowing his target.

    This attack increases Trundle's Attack Damage by {{ e3 }} for {{ e6 }} seconds, with his opponent losing half of this amount.","leveltip":{"label":["Damage","Attack Damage","Attack Damage Scaling"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[4,4,4,4,4],"cooldownBurn":"4","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[20,40,60,80,100],[100,105,110,115,120],[20,25,30,35,40],[-10,-12.5,-15,-17.5,-20],[7,7,7,7,7],[8,8,8,8,8],[0.75,0.75,0.75,0.75,0.75],[0.1,0.1,0.1,0.1,0.1],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/40/60/80/100","100/105/110/115/120","20/25/30/35/40","-10/-12.5/-15/-17.5/-20","7","8","0.75","0.1","0","0"],"vars":[{"link":"attackdamage","coeff":[0.8,0.9,1,1.1,1.2],"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"TrundleTrollSmash.png","sprite":"spell11.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"trundledesecrate","name":"Frozen Domain","description":"Trundle turns target location into his domain, gaining Attack Speed, Movement Speed, and increased healing from all sources while on it.","tooltip":"Trundle coats target location with ice for {{ e4 }} seconds, gaining {{ e1 }}% Movement Speed, {{ e2 }}% Attack Speed and {{ e3 }}% increased healing and regeneration from all sources.","leveltip":{"label":["Movement Speed","Attack Speed"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[20,25,30,35,40],[20,35,50,65,80],[20,20,20,20,20],[8,8,8,8,8],[775,775,775,775,775],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/25/30/35/40","20/35/50/65/80","20","8","775","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"trundledesecrate.png","sprite":"spell11.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TrundleCircle","name":"Pillar of Ice","description":"Trundle creates an ice pillar at target location, becoming impassable terrain and slowing all nearby enemy units.","tooltip":"Trundle creates an icy pillar at target location for {{ e1 }} seconds, becoming impassable terrain and slowing all nearby enemy units by {{ e2 }}%.","leveltip":{"label":["Cooldown","Slow"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[22,20,18,16,14],"cooldownBurn":"22/20/18/16/14","cost":[75,75,75,75,75],"costBurn":"75","effect":[null,[6,6,6,6,6],[30,35,40,45,50],[360,360,360,360,360],[225,225,225,225,225],[150,150,150,150,150],[225,225,225,225,225],[400,400,400,400,400],[60,60,60,60,60],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"6","30/35/40/45/50","360","225","150","225","400","60","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"TrundleCircle.png","sprite":"spell11.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TrundlePain","name":"Subjugate","description":"Trundle immediately steals a percent of his target's Health, Armor and Magic Resistance. Over the next 4 seconds the amount of Health, Armor, and Magic Resistance stolen is doubled.","tooltip":"Trundle drains {{ e1 }} (+{{ a1 }})% of an enemy champion's maximum Health as Magic Damage and {{ e2 }}% of their Armor and Magic Resist, half immediately and half over {{ e3 }} seconds. The Armor and Magic Resist is returned {{ e3 }} seconds after the drain ends.","leveltip":{"label":["Health Drain","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,90,70],"cooldownBurn":"110/90/70","cost":[100,100,100],"costBurn":"100","effect":[null,[20,27.5,35],[40,40,40],[4,4,4],[4,4,4],[8,8,8],[0.5,0.5,0.5],[5,5,5],[0.02,0.02,0.02],[0,0,0],[0,0,0]],"effectBurn":[null,"20/27.5/35","40","4","4","8","0.5","5","0.02","0","0"],"vars":[{"link":"spelldamage","coeff":0.02,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"TrundlePain.png","sprite":"spell11.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Tryndamere":{"id":23,"key":"Tryndamere","name":"Tryndamere","title":"the Barbarian King","spells":[{"id":"TryndamereQ","name":"Bloodlust","description":"Tryndamere thrives on the thrills of combat, increasing his Attack Damage as he is more and more wounded. He can cast Bloodlust to consume his Fury and heal himself.","tooltip":"Passive: Tryndamere thirsts for blood, gaining {{ e1 }} Attack Damage plus {{ e2 }} per 1% Health missing.

    Active: Tryndamere consumes his Fury, restoring {{ e3 }} (+{{ a1 }}) Health, plus {{ e4 }} (+{{ f2 }}) Health per Fury consumed.","leveltip":{"label":["Attack Damage","Attack Damage per Health % Missing","Heal","Heal Per Fury"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}","{{ e4 }} -> {{ e4NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[5,10,15,20,25],[0.15,0.2,0.25,0.3,0.35],[30,40,50,60,70],[0.5,0.95,1.4,1.85,2.3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"5/10/15/20/25","0.15/0.2/0.25/0.3/0.35","30/40/50/60/70","0.5/0.95/1.4/1.85/2.3","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"TryndamereQ.png","sprite":"spell11.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"TryndamereW","name":"Mocking Shout","description":"Tryndamere lets out an insulting cry, decreasing surrounding champions' Attack Damage. Enemies with their backs turned to Tryndamere also have their Movement Speed reduced.","tooltip":"Decreases surrounding champions' Attack Damage by {{ e1 }} for {{ e3 }} seconds, and enemies with their backs turned also have their Movement Speed reduced by {{ e2 }}% for {{ e4 }} seconds.","leveltip":{"label":["Attack Damage Reduction","Movement Speed Reduction"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[14,14,14,14,14],"cooldownBurn":"14","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,35,50,65,80],[30,37.5,45,52.5,60],[4,4,4,4,4],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/35/50/65/80","30/37.5/45/52.5/60","4","4","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"TryndamereW.png","sprite":"spell11.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"TryndamereE","name":"Spinning Slash","description":"Tryndamere slices toward a target unit, dealing damage to enemies in his path.","tooltip":"Tryndamere spins through his enemies, dealing {{ e1 }} (+{{ f1 }}) (+{{ a1 }}) physical damage to enemies in his path and generating Fury.

    Spinning Slash's cooldown is reduced by {{ e2 }} second whenever Tryndamere critically strikes. This reduction is increased to {{ e3 }} seconds against champions.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[70,100,130,160,190],[1,1,1,1,1],[2,2,2,2,2],[2,2,2,2,2],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","1","2","2","5","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"TryndamereE.png","sprite":"spell11.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"UndyingRage","name":"Undying Rage","description":"Tryndamere's lust for battle becomes so strong that he is unable to die, no matter how wounded he becomes.","tooltip":"Tryndamere becomes completely immune to death for {{ e3 }} seconds, refusing to be reduced below {{ e2 }} Health and instantly gaining {{ e1 }} Fury.","leveltip":{"label":["Fury Gained","Minimum Health","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,100,90],"cooldownBurn":"110/100/90","cost":[0,0,0],"costBurn":"0","effect":[null,[50,75,100],[30,50,70],[5,5,5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"50/75/100","30/50/70","5","0","0","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[400,400,400],"rangeBurn":"400","image":{"full":"UndyingRage.png","sprite":"spell11.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"TwistedFate":{"id":4,"key":"TwistedFate","name":"Twisted Fate","title":"the Card Master","spells":[{"id":"WildCards","name":"Wild Cards","description":"Twisted Fate throws three cards, dealing damage to each enemy unit they pass through.","tooltip":"Throws three cards that deal {{ e1 }} (+{{ a1 }}) Magic Damage to each enemy unit they pass through.","leveltip":{"label":["Damage","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[60,70,80,90,100],"costBurn":"60/70/80/90/100","effect":[null,[60,105,150,195,240],[40,55,70,85,100],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/105/150/195/240","40/55/70/85/100","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.65,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[10000,10000,10000,10000,10000],"rangeBurn":"10000","image":{"full":"WildCards.png","sprite":"spell11.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"PickACard","name":"Pick A Card","description":"Twisted Fate chooses a magic card from his deck, and uses that for his next attack, causing bonus effects.","tooltip":"Cast once to shuffle the deck and again to choose your card, enhancing your next attack.

    Blue Card deals {{ e1 }} (+{{ a2 }}) (+{{ a1 }}) Magic Damage and restores {{ e6 }} Mana.

    Red Card deals {{ e4 }} (+{{ a2 }}) (+{{ a1 }}) Magic Damage to units around the target and slows their Movement Speed by {{ e2 }}% for 2.5 seconds.

    Gold Card deals {{ e5 }} (+{{ a2 }}) (+{{ a1 }}) Magic Damage and stuns for {{ e3 }} seconds.","leveltip":{"label":["Blue Card Damage","Blue Card Mana Restore","Red Card Damage","Red Card Slow %","Gold Card Damage","Gold Card Stun Duration","Mana Cost"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e6 }} -> {{ e6NL }}","{{ e4 }} -> {{ e4NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e5 }} -> {{ e5NL }}","{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[40,55,70,85,100],"costBurn":"40/55/70/85/100","effect":[null,[40,60,80,100,120],[30,35,40,45,50],[1,1.25,1.5,1.75,2],[30,45,60,75,90],[15,22.5,30,37.5,45],[50,75,100,125,150],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/60/80/100/120","30/35/40/45/50","1/1.25/1.5/1.75/2","30/45/60/75/90","15/22.5/30/37.5/45","50/75/100/125/150","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"attackdamage","coeff":1,"key":"a2"},{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[200,200,200,200,200],"rangeBurn":"200","image":{"full":"PickACard.png","sprite":"spell11.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"CardmasterStack","name":"Stacked Deck","description":"Every 4 attacks, Twisted Fate deals bonus damage. In addition, his Attack Speed is increased.","tooltip":"Passive: Every 4 attacks, Twisted Fate deals an additional {{ e1 }} (+{{ a1 }}) Magic Damage.

    In addition, his Attack Speed is increased by {{ e3 }}%.","leveltip":{"label":["Bonus Damage","Attack Speed Increase"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[55,80,105,130,155],[0,0,0,0,0],[10,15,20,25,30],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"55/80/105/130/155","0","10/15/20/25/30","4","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":"","maxammo":"-1","range":[0,0,0,0,0],"rangeBurn":"0","image":{"full":"CardmasterStack.png","sprite":"spell11.png","group":"spell","x":48,"y":144,"w":48,"h":48}},{"id":"Destiny","name":"Destiny","description":"Twisted Fate predicts the fortunes of his foes, revealing all enemy champions and enabling the use of Gate, which teleports Twisted Fate to any target location in 1.5 seconds.","tooltip":"Grants True Sight all enemy champions on the map for {{ e1 }} seconds.

    While Destiny is active, Twisted Fate can teleport up to {{ e4 }} units away in 1.5 seconds.","leveltip":{"label":["Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":3,"cooldown":[180,150,120],"cooldownBurn":"180/150/120","cost":[150,125,100],"costBurn":"150/125/100","effect":[null,[6,8,10],[180,150,120],[150,125,100],[5500,5500,5500],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"6/8/10","180/150/120","150/125/100","5500","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[5500,5500,5500],"rangeBurn":"5500","image":{"full":"Destiny.png","sprite":"spell11.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Twitch":{"id":29,"key":"Twitch","name":"Twitch","title":"the Plague Rat","spells":[{"id":"TwitchHideInShadows","name":"Ambush","description":"Twitch becomes Camouflaged for a short duration and gains Movement Speed. When leaving Camouflage, Twitch gains Attack Speed for a short duration.

    When an enemy champion with Deadly Venom dies, Ambush's cooldown is reset.","tooltip":"Twitch becomes Camouflaged and gains {{ e3 }}% Movement Speed for {{ e2 }} seconds. This bonus triples when he is nearby an enemy champion that cannot see him.

    Twitch gains {{ e1 }}% Attack Speed for {{ e6 }} seconds after exiting Ambush. Ambush resets when a champion dies while affected by Deadly Venom.

    Stealth - Camouflage:
    Twitch is hidden from view while enemy champions remain outside his detection radius. Attacking or casting spells ends Camouflage.
    ","leveltip":{"label":["Camouflage Duration","Attack Speed"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[30,35,40,45,50],[10,11,12,13,14],[10,10,10,10,10],[1,1,1,1,1],[1,1,1,1,1],[5,5,5,5,5],[3,3,3,3,3],[500,500,500,500,500],[1000,1000,1000,1000,1000],[30,30,30,30,30]],"effectBurn":[null,"30/35/40/45/50","10/11/12/13/14","10","1","1","5","3","500","1000","30"],"vars":[],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"TwitchHideInShadows.png","sprite":"spell11.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TwitchVenomCask","name":"Venom Cask","description":"Twitch hurls a cask of venom that explodes in an area, slowing targets and applying deadly venom to the target.","tooltip":"Twitch hurls a cask that adds a stack of Deadly Venom to all enemies struck and leaves behind a toxic cloud that persists for {{ e3 }} seconds.

    Enemies that remain within the cloud have {{ e2 }}% reduced Movement Speed and receive an additional stack of Deadly Venom each second.","leveltip":{"label":["Slow Amount","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[2,2,2,2,2],[25,30,35,40,45],[3,3,3,3,3],[300,300,300,300,300],[3.5,3.5,3.5,3.5,3.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2","25/30/35/40/45","3","300","3.5","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[950,950,950,950,950],"rangeBurn":"950","image":{"full":"TwitchVenomCask.png","sprite":"spell11.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TwitchExpunge","name":"Contaminate","description":"Twitch wreaks further havoc on poisoned enemies with a blast of his vile diseases.","tooltip":"Deals {{ e2 }} physical damage plus {{ e1 }} (+{{ a1 }}) (+{{ f1 }}) per stack of Deadly Venom to all nearby enemies affected by Deadly Venom. (Max Stack Damage: {{ f2 }})","leveltip":{"label":["Base Damage","Damage per Stack","Mana Cost","Cooldown"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[15,20,25,30,35],[20,35,50,65,80],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","20/35/50/65/80","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200,1200,1200],"rangeBurn":"1200","image":{"full":"TwitchExpunge.png","sprite":"spell11.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"TwitchFullAutomatic","name":"Spray and Pray","description":"Twitch unleashes the full power of his crossbow, shooting bolts over a great distance that pierce all enemies caught in their path.","tooltip":"For {{ e2 }} seconds Twitch gains {{ e5 }} Attack Range and {{ e1 }} Bonus Attack Damage.

    For the duration, his basic attacks become piercing bolts that deal {{ e3 }}% less damage to subsequent targets, down to a minimum of {{ e4 }}% damage.

    Stealth - Camouflage:
    Activating Spray and Pray does not end Camouflage.","leveltip":{"label":["Attack Damage Bonus"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":3,"cooldown":[90,90,90],"cooldownBurn":"90","cost":[100,100,100],"costBurn":"100","effect":[null,[20,30,40],[5,5,5],[20,20,20],[40,40,40],[300,300,300],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"20/30/40","5","20","40","300","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200],"rangeBurn":"1200","image":{"full":"TwitchFullAutomatic.png","sprite":"spell11.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Udyr":{"id":77,"key":"Udyr","name":"Udyr","title":"the Spirit Walker","spells":[{"id":"UdyrTigerStance","name":"Tiger Stance","description":"Tiger Stance: Activation - Udyr's Attack Speed is increased for a few seconds. Persistent Effect - Udyr's first attack and every third attack after will deal a high amount of damage over 2 seconds.","tooltip":"Persistent Effect: Udyr's first attack and every third attack after will perform a Tiger Strike, dealing bonus {{ e1 }} (+{{ f2 }}) physical damage over {{ e5 }} seconds.

    If a target is affected by Tiger Strike, a new application of Tiger Strike will deal the remaining damage instantly.

    Activation: Increases Attack Speed by {{ e2 }}% for {{ e6 }} seconds.

    Udyr's stance mana costs decrease by 1 each time he levels up.","leveltip":{"label":["Tiger Strike Damage","Attack Speed","Tiger Strike Scaling"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ e4 }}% -> {{ e4NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[30,60,90,120,150],[30,40,50,60,70],[0.15,0.15,0.15,0.15,0.15],[120,130,140,150,160],[2,2,2,2,2],[5,5,5,5,5],[0.5,0.5,0.5,0.5,0.5],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/60/90/120/150","30/40/50/60/70","0.15","120/130/140/150/160","2","5","0.5","0.25","0","0"],"vars":[{"link":"attackdamage","coeff":[1.2,1.3,1.4,1.5,1.6],"key":"f2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"UdyrTigerStance.png","sprite":"spell11.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UdyrTurtleStance","name":"Turtle Stance","description":"Turtle Stance: Activation - Udyr gains a temporary shield that absorbs damage. Persistent Effect - Udyr's first attack and every third attack after heals him for 2.5% of his maximum health.","tooltip":"Persistent Effect: Udyr's first attack and every third attack after heals him for (+{{ f1 }}) ({{ e4 }}% of his maximum health), increasing by 1% for every percent of Udyr's missing health.

    Activation: Gains a shield that absorbs {{ e1 }} (+{{ a1 }}) damage for {{ e3 }} seconds.

    Udyr's stance mana costs decrease by 1 each time he levels up.","leveltip":{"label":["Shield Amount"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[60,95,130,165,200],[0,0,0,0,0],[5,5,5,5,5],[2.5,2.5,2.5,2.5,2.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","0","5","2.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"UdyrTurtleStance.png","sprite":"spell11.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UdyrBearStance","name":"Bear Stance","description":"Bear Stance: Activation - Udyr increases Movement Speed for a short duration. Persistent Effect - Udyr's basic attacks stun his target for 1 second. This effect cannot occur on the same target for several seconds.","tooltip":"Persistent Effect: Basic attacks stun the target for {{ e3 }} second. This effect cannot occur on the same target again for {{ e4 }} seconds.

    Activation: Increases Movement Speed by {{ e1 }}% and ignores unit collision for {{ e2 }} seconds.

    Udyr's stance mana costs decrease by 1 each time he levels up.","leveltip":{"label":["Movement Speed Increase","Movement Speed Duration"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[15,20,25,30,35],[2,2.25,2.5,2.75,3],[1,1,1,1,1],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","2/2.25/2.5/2.75/3","1","5","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"UdyrBearStance.png","sprite":"spell11.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UdyrPhoenixStance","name":"Phoenix Stance","description":"Phoenix Stance: Activation - Udyr unleashes pulsing waves of fire, dealing damage to nearby enemies. Persistent Effect - Udyr's first attack and every third attack after engulfs enemies in front of him with flames.","tooltip":"Persistent Effect: Udyr's first attack and every third attack after burns enemies in front of him, dealing {{ e2 }} (+{{ a1 }}) magic damage.

    Activation: Unleashes pulsing waves of fire, dealing {{ e1 }} (+{{ a2 }}) magic damage each second to nearby enemies for {{ e5 }} seconds.

    Udyr's stance mana costs decrease by 1 each time he levels up.","leveltip":{"label":["Pulse Damage","Flame Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[45,45,45,45,45],"costBurn":"45","effect":[null,[10,20,30,40,50],[40,80,120,160,200],[16,24,32,40,48],[1,1,1,1,1],[4,4,4,4,4],[325,325,325,325,325],[400,400,400,400,400],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/20/30/40/50","40/80/120/160/200","16/24/32/40/48","1","4","325","400","3","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"},{"link":"spelldamage","coeff":0.25,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[325,325,325,325,325],"rangeBurn":"325","image":{"full":"UdyrPhoenixStance.png","sprite":"spell12.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Urgot":{"id":6,"key":"Urgot","name":"Urgot","title":"the Headsman's Pride","spells":[{"id":"UrgotHeatseekingMissile","name":"Acid Hunter","description":"Urgot fires an Acid Hunter missile that collides with the first enemy it hits, slowing the target if he has his Terror Capacitor up. Acid Hunter missile-locks on enemies affected by Noxian Corrosive Charge.","tooltip":"Urgot fires a missile toward the cursor that deals {{ e1 }} (+{{ a1 }}) physical damage. If Acid Hunter kills a unit half its mana cost is refunded.

    Missile-lock can be achieved by holding the cursor over a target afflicted by Noxian Corrosive Charge.","leveltip":{"label":["Damage"],"effect":[" {{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[2,2,2,2,2],"cooldownBurn":"2","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[10,40,70,100,130],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/40/70/100/130","0.5","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":0.85,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"UrgotHeatseekingMissile.png","sprite":"spell12.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UrgotTerrorCapacitorActive2","name":"Terror Capacitor","description":"Urgot charges up his capacitor to gain a shield. While the shield is active, Urgot gains slowing attacks.","tooltip":"Urgot charges up his terror capacitor to gain a shield that absorbs {{ e1 }} (+{{ a1 }}) plus {{ e4 }}% of his maximum mana (+{{ f1 }}) damage for 5 seconds. While the shield is active, Urgot's attacks and missiles slow targets by {{ e2 }}%. ","leveltip":{"label":["Damage Absorption ","Slow % ","Cooldown","Mana Cost"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}% ","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[55,60,65,70,75],"costBurn":"55/60/65/70/75","effect":[null,[60,100,140,180,220],[20,25,30,35,40],[4,6,8,10,12],[8,8,8,8,8],[1.5,1.5,1.5,1.5,1.5],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/100/140/180/220","20/25/30/35/40","4/6/8/10/12","8","1.5","5","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.8,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"UrgotTerrorCapacitorActive2.png","sprite":"spell12.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UrgotPlasmaGrenade","name":"Noxian Corrosive Charge","description":"Urgot launches a corrosive charge that damages enemies in an area and reduces their Armor.","tooltip":"Urgot launches a Corrosive Charge at a target location. Enemies afflicted by the charge have their Armor reduced by {{ e2 }}% and take {{ e1 }} (+{{ f1 }}) physical damage over {{ e3 }} seconds.

    Acid Hunter is able to missile-lock onto targets affected by Noxian Corrosive Charge.","leveltip":{"label":["Damage","Armor Reduction","Cooldown","Mana Cost"],"effect":[" {{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[75,130,185,240,295],[12,14,16,18,20],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/130/185/240/295","12/14/16/18/20","5","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"UrgotPlasmaGrenade.png","sprite":"spell12.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"UrgotSwap2","name":"Hyper-Kinetic Position Reverser","description":"Urgot charges up his Hyper-Kinetic Position Reverser, swapping positions with the target and terrifying nearby enemies for 1.5 seconds. His target is suppressed for the duration of the channel. He gains Damage Reduction during and after the swap.","tooltip":"Urgot targets an enemy champion and channels his Hyper-Kinetic Position Reverser for 1 second, swapping locations with his target afterward and terrifying nearby enemies for 1.5 seconds. His target is suppressed for the duration of the channel.

    Urgot takes {{ e2 }}% reduced damage for {{ e4 }} seconds beginning at the start of the channel, and his target is slowed by 40% for 3 seconds after being swapped.","leveltip":{"label":["Swap Range","Damage Reduction","Cooldown "],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[550,700,850],[30,40,50],[200,250,300],[5,5,5],[1.5,1.5,1.5],[40,40,40],[3,3,3],[1,1,1],[0,0,0],[0,0,0]],"effectBurn":[null,"550/700/850","30/40/50","200/250/300","5","1.5","40","3","1","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[550,700,850],"rangeBurn":"550/700/850","image":{"full":"UrgotSwap2.png","sprite":"spell12.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Varus":{"id":110,"key":"Varus","name":"Varus","title":"the Arrow of Retribution","spells":[{"id":"VarusQ","name":"Piercing Arrow","description":"Varus readies and then fires a powerful shot that gains extra range and damage the longer he spends preparing to fire.","tooltip":"First Cast: Varus starts drawing back his next shot, gradually increasing its range and damage. While preparing to shoot Varus' Movement Speed is slowed by {{ e7 }}%. After {{ e5 }} seconds, Piercing Arrow fails but refunds {{ e4 }}% of its Mana cost.

    Second Cast: Varus fires, dealing {{ e1 }} (+{{ a1 }}) to {{ e2 }} (+{{ a2 }}) physical damage, reduced by {{ e3 }}% per enemy hit (minimum {{ e9 }}%).

    Piercing Arrow's cooldown is reduced by {{ f1 }} seconds if the arrow detonates Blight stacks from at least one enemy champion.","leveltip":{"label":["Max Damage","Cooldown","Mana Cost"],"effect":["{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[10,47,84,120,157],[15,70,125,180,235],[15,15,15,15,15],[50,50,50,50,50],[4,4,4,4,4],[4,4,4,4,4],[20,20,20,20,20],[0,0,0,0,0],[33,33,33,33,33],[0,0,0,0,0]],"effectBurn":[null,"10/47/84/120/157","15/70/125/180/235","15","50","4","4","20","0","33","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"},{"link":"attackdamage","coeff":1.5,"key":"a2"},{"link":"attackdamage","coeff":1,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"VarusQ.png","sprite":"spell12.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VarusW","name":"Blighted Quiver","description":"Varus' basic attacks deal bonus magic damage and apply Blight. Varus' other abilities detonate Blight, dealing magic damage based on the target's maximum Health.","tooltip":"Varus' basic attacks deal {{ e1 }} (+{{ a1 }}) bonus magic damage and apply Blight for {{ e3 }} seconds (stacks {{ e4 }} times).

    Varus' other abilities detonate Blight, dealing magic damage equal to {{ e2 }}% (+{{ a2 }}%) of the target's maximum Health per stack. Max damage vs. monsters: {{ e5 }} per stack.","leveltip":{"label":["Initial Damage","Max Health Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[10,14,18,22,26],[2,2.75,3.5,4.25,5],[6,6,6,6,6],[3,3,3,3,3],[120,120,120,120,120],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/14/18/22/26","2/2.75/3.5/4.25/5","6","3","120","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.25,"key":"a1"},{"link":"spelldamage","coeff":0.02,"key":"a2"}],"costType":"Passive","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"VarusW.png","sprite":"spell12.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"Passive"},{"id":"VarusE","name":"Hail of Arrows","description":"Varus fires a hail of arrows that deal physical damage and desecrate the ground. Desecrated ground slows enemies' Movement Speed and reduces their self healing and regeneration. ","tooltip":"Varus fires a hail of arrows that deals {{ e1 }} (+{{ a1 }}) physical damage and desecrates the ground for {{ e3 }} seconds.

    Desecrated Ground slows enemy Movement Speed by {{ e2 }}% and reduces healing effects by {{ e4 }}%.","leveltip":{"label":["Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[18,16,14,12,10],"cooldownBurn":"18/16/14/12/10","cost":[80,80,80,80,80],"costBurn":"80","effect":[null,[65,100,135,170,205],[25,30,35,40,45],[4,4,4,4,4],[40,40,40,40,40],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/100/135/170/205","25/30/35/40/45","4","40","300","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[925,925,925,925,925],"rangeBurn":"925","image":{"full":"VarusE.png","sprite":"spell12.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VarusR","name":"Chain of Corruption","description":"Varus flings out a damaging tendril of corruption that immobilizes the first enemy champion hit and then spreads towards nearby uninfected champions, immobilizing them too on contact. ","tooltip":"Varus flings out a tendril of corruption that deals {{ e1 }} (+{{ a1 }}) magic damage and immobilizes the first enemy champion hit for {{ e2 }} seconds.

    The corruption then spreads towards nearby uninfected enemy champions. If it reaches them, they take the same damage and are also immobilized. Immobilized units gain {{ e4 }} Blight stacks over the duration.","leveltip":{"label":["Damage ","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,90,70],"cooldownBurn":"110/90/70","cost":[100,100,100],"costBurn":"100","effect":[null,[100,175,250],[2,2,2],[650,650,650],[3,3,3],[0.5,0.5,0.5],[600,600,600],[1.75,1.75,1.75],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/175/250","2","650","3","0.5","600","1.75","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1200,1200,1200],"rangeBurn":"1200","image":{"full":"VarusR.png","sprite":"spell12.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Vayne":{"id":67,"key":"Vayne","name":"Vayne","title":"the Night Hunter","spells":[{"id":"VayneTumble","name":"Tumble","description":"Vayne tumbles, maneuvering to carefully place her next shot. Her next attack deals bonus damage.","tooltip":"Rolls a short distance. The next basic attack within {{ e1 }} seconds deals {{ f1 }} bonus physical damage, equal to {{ e2 }}% of total Attack Damage.","leveltip":{"label":["Bonus Damage","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[6,5,4,3,2],"cooldownBurn":"6/5/4/3/2","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[7,7,7,7,7],[30,35,40,45,50],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"7","30/35/40/45/50","4","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":[0.3,0.35,0.4,0.45,0.5],"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"VayneTumble.png","sprite":"spell12.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VayneSilveredBolts","name":"Silver Bolts","description":"Vayne tips her bolts with a rare metal, toxic to evil things. The third consecutive attack or ability against the same target deals a percentage of the target's maximum Health as bonus true damage. (Max: 200 damage vs. Monsters)","tooltip":"Every third consecutive attack or ability against an enemy deals an additional {{ e1 }}% of the enemy's maximum Health as true damage.

    Deals a maximum of 200 damage vs. Monsters and a minimum of {{ e2 }} damage vs. all targets.","leveltip":{"label":["Max Health Damage","Minimum Damage"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[6,7.5,9,10.5,12],[40,60,80,100,120],[200,200,200,200,200],[3.5,3.5,3.5,3.5,3.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"6/7.5/9/10.5/12","40/60/80/100/120","200","3.5","0","0","0","0","0","0"],"vars":[],"costType":"Passive","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"VayneSilveredBolts.png","sprite":"spell12.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"Passive"},{"id":"VayneCondemn","name":"Condemn","description":"Vayne draws a heavy crossbow from her back, and fires a huge bolt at her target, knocking them back and dealing damage. If they collide with terrain, they are impaled, dealing bonus damage and stunning them.","tooltip":"Fires a bolt that knocks back target enemy and deals {{ e1 }} (+{{ f1 }}) physical damage. Enemies that collide with terrain take {{ e2 }} (+{{ f1 }}) additional physical damage and are stunned for {{ e3 }} seconds.","leveltip":{"label":["Initial Damage","Collision Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[90,90,90,90,90],"costBurn":"90","effect":[null,[45,80,115,150,185],[45,80,115,150,185],[1.5,1.5,1.5,1.5,1.5],[475,475,475,475,475],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"45/80/115/150/185","45/80/115/150/185","1.5","475","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.5,"key":"f1"},{"link":"bonusattackdamage","coeff":0.5,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[710,710,710,710,710],"rangeBurn":"710","image":{"full":"VayneCondemn.png","sprite":"spell12.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VayneInquisition","name":"Final Hour","description":"Readying herself for an epic confrontation, Vayne gains increased Attack Damage, Invisibility during Tumble, and triple the bonus Movement Speed from Night Hunter.","tooltip":"Gains {{ e1 }} Bonus Attack Damage for {{ e2 }} seconds. While active, Tumble grants Invisibility for {{ e3 }} second, and Night Hunter's bonus Movement Speed is increased to {{ e4 }}.

    16","leveltip":{"label":["Bonus Attack Damage","Duration"],"effect":["{{ cooldown }} -> {{ cooldownNL }}","{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":3,"cooldown":[100,85,70],"cooldownBurn":"100/85/70","cost":[80,80,80],"costBurn":"80","effect":[null,[30,50,70],[8,10,12],[1,1,1],[90,90,90],[100,85,70],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"30/50/70","8/10/12","1","90","100/85/70","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1,1,1],"rangeBurn":"1","image":{"full":"VayneInquisition.png","sprite":"spell12.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Veigar":{"id":45,"key":"Veigar","name":"Veigar","title":"the Tiny Master of Evil","spells":[{"id":"VeigarBalefulStrike","name":"Baleful Strike","description":"Veigar unleashes a bolt of dark energy that deals magic damage to the first two enemies hit. Units killed by this bolt grant Veigar some ability power permanently.","tooltip":"Unleashes a bolt of dark energy, dealing {{ e1 }} (+{{ a1 }}) magic damage to the first two enemies hit.

    Killing a unit with this also grants Veigar a stack of Phenomenal Evil. Large minions and large monsters grant two.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[70,110,150,190,230],[1,1,1,1,1],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","1","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"VeigarBalefulStrike.png","sprite":"spell12.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VeigarDarkMatter","name":"Dark Matter","description":"Veigar calls a great mass of dark matter to fall from the sky to the target location, dealing magic damage when it lands.","tooltip":"After 1.2 seconds, dark matter falls from the sky to the target location, dealing {{ e1 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9.5,9,8.5,8],"cooldownBurn":"10/9.5/9/8.5/8","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[100,150,200,250,300],[1.2,1.2,1.2,1.2,1.2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100/150/200/250/300","1.2","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"VeigarDarkMatter.png","sprite":"spell12.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VeigarEventHorizon","name":"Event Horizon","description":"After a brief delay Veigar twists the edges of space around the target location for 3 seconds, stunning enemies who pass through the perimeter.","tooltip":"After a {{ e2 }} second delay, Veigar twists the edges of space around the target location for 3 seconds, forming a pentagon of walls. Enemies who attempt to pass through the perimeter are stopped and stunned for {{ e1 }} seconds.","leveltip":{"label":["Stun Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[18,17,16,15,14],"cooldownBurn":"18/17/16/15/14","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[1.5,1.75,2,2.25,2.5],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"1.5/1.75/2/2.25/2.5","0.5","0","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[725,725,725,725,725],"rangeBurn":"725","image":{"full":"VeigarEventHorizon.png","sprite":"spell12.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VeigarR","name":"Primordial Burst","description":"Blasts target enemy champion, dealing a large amount of magic damage, increasing based on the target's missing health.","tooltip":"Blasts the target with primal magic to deal {{ e1 }} (+{{ a1 }}) to {{ e2 }} (+{{ a2 }}) magic damage, increasing based on the target's missing health.

    Damage is maximized against enemies below 33% health.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[175,250,325],[350,500,650],[1,1,1],[2,2,2],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"175/250/325","350/500/650","1","2","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"},{"link":"spelldamage","coeff":1.5,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[650,650,650],"rangeBurn":"650","image":{"full":"VeigarR.png","sprite":"spell12.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Velkoz":{"id":161,"key":"Velkoz","name":"Vel'Koz","title":"the Eye of the Void","spells":[{"id":"VelkozQ","name":"Plasma Fission","description":"Vel'Koz shoots a bolt of plasma that splits in two on reactivation or upon hitting an enemy. The bolt slows and damages on hit.","tooltip":"Vel'Koz shoots a plasma bolt that deals {{ e1 }} (+{{ a1 }}) Magic Damage and applies a slow of {{ e5 }}% that decays over {{ e4 }} second(s).

    Upon reactivation or upon hitting an enemy, the bolt splits at a 90 degree angle.

    Killing a unit with this refunds {{ e2 }}% of the Mana Cost.","leveltip":{"label":["Damage","Slow Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[7,7,7,7,7],"cooldownBurn":"7","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[80,120,160,200,240],[50,50,50,50,50],[0.25,0.25,0.25,0.25,0.25],[1,1.4,1.8,2.2,2.6],[70,70,70,70,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","50","0.25","1/1.4/1.8/2.2/2.6","70","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"VelkozQ.png","sprite":"spell12.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VelkozW","name":"Void Rift","description":"Vel'Koz opens a rift to the void that deals an initial burst of damage, then explodes for a second burst of damage after a delay.","tooltip":"Vel'Koz opens a rift to the void that deals {{ e1 }} (+{{ a1 }}) Magic Damage. After a delay, it deals an additional {{ e2 }} (+{{ a2 }}) Magic Damage.

    Void Rift has a 2 second cooldown between casts.","leveltip":{"label":["Initial Damage","Secondary Damage","Ammo Recharge Time","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[1.5,1.5,1.5,1.5,1.5],"cooldownBurn":"1.5","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[30,50,70,90,110],[45,75,105,135,165],[100,100,100,100,100],[0,0,0,0,0],[0.25,0.25,0.25,0.25,0.25],[0.5,0.5,0.5,0.5,0.5],[88,88,88,88,88],[500,500,500,500,500],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/50/70/90/110","45/75/105/135/165","100","0","0.25","0.5","88","500","0","0"],"vars":[{"link":"spelldamage","coeff":0.15,"key":"a1"},{"link":"spelldamage","coeff":0.25,"key":"a2"}],"costType":" Mana","maxammo":"2","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"VelkozW.png","sprite":"spell12.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VelkozE","name":"Tectonic Disruption","description":"Vel'Koz causes an area to explode, knocking up enemies, and knocking close enemies slightly away.","tooltip":"Vel'Koz disrupts a nearby area which, after a delay, deals {{ e1 }} (+{{ a1 }}) Magic Damage and knocks up enemies hit for {{ e2 }} seconds.

    Any enemies hit that are close to Vel'Koz will be pushed slightly in the direction the ability was cast.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[70,100,130,160,190],[0.75,0.75,0.75,0.75,0.75],[0,0,0,0,0],[225,225,225,225,225],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/100/130/160/190","0.75","0","225","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[810,810,810,810,810],"rangeBurn":"810","image":{"full":"VelkozE.png","sprite":"spell12.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VelkozR","name":"Life Form Disintegration Ray","description":"Vel'Koz unleashes a channelled beam that follows the cursor for 2.5 seconds that deals magic damage. Organic Deconstruction Researches enemy champions causing them to take true damage instead.","tooltip":"Passive: Deconstructing enemy Champions Researches them for {{ e2 }} seconds. Basic attacks and all abilities refresh Research.

    Active: Vel'Koz channels a ray of energy that follows the cursor for 2.5 seconds, dealing Magic Damage up to a total of {{ e1 }} (+{{ a1 }}) and slowing units hit by {{ e3 }}%.

    Enemies that stay in the beam will periodically gain Organic Deconstruction stacks. Deals True Damage instead of Magic against Researched units.","leveltip":{"label":["Total Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[100,100,100],"costBurn":"100","effect":[null,[450,625,800],[7,7,7],[20,20,20],[40,40,40],[175,175,175],[7,7,7],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"450/625/800","7","20","40","175","7","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1.25,"key":"a1"}],"costType":"Mana","maxammo":"-1","range":[1575,1575,1575],"rangeBurn":"1575","image":{"full":"VelkozR.png","sprite":"spell12.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"Mana"}]},"Vi":{"id":254,"key":"Vi","name":"Vi","title":"the Piltover Enforcer","spells":[{"id":"ViQ","name":"Vault Breaker","description":"Vi charges her gauntlets and unleashes a vault shattering punch, carrying her forward. Enemies she hits are knocked back and receive a stack of Denting Blows.","tooltip":"Charges a powerful punch that carries Vi forward.

    First Cast: Slows Movement Speed by {{ e4 }}% while increasing damage and dash range over 1.25 seconds.

    Second Cast: Dashes forward dealing {{ e1 }} (+{{ a1 }}) to {{ f2 }} (+{{ f1 }}) physical damage and applying Denting Blows to all enemies hit (deals {{ e3 }}% damage to minions and monsters). Stops upon colliding with an enemy champion, knocking it back.","leveltip":{"label":["Min Damage","Max Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,14,12,10,8],"cooldownBurn":"16/14/12/10/8","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[100,150,200,250,300],[2,2,2,2,2],[75,75,75,75,75],[15,15,15,15,15],[6,6,6,6,6],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"100/150/200/250/300","2","75","15","6","5","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"},{"link":"bonusattackdamage","coeff":1.4,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[250,250,250,250,250],"rangeBurn":"250","image":{"full":"ViQ.png","sprite":"spell12.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ViW","name":"Denting Blows","description":"Vi's punches break her opponent's Armor, dealing bonus damage and granting her Attack Speed.","tooltip":"Every 3rd attack on the same target deals an additional {{ e1 }}% (+{{ f1 }}%) of the target's maximum Health as physical damage, reduces its Armor by {{ e3 }}% and grants Vi {{ e2 }}% Attack Speed for {{ e4 }} seconds (max {{ e5 }} damage vs. monsters).","leveltip":{"label":["Max Health Damage","Attack Speed"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[4,5.5,7,8.5,10],[30,35,40,45,50],[20,20,20,20,20],[4,4,4,4,4],[300,300,300,300,300],[4,4,4,4,4],[0.0286,0.0286,0.0286,0.0286,0.0286],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/5.5/7/8.5/10","30/35/40/45/50","20","4","300","4","0.03","0","0","0"],"vars":[{"link":"@special.viw","coeff":35,"key":"f1"}],"costType":"Passive","maxammo":"-1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"ViW.png","sprite":"spell12.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"Passive"},{"id":"ViE","name":"Excessive Force","description":"Vi's next attack blasts through her target, dealing damage to enemies behind it.","tooltip":"Causes next basic attack to deal {{ e1 }} (+{{ f3 }}) (+{{ a1 }}) physical damage to the target and enemies behind it.

    Vi charges a new punch every {{ f1 }} seconds and can hold 2 charges at once.","leveltip":{"label":["Damage","Charge Time","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f1 }} -> {{ f2 }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[1,1,1,1,1],"cooldownBurn":"1","cost":[40,45,50,55,60],"costBurn":"40/45/50/55/60","effect":[null,[10,30,50,70,90],[1.15,1.15,1.15,1.15,1.15],[50,50,50,50,50],[1.5,1.5,1.5,1.5,1.5],[6,6,6,6,6],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"10/30/50/70/90","1.15","50","1.5","6","1","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.15,"key":"f3"},{"link":"spelldamage","coeff":0.7,"key":"a1"},{"link":"@text","coeff":[14,12.5,11,9.5,8],"key":"f1"},{"link":"@text","coeff":[14,12.5,11,9.5,8],"key":"f1"}],"costType":" Mana","maxammo":"2","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"ViE.png","sprite":"spell12.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ViR","name":"Assault and Battery","description":"Vi runs down an enemy, knocking aside anyone in the way. When she reaches her target she knocks it into the air, jumps after it, and slams it back into the ground.","tooltip":"Targets an enemy champion and chases it down, knocking it up for 1.25 seconds, dealing {{ e1 }} (+{{ a1 }}) physical damage.

    While charging, Vi cannot be stopped and has True Sight of the target. Any enemies in the way are knocked aside and dealt 75% of the damage.","leveltip":{"label":["Total Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"," {{ cost }} -> {{ costNL }}"]},"maxrank":3,"cooldown":[150,115,80],"cooldownBurn":"150/115/80","cost":[100,125,150],"costBurn":"100/125/150","effect":[null,[150,300,450],[0.75,0.75,0.75],[1.25,1.25,1.25],[4,4,4],[1,1.5,2],[800,800,800],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/300/450","0.75","1.25","4","1/1.5/2","800","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[800,800,800],"rangeBurn":"800","image":{"full":"ViR.png","sprite":"spell12.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Viktor":{"id":112,"key":"Viktor","name":"Viktor","title":"the Machine Herald","spells":[{"id":"ViktorPowerTransfer","name":"Siphon Power","description":"Viktor blasts an enemy unit dealing magic damage, gaining a shield and empowering his next basic attack.

    Augment: Viktor gains bonus Movement Speed after casting.","tooltip":"Viktor blasts an enemy unit, dealing {{ e1 }} (+{{ a1 }}) magic damage while granting Viktor a shield that absorbs up to {{ f1 }} (+{{ f2 }}) damage over the next {{ e2 }} seconds.

    Viktor's next basic attack deals {{ e5 }} (+{{ a2 }}) bonus magic damage.

    Augment - Turbocharge: Viktor gains {{ e3 }}% Movement Speed for {{ e2 }} seconds.","leveltip":{"label":["Damage (missile)","Damage (attack)","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[8,7,6,5,4],"cooldownBurn":"8/7/6/5/4","cost":[45,50,55,60,65],"costBurn":"45/50/55/60/65","effect":[null,[60,80,100,120,140],[2.5,2.5,2.5,2.5,2.5],[30,30,30,30,30],[0,0,0,0,0],[20,40,60,80,100],[0.08,0.08,0.08,0.08,0.08],[0.15,0.15,0.15,0.15,0.15],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/80/100/120/140","2.5","30","0","20/40/60/80/100","0.08","0.15","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.5,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"ViktorPowerTransfer.png","sprite":"spell12.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ViktorGravitonField","name":"Gravity Field","description":"Viktor conjures a heavy gravitational field that slows enemies in its radius. Enemies who stay within the device for too long are stunned.

    Augment: Enemies stunned by Gravity Field are dragged to the center.","tooltip":"Viktor deploys a gravitational imprisonment device for {{ e3 }} seconds, slowing enemy units by {{ e1 }}% and adding a stack every {{ e4 }} seconds. At {{ e5 }} stacks the target is stunned for {{ e2 }} seconds.

    Augment - Implosion: Enemies stunned by Gravity Field are dragged to the center.","leveltip":{"label":["Slow","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[17,16,15,14,13],"cooldownBurn":"17/16/15/14/13","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[28,32,36,40,44],[1.5,1.5,1.5,1.5,1.5],[4,4,4,4,4],[0.5,0.5,0.5,0.5,0.5],[3,3,3,3,3],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"28/32/36/40/44","1.5","4","0.5","3","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"ViktorGravitonField.png","sprite":"spell12.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ViktorDeathRay","name":"Death Ray","description":"Viktor uses his robotic arm to fire a chaos beam that cuts across the field in a line, dealing damage to all enemies in its path.

    Augment: An explosion follows the Death Ray's wake, dealing magic damage.","tooltip":"Viktor uses his robotic arm to fire a chaos beam that cuts across the field in a line, dealing {{ e1 }} (+{{ a1 }}) magic damage to every enemy in its path.

    Augment - Aftershock: An explosion follows the Death Ray's wake, dealing {{ e2 }} (+{{ a2 }}) magic damage.","leveltip":{"label":["Damage (Death Ray)","Damage (Aftershock)","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12,11,10,9],"cooldownBurn":"13/12/11/10/9","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[70,110,150,190,230],[20,60,100,140,180],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","20/60/100/140/180","1","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.7,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[525,525,525,525,525],"rangeBurn":"525","image":{"full":"ViktorDeathRay.png","sprite":"spell12.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ViktorChaosStorm","name":"Chaos Storm","description":"Viktor conjures a singularity on the field which deals magic damage and interrupts enemy channels. The singularity then periodically does magic damage to all nearby enemies. Viktor can redirect the singularity.

    Augment: The Chaos Storm moves faster.","tooltip":"Viktor conjures a singularity at target location, dealing {{ e1 }} (+{{ a1 }}) magic damage and interrupting enemy channels.

    Viktor can redirect the singularity for {{ e3 }} seconds, which will discharge {{ e2 }} (+{{ a2 }}) magic damage every {{ e4 }} seconds to nearby enemies. The singularity will move slower as it attempts to move farther away from Viktor.

    Augment - Velocity: Chaos Storm moves {{ e0 }}% faster.","leveltip":{"label":["Damage (impact)","Damage (discharge)","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[100,175,250],[150,250,350],[6.5,6.5,6.5],[2,2,2],[3,3,3],[400,400,400],[200,200,200],[300,300,300],[900,900,900],[0.2,0.2,0.2]],"effectBurn":[null,"100/175/250","150/250/350","6.5","2","3","400","200","300","900","0.2"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.6,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[700,700,700],"rangeBurn":"700","image":{"full":"ViktorChaosStorm.png","sprite":"spell12.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Vladimir":{"id":8,"key":"Vladimir","name":"Vladimir","title":"the Crimson Reaper","spells":[{"id":"VladimirQ","name":"Transfusion","description":"Vladimir steals life from the target enemy. When Vladimir's resource is full, Transfusion will benefit from massively increased damage and healing for a brief time.","tooltip":"Vladimir drains the lifeforce of his target, dealing {{ e1 }} (+{{ a1 }}) magic damage and healing himself for {{ e2 }} (+{{ a2 }}) health. After casting Transfusion twice, Vladimir gains Crimson Rush the next time it becomes available to cast.

    Crimson Rush: Vladimir is briefly hasted and for the next {{ e8 }} seconds Transfusion deals {{ e7 }}% increased damage and heals for an additional {{ f7 }} plus {{ e5 }}% (+{{ f6 }}%) of his missing health (empowered heal has {{ e9 }}% effectiveness against minions). ","leveltip":{"label":["Damage","Heal","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[80,100,120,140,160],[20,25,30,35,40],[0,0,0,0,0],[0,0,0,0,0],[5,5,5,5,5],[0.04,0.04,0.04,0.04,0.04],[85,85,85,85,85],[2.5,2.5,2.5,2.5,2.5],[35,35,35,35,35],[0,0,0,0,0]],"effectBurn":[null,"80/100/120/140/160","20/25/30/35/40","0","0","5","0.04","85","2.5","35","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"},{"link":"spelldamage","coeff":0.35,"key":"a2"}],"costType":"No Cost","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"VladimirQ.png","sprite":"spell12.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"No Cost"},{"id":"VladimirSanguinePool","name":"Sanguine Pool","description":"Vladimir sinks into a pool of blood, becoming untargetable for 2 seconds. Additionally, enemies on the pool are slowed and Vladimir siphons life from them.","tooltip":"Vladimir sinks into a pool of blood for 2 seconds, gaining a brief haste and becoming untargetable while slowing enemies above him by {{ e3 }}%.

    Vladimir deals {{ e1 }} (+{{ f1 }}) [{{ e4 }}% of bonus Health] magic damage over the duration and heals himself for {{ e5 }}% of that amount.

    Sanguine Pool can be cast while charging Tides of Blood.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[28,25,22,19,16],"cooldownBurn":"28/25/22/19/16","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[80,135,190,245,300],[20,20,20,20,20],[40,40,40,40,40],[10,10,10,10,10],[15,15,15,15,15],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/135/190/245/300","20","40","10","15","0","0","0","0","0"],"vars":[{"link":"bonushealth","coeff":0.15,"key":"f1"}],"costType":"% of current Health","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"VladimirSanguinePool.png","sprite":"spell12.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ e2 }}% of current Health"},{"id":"VladimirE","name":"Tides of Blood","description":"Vladimir pays his own health to charge up a reservoir of blood which, when released, deals damage in the area around him but can be blocked by enemy units.","tooltip":"First Cast: Vladimir charges up a reservoir of blood, spending up to {{ e2 }}% of his max Health ({{ f3 }}) to increase this spell's damage. While at full charge, Vladimir is slowed.

    Second Cast: Vladimir unleashes a nova of blood at surrounding enemies which deals between {{ e3 }} (+{{ f2 }}) (+{{ a1 }}) and {{ e0 }} (+{{ f4 }}) (+{{ a2 }}) magic damage and, at full charge, briefly slows by {{ e9 }}%. Targets hit block a portion of the nova.

    Tides of Blood will release automatically if held for more than {{ e7 }} seconds.","leveltip":{"label":["Min Damage","Max Damage","Slow Amount","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e0 }} -> {{ e0NL }}","{{ e9 }}% -> {{ e9NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[13,11,9,7,5],"cooldownBurn":"13/11/9/7/5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[0,0,0,0,0],[8,8,8,8,8],[30,45,60,75,90],[6,6,6,6,6],[150,150,150,150,150],[0,0,0,0,0],[1.5,1.5,1.5,1.5,1.5],[1,1,1,1,1],[40,45,50,55,60],[60,90,120,150,180]],"effectBurn":[null,"0","8","30/45/60/75/90","6","150","0","1.5","1","40/45/50/55/60","60/90/120/150/180"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"},{"link":"spelldamage","coeff":1,"key":"a2"}],"costType":"% of max Health","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"VladimirE.png","sprite":"spell12.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"Channeling Cost {{ e2 }}% of max Health"},{"id":"VladimirHemoplague","name":"Hemoplague","description":"Vladimir infects an area with a virulent plague. Affected enemies take increased damage for the duration. After a few seconds, Hemoplague deals magic damage to infected enemies and heals Vladimir for each enemy Champion hit.","tooltip":"Vladimir infects enemies in a target area with a virulent plague, causing them to take {{ e2 }}% increased damage from all sources for {{ e4 }} seconds.

    After {{ e4 }} seconds, Vladimir deals {{ e1 }} (+{{ a1 }}) magic damage to all infected targets. If Hemoplague damages an enemy Champion, Vladimir heals himself for {{ f4 }} (+{{ f3 }}), plus {{ e5 }}% for each Champion beyond the first.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[150,135,120],"cooldownBurn":"150/135/120","cost":[0,0,0],"costBurn":"0","effect":[null,[150,250,350],[10,10,10],[100,100,100],[4,4,4],[50,50,50],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","10","100","4","50","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[625,625,625],"rangeBurn":"625","image":{"full":"VladimirHemoplague.png","sprite":"spell12.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"No Cost"}]},"Volibear":{"id":106,"key":"Volibear","name":"Volibear","title":"the Thunder's Roar","spells":[{"id":"VolibearQ","name":"Rolling Thunder","description":"Volibear drops to all fours and runs faster. This bonus speed increases when chasing enemy champions. The first enemy he attacks is thrown backwards over Volibear.","tooltip":"Volibear drops to all fours to hunt his enemies, gaining {{ e4 }}% Movement Speed for {{ e3 }} seconds. This bonus is enhanced to {{ e2 }}% Movement Speed while moving toward enemy champions.

    Volibear's next attack during this time deals an additional {{ e1 }} physical damage and flings the target behind him.","leveltip":{"label":["Bonus Damage","Enhanced Movement Speed","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[30,60,90,120,150],[30,35,40,45,50],[4,4,4,4,4],[15,15,15,15,15],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/60/90/120/150","30/35/40/45/50","4","15","400","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"VolibearQ.png","sprite":"spell12.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VolibearW","name":"Frenzy","description":"Volibear's repeated attacks grant him additional Attack Speed. Once Volibear has repeatedly attacked three times, he can perform a vicious bite on his target which deals increased damage based on the target's missing Health.","tooltip":"Passive: Volibear gains {{ e1 }}% Attack Speed with each attack. Stacks up to {{ e2 }} times.

    Active: When Volibear has {{ e2 }} stacks of Frenzy, he can bite an enemy to deal {{ e3 }} (+{{ f1 }} [{{ e4 }}% of bonus Health]) physical damage, increased by {{ e5 }}% for each {{ e7 }}% Health the target is missing. If Volibear bites a monster, Frenzy's cooldown is reduced by {{ e6 }}%.","leveltip":{"label":["Attack Speed Bonus","Bite Damage"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[18,18,18,18,18],"cooldownBurn":"18","cost":[35,35,35,35,35],"costBurn":"35","effect":[null,[4,8,12,16,20],[3,3,3,3,3],[60,110,160,210,260],[15,15,15,15,15],[1,1,1,1,1],[50,50,50,50,50],[1,1,1,1,1],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4/8/12/16/20","3","60/110/160/210/260","15","1","50","1","0","0","0"],"vars":[{"link":"bonushealth","coeff":0.15,"key":"f1"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"VolibearW.png","sprite":"spell12.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VolibearE","name":"Majestic Roar","description":"Volibear lets out a powerful roar that damages and slows enemies. Minions and monsters are feared as well.","tooltip":"Volibear deals {{ e2 }} (+{{ a1 }}) magic damage to nearby enemies and slows them by {{ e1 }}% for {{ e3 }} seconds.

    Minions and monsters are feared as well.","leveltip":{"label":["Damage","Slow Amount ","Mana Cost "],"effect":["{{ e2 }} -> {{ e2NL }}","{{ e1 }}% -> {{ e1NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[11,11,11,11,11],"cooldownBurn":"11","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[30,35,40,45,50],[60,105,150,195,240],[3,3,3,3,3],[49,49,49,49,49],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/35/40/45/50","60/105/150/195/240","3","49","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[425,425,425,425,425],"rangeBurn":"425","image":{"full":"VolibearE.png","sprite":"spell12.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"VolibearR","name":"Thunder Claws","description":"Volibear erupts with chain lightning, damaging a number nearby enemies. The power of the storm causes Volibear's attacks to blast his targets with lightning that bounces to other nearby enemies.","tooltip":"Volibear erupts with chain lightning, striking up to {{ e4 }} nearby enemies with {{ e1 }} (+{{ a1 }}) magic damage.

    For the next {{ e3 }} seconds, the storm empowers Volibear, causing his basic attacks to deal {{ e1 }} (+{{ a1 }}) bonus magic damage that chains to {{ e4 }} nearby enemies.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[100,90,80],"cooldownBurn":"100/90/80","cost":[100,100,100],"costBurn":"100","effect":[null,[75,115,155],[9,9,9],[12,12,12],[8,8,8],[0.08,0.08,0.08],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"75/115/155","9","12","8","0.08","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"VolibearR.png","sprite":"spell12.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Warwick":{"id":19,"key":"Warwick","name":"Warwick","title":"the Uncaged Wrath of Zaun","spells":[{"id":"WarwickQ","name":"Jaws of the Beast","description":"Warwick lunges forward and bites his target, dealing damage based on their maximum health and healing for damage dealt.","tooltip":"Warwick lunges forward and bites his target. While the key is held down, he will attach to the target then leap behind them.

    On release, deals {{ a1 }}+{{ a2 }} plus {{ e1 }}% of your target's maximum health as magic damage (applies On Hit effects). Heal {{ e3 }}% of damage dealt.

    16","leveltip":{"label":["Heal","Percent Health Damage","Cost"],"effect":["{{ e3 }}% -> {{ e3NL }}%","{{ e1 }}% -> {{ e1NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[50,60,70,80,90],"costBurn":"50/60/70/80/90","effect":[null,[6,7,8,9,10],[100,150,200,250,300],[30,40,50,60,70],[100,125,150,175,200],[450,450,450,450,450],[200,200,200,200,200],[300,300,300,300,300],[425,425,425,425,425],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"6/7/8/9/10","100/150/200/250/300","30/40/50/60/70","100/125/150/175/200","450","200","300","425","0","0"],"vars":[{"link":"attackdamage","coeff":1.2,"key":"a1"},{"link":"spelldamage","coeff":0.9,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"WarwickQ.png","sprite":"spell12.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"WarwickW","name":"Blood Hunt","description":"Warwick senses enemies below 50% health, gaining movement speed toward and attack speed against them. When they fall below 20% health, he frenzies and these bonuses triple.","tooltip":"Passive: Warwick gains {{ e2 }}% attack speed against enemies below 50% health. He also senses low health champions globally, moving {{ e1 }}% faster toward them when out of combat. These bonuses are tripled against enemies below 20% health.

    Active: Warwick briefly senses all enemies. The nearest sensed champion is Blood Hunted for 8 seconds. Cast only while not in combat with a champion.

    While no enemies are being hunted, Blood Hunt cools down twice as fast.","leveltip":{"label":["Movement Speed","Attack Speed","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[120,105,90,75,60],"cooldownBurn":"120/105/90/75/60","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[35,40,45,50,55],[50,65,80,95,110],[10,15,20,25,30],[80,90,100,110,120],[30,30,30,30,30],[8,8,8,8,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/40/45/50/55","50/65/80/95/110","10/15/20/25/30","80/90/100/110/120","30","8","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"2","range":[4000,4000,4000,4000,4000],"rangeBurn":"4000","image":{"full":"WarwickW.png","sprite":"spell12.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"WarwickE","name":"Primal Howl","description":"Warwick gains damage reduction for 2.5 seconds. At the end, or if re-activated, he howls, causing nearby enemies to flee for 1 second.","tooltip":"Warwick gains {{ e1 }}% damage reduction for 2.5 seconds. At the end of the duration, or if activated again, Warwick howls, causing all nearby enemies to flee for {{ e3 }} second.","leveltip":{"label":["Damage Reduction","Cooldown"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[15,14,13,12,11],"cooldownBurn":"15/14/13/12/11","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[35,40,45,50,55],[2.75,2.75,2.75,2.75,2.75],[1,1,1,1,1],[1,1,1,1,1],[800,800,800,800,800],[400,400,400,400,400],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"35/40/45/50/55","2.75","1","1","800","400","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[375,375,375,375,375],"rangeBurn":"375","image":{"full":"WarwickE.png","sprite":"spell12.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"WarwickR","name":"Infinite Duress","description":"Warwick leaps in a direction (scaling with his bonus movement speed), suppressing the first champion he collides with for 1.5 seconds.","tooltip":"Warwick leaps 2.5 seconds worth of movement speed in a direction, suppressing the first champion he collides with for {{ e2 }} seconds. Deals {{ e7 }} (+{{ a1 }}) magic damage and applies on-hit effects 3 times. Warwick heals for 100% of all damage he deals during Infinite Duress.","leveltip":{"label":["Base Damage","Cooldown"],"effect":["{{ e7 }} -> {{ e7NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[110,90,70],"cooldownBurn":"110/90/70","cost":[100,100,100],"costBurn":"100","effect":[null,[150,250,350],[1.5,1.5,1.5],[30,30,30],[5,5,5],[20,20,20],[0,0,0],[175,350,525],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"150/250/350","1.5","30","5","20","0","175/350/525","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.67,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[25000,25000,25000],"rangeBurn":"25000","image":{"full":"WarwickR.png","sprite":"spell13.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Xayah":{"id":498,"key":"Xayah","name":"Xayah","title":"the Rebel","spells":[{"id":"XayahQ","name":"Double Daggers","description":"Xayah throws two damaging daggers that also drop Feathers she can recall.","tooltip":"Xayah throws two knives dealing {{ f1 }} (+{{ f2 }}) physical damage and leaving two Feathers. Targets hit after the first take {{ e2 }}% damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[10,9,8,7,6],"cooldownBurn":"10/9/8/7/6","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[80,120,160,200,240],[50,50,50,50,50],[0.334,0.334,0.334,0.334,0.334],[0.584,0.584,0.584,0.584,0.584],[3500,3500,3500,3500,3500],[3500,3500,3500,3500,3500],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","50","0.33","0.58","3500","3500","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"XayahQ.png","sprite":"spell13.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XayahW","name":"Deadly Plumage","description":"Xayah creates a storm of blades that increase her basic attack speed and damage while granting her movement speed if she attacks a champion.","tooltip":"Xayah creates a storm of blades for {{ e2 }} seconds that grant her {{ e1 }}% attack speed and cause her basic attacks to strike an additional time for {{ e5 }}% damage.

    If Deadly Plumage strikes an enemy champion, Xayah gains {{ e3 }}% movement speed for {{ e4 }} seconds.

    If Rakan is nearby he will also gain the effects of Deadly Plumage.","leveltip":{"label":["Attack Speed","Cooldown","Cost"],"effect":["{{ e1 }}% -> {{ e1NL }}%","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[60,55,50,45,40],"costBurn":"60/55/50/45/40","effect":[null,[30,35,40,45,50],[4,4,4,4,4],[30,30,30,30,30],[1.5,1.5,1.5,1.5,1.5],[20,20,20,20,20],[1000,1000,1000,1000,1000],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/35/40/45/50","4","30","1.5","20","1000","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"XayahW.png","sprite":"spell13.png","group":"spell","x":96,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XayahE","name":"Bladecaller","description":"Xayah calls back all her dropped Feathers dealing damage and rooting enemies.","tooltip":"Xayah calls all Feathers back to her, dealing {{ e1 }} (+{{ f3 }}) (+{{ f4 }}) physical damage to enemies they pass through (Increased by Critical Strike Chance).

    Hitting an enemy with {{ e5 }} Feathers roots them for {{ e4 }} second.

    Minions take {{ e2 }}% damage from Bladecaller.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[50,60,70,80,90],[50,50,50,50,50],[0.1,0.1,0.1,0.1,0.1],[1,1,1,1,1],[3,3,3,3,3],[0.25,0.25,0.25,0.25,0.25],[0,0,0,0,0],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"50/60/70/80/90","50","0.1","1","3","0.25","0","0.5","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[2000,2000,2000,2000,2000],"rangeBurn":"2000","image":{"full":"XayahE.png","sprite":"spell13.png","group":"spell","x":144,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XayahR","name":"Featherstorm","description":"Xayah leaps into the air becoming untargetable and throwing out a fan of daggers, dropping Feathers she can recall.","tooltip":"Xayah leaps into the air becoming untargetable. She then rains down daggers which deal {{ e1 }} (+{{ a1 }}) physical damage and leave behind a line of Feathers.

    Xayah can move while in the air.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[140,125,110],"cooldownBurn":"140/125/110","cost":[100,100,100],"costBurn":"100","effect":[null,[100,150,200],[1,1,1],[1.25,1.25,1.25],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"100/150/200","1","1.25","0","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[450,450,450],"rangeBurn":"450","image":{"full":"XayahR.png","sprite":"spell13.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Xerath":{"id":101,"key":"Xerath","name":"Xerath","title":"the Magus Ascendant","spells":[{"id":"XerathArcanopulseChargeUp","name":"Arcanopulse","description":"Fires a long-range beam of energy, dealing magic damage to all targets hit.","tooltip":"First cast: Xerath charges Arcanopulse, gradually decreasing his Movement Speed while increasing the spell's range.

    Second cast: Xerath fires Arcanopulse, dealing {{ e1 }} (+{{ a1 }}) magic damage to all enemies in a line.

    While charging Arcanopulse, Xerath cannot attack or cast other spells. If Xerath does not fire the spell, half the Mana cost is refunded.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[80,90,100,110,120],"costBurn":"80/90/100/110/120","effect":[null,[80,120,160,200,240],[4,4,4,4,4],[0.5,0.5,0.5,0.5,0.5],[145,145,145,145,145],[0.5,0.5,0.5,0.5,0.5],[-0.2,-0.2,-0.2,-0.2,-0.2],[0.1,0.1,0.1,0.1,0.1],[0.5,0.5,0.5,0.5,0.5],[1.5,1.5,1.5,1.5,1.5],[0,0,0,0,0]],"effectBurn":[null,"80/120/160/200/240","4","0.5","145","0.5","-0.2","0.1","0.5","1.5","0"],"vars":[{"link":"spelldamage","coeff":0.75,"key":"a1"}],"costType":" Mana","maxammo":"1","range":[750,750,750,750,750],"rangeBurn":"750","image":{"full":"XerathArcanopulseChargeUp.png","sprite":"spell13.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XerathArcaneBarrage2","name":"Eye of Destruction","description":"Calls down a barrage of arcane energy, slowing and dealing magic damage to all enemies in an area. Targets in the middle receive additional damage and a stronger slow.","tooltip":"Xerath calls down a blast of arcane energy, dealing {{ e1 }} (+{{ a1 }}) magic damage to all enemies within the target area, slowing them by {{ e8 }}% for {{ e4 }} seconds. Enemies in the center of the blast take {{ f1 }} (+{{ f2 }}) magic damage and are slowed by {{ e3 }}%. This slow decays rapidly.","leveltip":{"label":["Damage","Cooldown","Slow Amount","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}","{{ e3 }}% -> {{ e3NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[1,1,1,1,1],"cooldownBurn":"1","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[60,90,120,150,180],[2,2,2,2,2],[60,65,70,75,80],[2.5,2.5,2.5,2.5,2.5],[0.5,0.5,0.5,0.5,0.5],[50,50,50,50,50],[50,50,50,50,50],[10,10,10,10,10],[0.8,0.8,0.8,0.8,0.8],[-0.15,-0.15,-0.15,-0.15,-0.15]],"effectBurn":[null,"60/90/120/150/180","2","60/65/70/75/80","2.5","0.5","50","50","10","0.8","-0.15"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"XerathArcaneBarrage2.png","sprite":"spell13.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XerathMageSpear","name":"Shocking Orb","description":"Deals magic damage to an enemy and stuns them.","tooltip":"Xerath fires an orb of raw magic. The first enemy hit takes {{ e1 }} (+{{ a1 }}) magic damage and is stunned for between {{ e8 }} and {{ e2 }} seconds. The stun duration lengthens based on how far the orb travels.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[13,12.5,12,11.5,11],"cooldownBurn":"13/12.5/12/11.5/11","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[80,110,140,170,200],[2,2,2,2,2],[0.17,0.17,0.17,0.17,0.17],[1125,1125,1125,1125,1125],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0],[0.5,0.5,0.5,0.5,0.5],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"80/110/140/170/200","2","0.17","1125","0.5","0","0","0.5","0","0"],"vars":[{"link":"spelldamage","coeff":0.45,"key":"a1"}],"costType":" Mana","maxammo":"1","range":[1050,1050,1050,1050,1050],"rangeBurn":"1050","image":{"full":"XerathMageSpear.png","sprite":"spell13.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XerathLocusOfPower2","name":"Rite of the Arcane","description":"Xerath immobilizes himself and gains numerous long-range barrages.","tooltip":"Xerath ascends to his true form, becoming rooted in place and gaining {{ e2 }} Arcane Barrages. This magic artillery deals {{ e3 }} (+{{ a1 }}) magic damage to all enemies hit.

    The root ends after {{ e1 }} seconds, when all shots have been fired or when manually deactivated by issuing a move command. If no barrages are fired, {{ e7 }}% of the cooldown is refunded.","leveltip":{"label":["Damage","Number of Shots","Range","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e2 }} -> {{ e2NL }}","{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,115,100],"cooldownBurn":"130/115/100","cost":[100,100,100],"costBurn":"100","effect":[null,[10,10,10],[3,4,5],[200,230,260],[200,200,200],[3200,4400,5600],[0.8,0.8,0.8],[50,50,50],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"10","3/4/5","200/230/260","200","3200/4400/5600","0.8","50","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.43,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[3200,4400,5600],"rangeBurn":"3200/4400/5600","image":{"full":"XerathLocusOfPower2.png","sprite":"spell13.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"XinZhao":{"id":5,"key":"XinZhao","name":"Xin Zhao","title":"the Seneschal of Demacia","spells":[{"id":"XenZhaoComboTarget","name":"Three Talon Strike","description":"Xin Zhao's next 3 standard attacks deal increased damage that reduce his other ability cooldowns, with the third attack knocking an opponent into the air.","tooltip":"Xin Zhao's next 3 basic attacks deal {{ e1 }} (+{{ a2 }}) physical damage and reduce his other abilities' cooldowns by 1 second. The final strike also knocks the target into the air for {{ e2 }} seconds.","leveltip":{"label":["Bonus Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[9,8,7,6,5],"cooldownBurn":"9/8/7/6/5","cost":[30,30,30,30,30],"costBurn":"30","effect":[null,[15,30,45,60,75],[0.75,0.75,0.75,0.75,0.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/30/45/60/75","0.75","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.2,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[375,375,375,375,375],"rangeBurn":"375","image":{"full":"XenZhaoComboTarget.png","sprite":"spell13.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XenZhaoBattleCry","name":"Battle Cry","description":"Xin Zhao passively will critically strike every third attack, healing on every third hit. This ability can be activated to attack faster.","tooltip":"Passive: Every third basic attack Xin Zhao critically strikes his target for {{ e7 }}% additional damage. On every third hit, he heals himself for {{ e1 }} (+{{ a1 }}) (+{{ a1 }}).

    Active: Xin Zhao unleashes a battle cry, increasing his Attack Speed by {{ e2 }}% for {{ e3 }} seconds.","leveltip":{"label":["Heal Amount","Bonus Damage","Attack Speed","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e7 }}% -> {{ e7NL }}%","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[16,15,14,13,12],"cooldownBurn":"16/15/14/13/12","cost":[40,40,40,40,40],"costBurn":"40","effect":[null,[30,35,40,45,50],[40,45,50,55,60],[5,5,5,5,5],[1,1,1,1,1],[-0.75,-0.625,-0.5,-0.375,-0.25],[0.3,0.3,0.3,0.3,0.3],[25,37.5,50,62.5,75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/35/40/45/50","40/45/50/55/60","5","1","-0.75/-0.625/-0.5/-0.375/-0.25","0.3","25/37.5/50/62.5/75","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.4,"key":"a1"},{"link":"spelldamage","coeff":0.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[20,20,20,20,20],"rangeBurn":"20","image":{"full":"XenZhaoBattleCry.png","sprite":"spell13.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XenZhaoSweep","name":"Audacious Charge","description":"Xin Zhao charges an enemy, dealing damage and slowing all enemies in the area.","tooltip":"Xin Zhao charges and challenges an enemy. The charge deals {{ e1 }} (+{{ a1 }}) magic damage to all nearby enemies and slows them by {{ e2 }}% for {{ e4 }} seconds. ","leveltip":{"label":["Damage","Slow %"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[60,60,60,60,60],"costBurn":"60","effect":[null,[70,110,150,190,230],[25,30,35,40,45],[0,0,0,0,0],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/110/150/190/230","25/30/35/40/45","0","2","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"XenZhaoSweep.png","sprite":"spell13.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"XenZhaoParry","name":"Crescent Sweep","description":"Xin Zhao deals damage to nearby enemies based on their current Health and knocks non-challenged targets back. Xin Zhao gains bonus Armor and Magic Resist based on number of champions hit.","tooltip":"Xin Zhao unleashes a sweep around him that deals {{ e1 }} (+{{ a1 }}) plus {{ e2 }}% of target's current Health in physical damage and stuns enemies while knocking them back (max 600 vs minions and monsters). Xin Zhao gains {{ e3 }} Armor and Magic Resist for 6 seconds for each champion hit.

    Challenge: If a challenged target is hit by the sweep, it is unaffected by the knockback.","leveltip":{"label":["Bonus Damage","Resistance Per Target","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,110,100],"cooldownBurn":"120/110/100","cost":[100,100,100],"costBurn":"100","effect":[null,[75,175,275],[15,15,15],[15,20,25],[7,10,13],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"75/175/275","15","15/20/25","7/10/13","0","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[500,500,500],"rangeBurn":"500","image":{"full":"XenZhaoParry.png","sprite":"spell13.png","group":"spell","x":96,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Yasuo":{"id":157,"key":"Yasuo","name":"Yasuo","title":"the Unforgiven","spells":[{"id":"YasuoQW","name":"Steel Tempest","description":"A skillshot basic attack. After two successful Steel Tempests, the next fires a tornado that knocks enemies Airborne.","tooltip":"Thrusts forward, dealing {{ e1 }} (+{{ a1 }}) physical damage.

    On hit, Steel Tempest grants a stack of Gathering Storm for 6 seconds. At 2 stacks, Steel Tempest fires a whirlwind that knocks Airborne.

    Steel Tempest is treated as a basic attack: It can critically strike, applies on-hit effects, is interruptible by crowd control and its cooldown and cast time are reduced by Attack Speed.

    If cast while dashing, Steel Tempest will strike as a circle.","leveltip":{"label":["Damage"],"effect":["{{ e1 }} -> {{ e1NL }}"]},"maxrank":5,"cooldown":[6,5.5,5,4.5,4],"cooldownBurn":"6/5.5/5/4.5/4","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,40,60,80,100],[100,100,100,100,100],[85,95,105,115,125],[4,4,4,4,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/40/60/80/100","100","85/95/105/115/125","4","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[475,475,475,475,475],"rangeBurn":"475","image":{"full":"YasuoQW.png","sprite":"spell13.png","group":"spell","x":144,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"YasuoWMovingWall","name":"Wind Wall","description":"Creates a moving wall that blocks enemy projectiles.","tooltip":"Creates a moving wall that blocks all enemy projectiles for 4 seconds.","leveltip":{"label":["Wall Width","Cooldown"],"effect":["{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[26,24,22,20,18],"cooldownBurn":"26/24/22/20/18","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[15,20,25,30,35],[60,90,120,150,180],[3,6,9,12,15],[300,350,400,450,500],[320,390,460,530,600],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"15/20/25/30/35","60/90/120/150/180","3/6/9/12/15","300/350/400/450/500","320/390/460/530/600","0","0","0","0","0"],"vars":[],"costType":"No Cost","maxammo":"-1","range":[400,400,400,400,400],"rangeBurn":"400","image":{"full":"YasuoWMovingWall.png","sprite":"spell13.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"No Cost"},{"id":"YasuoDashWrapper","name":"Sweeping Blade","description":"Dashes through a unit, dealing escalating Magic Damage with each cast.","tooltip":"Dashes through target enemy, dealing {{ e1 }} (+{{ a1 }}) (+{{ f3 }}) Magic Damage. Each cast increases your next dash's base Damage by 25%, up to {{ e6 }}%.

    Cannot be re-cast on the same enemy for {{ e2 }} seconds.

    If cast while dashing, Steel Tempest will strike as a circle.","leveltip":{"label":["Damage","Recast Block","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }} -> {{ e2NL }}","{{ e3 }} -> {{ e3NL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[60,70,80,90,100],[10,9,8,7,6],[0.5,0.4,0.3,0.2,0.1],[1.5,1.5,1.5,1.5,1.5],[15,17.5,20,22.5,25],[50,50,50,50,50],[20,20,20,20,20],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/70/80/90/100","10/9/8/7/6","0.5/0.4/0.3/0.2/0.1","1.5","15/17.5/20/22.5/25","50","20","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"}],"costType":"","maxammo":"-1","range":[475,475,475,475,475],"rangeBurn":"475","image":{"full":"YasuoDashWrapper.png","sprite":"spell13.png","group":"spell","x":240,"y":48,"w":48,"h":48}},{"id":"YasuoRKnockUpComboW","name":"Last Breath","description":"Moves to a unit and strikes them repeatedly for heavy damage. Can only be cast on Airborne targets.","tooltip":"Blinks to an Airborne enemy champion, dealing {{ e1 }} (+{{ a1 }}) physical damage and holding all Airborne enemies in the area in the air for an additional 1 second. Grants maximum Flow but resets all stacks of Gathering Storm.

    For 15 seconds, Yasuo's critical strikes gain {{ e5 }}% Bonus Armor Penetration - this affects Armor from items, buffs, runes and masteries.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ f1 }} -> {{ f2 }}"]},"maxrank":3,"cooldown":[0,0,0],"cooldownBurn":"0","cost":[0,0,0],"costBurn":"0","effect":[null,[200,300,400],[33,66,100],[650,750,850],[1.5,1.5,1.5],[50,50,50],[33,66,100],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"200/300/400","33/66/100","650/750/850","1.5","50","33/66/100","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":1.5,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[1200,1200,1200],"rangeBurn":"1200","image":{"full":"YasuoRKnockUpComboW.png","sprite":"spell13.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"No Cost"}]},"Yorick":{"id":83,"key":"Yorick","name":"Yorick","title":"Shepherd of Souls","spells":[{"id":"YorickQ","name":"Last Rites","description":"Yorick deals bonus damage on his next attack and heals himself. If the target dies a grave will be dug.","tooltip":"Yorick's next basic attack deals {{ e1 }} (+{{ a1 }}) physical damage and restores {{ f1 }} Health (doubled when below half Health). Last Rites will leave a grave if it kills the target.

    When there are three or more graves nearby and Last Rites has been used, Yorick can cast Awakening to raise Mist Walkers from graves.
    ","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[25,25,25,25,25],"costBurn":"25","effect":[null,[30,55,80,105,130],[5,5,5,5,5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"30/55/80/105/130","5","0","0","0","0","0","0","0","0"],"vars":[{"link":"attackdamage","coeff":1.4,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[0,0,0,0,0],"rangeBurn":"0","image":{"full":"YorickQ.png","sprite":"spell13.png","group":"spell","x":336,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"YorickW","name":"Dark Procession","description":"Yorick summons a destructible wall at target location that will block enemy movement.","tooltip":"Yorick summons a destructible wall of spirits with {{ e5 }} Health around target area for {{ e1 }} seconds. Allies can walk through the wall freely.","leveltip":{"label":["Health","Cooldown"],"effect":["{{ e5 }} -> {{ e5NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[20,18,16,14,12],"cooldownBurn":"20/18/16/14/12","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[4,4,4,4,4],[18,18,18,18,18],[0.75,0.75,0.75,0.75,0.75],[0,0,1,1,2],[2,2,3,3,4],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4","18","0.75","0/0/1/1/2","2/2/3/3/4","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"YorickW.png","sprite":"spell13.png","group":"spell","x":384,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"YorickE","name":"Mourning Mist","description":"Yorick throws a globule of Black Mist that damages, slows and marks enemies.","tooltip":"Yorick throws a globule of Black Mist that deals {{ e7 }}% of enemy's current Health as magic damage (minimum {{ e1 }} (+{{ a1 }})), slows by {{ e3 }}% for {{ e6 }} seconds, and marks champions and monsters for {{ e2 }} seconds.

    Yorick and his minions are hasted by {{ e5 }}% while moving toward the mark.","leveltip":{"label":["Minimum Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[70,105,140,175,210],[4,4,4,4,4],[30,30,30,30,30],[1000,1000,1000,1000,1000],[20,20,20,20,20],[2,2,2,2,2],[15,15,15,15,15],[300,300,300,300,300],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/105/140/175/210","4","30","1000","20","2","15","300","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"YorickE.png","sprite":"spell13.png","group":"spell","x":432,"y":48,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"YorickR","name":"Eulogy of the Isles","description":"Yorick summons the Maiden of the Mist that causes Yorick's attacks against the Maiden's target to deal bonus damage. The Maiden will also automatically raise Walkers from dead enemies.","tooltip":"Yorick summons The Maiden of the Mist and {{ e1 }} Mist Walkers. The Maiden has {{ e3 }} (+{{ f1 }}) Health, deals {{ e4 }} (+{{ f2 }}) magic damage, and raises Mist Walkers from nearby enemy deaths.

    When Yorick damages the Maiden's target he will deal {{ e5 }}% of their max Health as bonus magic damage ({{ e7 }} second cooldown).

    If the Maiden is summoned inside a lane, she will start pushing the lane.","leveltip":{"label":["Health","Damage","Mist Walkers","Mark Damage","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ e4 }} -> {{ e4NL }}","{{ e1 }} -> {{ e1NL }}","{{ e5 }}% ->{{ e5NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[160,150,140],"cooldownBurn":"160/150/140","cost":[100,100,100],"costBurn":"100","effect":[null,[2,3,4],[700,700,700],[700,1500,3000],[10,20,40],[5,7.5,10],[1,1,1],[2,2,2],[200,300,400],[0,0,0],[0,0,0]],"effectBurn":[null,"2/3/4","700","700/1500/3000","10/20/40","5/7.5/10","1","2","200/300/400","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600],"rangeBurn":"600","image":{"full":"YorickR.png","sprite":"spell13.png","group":"spell","x":0,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Zac":{"id":154,"key":"Zac","name":"Zac","title":"the Secret Weapon","spells":[{"id":"ZacQ","name":"Stretching Strikes","description":"Zac stretches an arm, grabbing an enemy. Attacking a different enemy will cause him to throw both targets towards each other.","tooltip":"Zac's arm stretches and grabs the first enemy it hits, dealing {{ e1 }} (+{{ a1 }}) (+{{ f2 }}) magic damage and briefly slowing them. Zac's next basic attack is replaced with a long range smack that repeats the initial magic damage and slow effect.

    If Zac grabs a different enemy with each attack he'll throw them towards each other, dealing {{ e1 }} (+{{ a1 }}) (+{{ f2 }}) magic damage in an area if they collide.
    ","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }}-> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[12,11,10,9,8],"cooldownBurn":"12/11/10/9/8","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[30,40,50,60,70],[4,4,4,4,4],[800,800,800,800,800],[-0.6,-0.6,-0.6,-0.6,-0.6],[0.5,0.5,0.5,0.5,0.5],[2.5,2.5,2.5,2.5,2.5],[1000,1000,1000,1000,1000],[700,700,700,700,700],[300,300,300,300,300],[300,300,300,300,300]],"effectBurn":[null,"30/40/50/60/70","4","800","-0.6","0.5","2.5","1000","700","300","300"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"},{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":"% of current Health","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"ZacQ.png","sprite":"spell13.png","group":"spell","x":48,"y":96,"w":48,"h":48},"resource":"{{ e2 }}% of current Health"},{"id":"ZacW","name":"Unstable Matter","description":"Zac's body erupts, damaging nearby enemies.","tooltip":"Zac's body erupts, dealing {{ e1 }} magic damage +{{ e3 }} (+{{ a1 }})% of the enemy's max health as magic damage to nearby enemies.

    Absorbing Goo reduces Unstable Matter's cooldown by {{ e4 }} second.

    16","leveltip":{"label":["Flat Damage","Percent Max Health Damage"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }}% -> {{ e3NL }}%"]},"maxrank":5,"cooldown":[5,5,5,5,5],"cooldownBurn":"5","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[40,55,70,85,100],[4,4,4,4,4],[4,5,6,7,8],[1,1,1,1,1],[200,200,200,200,200],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/55/70/85/100","4","4/5/6/7/8","1","200","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.02,"key":"a1"}],"costType":"% of current Health","maxammo":"-1","range":[350,350,350,350,350],"rangeBurn":"350","image":{"full":"ZacW.png","sprite":"spell13.png","group":"spell","x":96,"y":96,"w":48,"h":48},"resource":"{{ e2 }}% of current Health"},{"id":"ZacE","name":"Elastic Slingshot","description":"Zac attaches his arms to the ground and stretches back, launching himself forward.","tooltip":"First cast: Zac faces the cursor and charges up over {{ e4 }} seconds.

    Second cast: Launches Zac towards a location, knocking up nearby enemies for {{ f2 }} to {{ f3 }} seconds (based on charge time) and dealing {{ e1 }} (+{{ a1 }}) magic damage. Zac spawns extra chunks of Goo for each enemy champion hit.

    16","leveltip":{"label":["Damage","Range","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[24,21,18,15,12],"cooldownBurn":"24/21/18/15/12","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[80,130,180,230,280],[4,4,4,4,4],[1200,1350,1500,1650,1800],[0.9,1,1.1,1.2,1.3],[0.5,0.5,0.5,0.5,0.5],[500,500,500,500,500],[1350,1350,1350,1350,1350],[0.6,0.6,0.6,0.6,0.6],[265,265,265,265,265],[1,1,1,1,1]],"effectBurn":[null,"80/130/180/230/280","4","1200/1350/1500/1650/1800","0.9/1/1.1/1.2/1.3","0.5","500","1350","0.6","265","1"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":"% of current Health","maxammo":"-1","range":[300,300,300,300,300],"rangeBurn":"300","image":{"full":"ZacE.png","sprite":"spell13.png","group":"spell","x":144,"y":96,"w":48,"h":48},"resource":"{{ e2 }}% of current Health"},{"id":"ZacR","name":"Let's Bounce!","description":"Zac flattens himself, making him immune to crowd control while slowing enemies standing on top of him. When he decides to bounce away, he will suck up any enemies standing on top of him, taking them for a ride.","tooltip":"First cast: Zac squishes himself down, making him immune to Crowd Control for up to {{ e5 }} seconds. Enemies standing on top of him are slowed by {{ e4 }}%.

    Second cast: Charging for at least {{ f1 }} seconds before recasting causes Zac to scoop up enemies on top of him, carrying them towards a location. Upon landing nearby enemies take {{ e1 }} (+{{ a1 }}) magic damage and are briefly slowed.

    Reactivating before Zac charges up knocks back nearby enemies instead.
    ","leveltip":{"label":["Damage","Range","Slow Percent","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e3 }} -> {{ e3NL }}","{{ e4 }}% -> {{ e4NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[130,115,100],"cooldownBurn":"130/115/100","cost":[0,0,0],"costBurn":"0","effect":[null,[150,250,350],[1.1,1.1,1.1],[700,850,1000],[30,40,50],[2.5,2.5,2.5],[275,275,275],[300,300,300],[1,1,1],[0.25,0.25,0.25],[0.1,0.1,0.1]],"effectBurn":[null,"150/250/350","1.1","700/850/1000","30/40/50","2.5","275","300","1","0.25","0.1"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[300,300,300],"rangeBurn":"300","image":{"full":"ZacR.png","sprite":"spell13.png","group":"spell","x":192,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Zed":{"id":238,"key":"Zed","name":"Zed","title":"the Master of Shadows","spells":[{"id":"ZedQ","name":"Razor Shuriken","description":"Zed and his shadow both throw their spinning blades forward, dealing damage to any targets they pass through.","tooltip":"Zed and his shadows throw their shurikens, each dealing {{ e1 }} (+{{ a1 }}) physical damage to the first enemy they pass through, and {{ e3 }} (+{{ a2 }}) physical damage to each additional enemy.

    Additional shurikens that hit the same enemy deal 25% less damage than previous shurikens.","leveltip":{"label":["Damage","Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,6,6,6,6],"cooldownBurn":"6","cost":[75,70,65,60,55],"costBurn":"75/70/65/60/55","effect":[null,[70,105,140,175,210],[0.6,0.6,0.6,0.6,0.6],[42,63,84,105,126],[0.75,0.75,0.75,0.75,0.75],[925,925,925,925,925],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"70/105/140/175/210","0.6","42/63/84/105/126","0.75","925","0","0","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.9,"key":"a1"},{"link":"bonusattackdamage","coeff":0.54,"key":"a2"}],"costType":" Energy","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"ZedQ.png","sprite":"spell13.png","group":"spell","x":240,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ZedW","name":"Living Shadow","description":"Zed's shadow dashes forward, remaining in place for 4 seconds, and mimicking his spell casts. Zed can reactivate to swap places with the shadow.","tooltip":"Passive: Whenever Zed and his shadows strike an enemy with the same ability, Zed gains {{ e3 }} energy. Energy can only be gained once per cast ability.

    Active: Zed's shadow dashes forward, remaining in place for {{ e1 }} seconds.
    Reactivating Living Shadow will cause Zed to switch positions with this shadow.","leveltip":{"label":["Energy Return","Cost","Cooldown"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cost }} -> {{ costNL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[22,20,18,16,14],"cooldownBurn":"22/20/18/16/14","cost":[40,35,30,25,20],"costBurn":"40/35/30/25/20","effect":[null,[4.5,4.5,4.5,4.5,4.5],[0.2,0.2,0.2,0.2,0.2],[30,35,40,45,50],[4.5,4.5,4.5,4.5,4.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"4.5","0.2","30/35/40/45/50","4.5","0","0","0","0","0","0"],"vars":[],"costType":" Energy","maxammo":"-1","range":[650,650,650,650,650],"rangeBurn":"650","image":{"full":"ZedW.png","sprite":"spell13.png","group":"spell","x":288,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ZedE","name":"Shadow Slash","description":"Zed and his shadow spin their blades, creating a burst of shadow energy. The shadow's spin slows.","tooltip":"Zed and his shadows slash, dealing {{ e1 }} (+{{ a1 }}) physical damage to nearby enemies.

    Each enemy champion hit by Zed's slash reduces Living Shadow's cooldown by {{ e4 }} seconds.

    Enemies hit by a Shadow's slash are slowed by {{ e2 }}% for 1.5 seconds. Enemies hit by multiple slashes take no additional damage but are slowed by {{ e3 }}% instead.","leveltip":{"label":["Damage","Slow","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[5,4.5,4,3.5,3],"cooldownBurn":"5/4.5/4/3.5/3","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[65,90,115,140,165],[20,25,30,35,40],[30,37.5,45,52.5,60],[2,2,2,2,2],[1.5,1.5,1.5,1.5,1.5],[315,315,315,315,315],[290,290,290,290,290],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"65/90/115/140/165","20/25/30/35/40","30/37.5/45/52.5/60","2","1.5","315","290","0","0","0"],"vars":[{"link":"bonusattackdamage","coeff":0.8,"key":"a1"}],"costType":" Energy","maxammo":"-1","range":[290,290,290,290,290],"rangeBurn":"290","image":{"full":"ZedE.png","sprite":"spell13.png","group":"spell","x":336,"y":96,"w":48,"h":48},"resource":"{{ cost }} Energy"},{"id":"ZedR","name":"Death Mark","description":"Zed leaves a shadow behind and dashes to target Champion, marking them for death. After 3 seconds, the mark will trigger, dealing a percentage of the damage Zed has dealt while the mark was active. If the Champion dies under Death Mark, Zed can gain a portion of their attack damage.","tooltip":"Zed becomes untargetable and dashes to an enemy champion, marking them. After 3 seconds, the mark triggers, dealing physical damage equal to {{ a1 }} + {{ e2 }}% of all damage dealt to the target by Zed while the mark was active.

    The dash leaves a shadow behind for {{ e4 }} seconds. Reactivating Death Mark causes Zed to switch positions with this shadow.

    Reaper of Shadows: Zed reaps the shadow of the strongest foe slain under Death Mark, gaining {{ f2 }} attack damage. ({{ e0 }} + {{ e9 }}% of the victim's attack damage.)","leveltip":{"label":["Mark Detonation Damage","Percent Attack Damage Stolen","Cooldown"],"effect":["{{ e2 }}% -> {{ e2NL }}%","{{ e9 }}% -> {{ e9NL }}%","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,100,80],"cooldownBurn":"120/100/80","cost":[0,0,0],"costBurn":"0","effect":[null,[1,1,1],[25,35,45],[0.5,0.5,0.5],[6,6,6],[6.5,6.5,6.5],[3,3,3],[7.5,7.5,7.5],[4,4,4],[5,10,15],[5,5,5]],"effectBurn":[null,"1","25/35/45","0.5","6","6.5","3","7.5","4","5/10/15","5"],"vars":[{"link":"attackdamage","coeff":1,"key":"a1"}],"costType":"No Cost","maxammo":"-1","range":[625,625,625],"rangeBurn":"625","image":{"full":"ZedR.png","sprite":"spell13.png","group":"spell","x":384,"y":96,"w":48,"h":48},"resource":"No Cost"}]},"Ziggs":{"id":115,"key":"Ziggs","name":"Ziggs","title":"the Hexplosives Expert","spells":[{"id":"ZiggsQ","name":"Bouncing Bomb","description":"Ziggs throws a bouncing bomb that deals magic damage.","tooltip":"Ziggs throws a bouncing bomb that deals {{ e1 }} (+{{ a1 }}) magic damage.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[6,5.5,5,4.5,4],"cooldownBurn":"6/5.5/5/4.5/4","cost":[50,55,60,65,70],"costBurn":"50/55/60/65/70","effect":[null,[75,120,165,210,255],[850,850,850,850,850],[325,325,325,325,325],[225,225,225,225,225],[150,150,150,150,150],[240,240,240,240,240],[70,70,70,70,70],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/120/165/210/255","850","325","225","150","240","70","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.65,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"ZiggsQ.png","sprite":"spell13.png","group":"spell","x":432,"y":96,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZiggsW","name":"Satchel Charge","description":"Ziggs flings an explosive charge that detonates after 4 seconds, or when this ability is activated again. The explosion deals magic damage to enemies, knocking them away. Ziggs is also knocked away, but takes no damage. Ziggs can use the Satchel to hexplode vulnerable enemy turrets.","tooltip":"Ziggs flings an explosive charge that detonates after {{ e2 }} seconds, or when this ability is activated again. The explosion deals {{ e1 }} (+{{ a1 }}) magic damage to enemies, knocking them away. Ziggs is also knocked away, but takes no damage.

    Satchel Charge will hexplode structurally unsound towers below {{ f1*100 }}% of their Health.","leveltip":{"label":["Damage","Cooldown","Tower Destruction Threshold"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ f1*100 }}% -> {{ f2*100 }}%"]},"maxrank":5,"cooldown":[26,24,22,20,18],"cooldownBurn":"26/24/22/20/18","cost":[65,65,65,65,65],"costBurn":"65","effect":[null,[70,105,140,175,210],[4,4,4,4,4],[0.25,0.275,0.3,0.325,0.35],[600,600,600,600,600],[50,50,50,50,50],[400,400,400,400,400],[325,325,325,325,325],[775,775,775,775,775],[400,400,400,400,400],[6,6,6,6,6]],"effectBurn":[null,"70/105/140/175/210","4","0.25/0.275/0.3/0.325/0.35","600","50","400","325","775","400","6"],"vars":[{"link":"spelldamage","coeff":0.35,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[1000,1000,1000,1000,1000],"rangeBurn":"1000","image":{"full":"ZiggsW.png","sprite":"spell13.png","group":"spell","x":0,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZiggsE","name":"Hexplosive Minefield","description":"Ziggs scatters proximity mines that detonate on enemy contact, dealing magic damage and slowing.","tooltip":"Ziggs scatters proximity mines that detonate on enemy contact, dealing {{ e1 }} (+{{ a1 }}) magic damage. Enemies hit are slowed by {{ e2 }}% for {{ e4 }} seconds.

    Enemies triggering a mine take {{ e5 }}% damage from additional mines. Mines disarm automatically after {{ e3 }} seconds.","leveltip":{"label":["Mine Damage","Slow","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e2 }}% -> {{ e2NL }}%","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[16,16,16,16,16],"cooldownBurn":"16","cost":[70,80,90,100,110],"costBurn":"70/80/90/100/110","effect":[null,[40,65,90,115,140],[20,25,30,35,40],[10,10,10,10,10],[1.5,1.5,1.5,1.5,1.5],[40,40,40,40,40],[-0.2,-0.25,-0.3,-0.35,-0.4],[135,135,135,135,135],[150,150,150,150,150],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"40/65/90/115/140","20/25/30/35/40","10","1.5","40","-0.2/-0.25/-0.3/-0.35/-0.4","135","150","0","0"],"vars":[{"link":"spelldamage","coeff":0.3,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"ZiggsE.png","sprite":"spell13.png","group":"spell","x":48,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZiggsR","name":"Mega Inferno Bomb","description":"Ziggs deploys his ultimate creation, the Mega Inferno Bomb, hurling it an enormous distance. Enemies in the primary blast zone take more damage than those farther away. ","tooltip":"Ziggs deploys his ultimate creation, the Mega Inferno Bomb, hurling it an enormous distance. Enemies in the primary blast zone take {{ e1 }} (+{{ a1 }}) magic damage. Enemies farther away take two-thirds damage.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":3,"cooldown":[120,105,90],"cooldownBurn":"120/105/90","cost":[100,100,100],"costBurn":"100","effect":[null,[300,450,600],[66.6667,66.6667,66.6667],[525,525,525],[250,250,250],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"300/450/600","66.67","525","250","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":1.1,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[5000,5000,5000],"rangeBurn":"5000","image":{"full":"ZiggsR.png","sprite":"spell13.png","group":"spell","x":96,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Zilean":{"id":26,"key":"Zilean","name":"Zilean","title":"the Chronokeeper","spells":[{"id":"ZileanQ","name":"Time Bomb","description":"Tosses a bomb to target area that sticks to units that come near it (prioritizes Champions). It detonates after 3 seconds, dealing area of effect damage. If a Time Bomb is detonated early by another Time Bomb, it also stuns enemies.","tooltip":"Zilean tosses a time-delayed bomb to a location. The bomb sticks to the first unit which comes within a small area around it (prioritizes Champions). After {{ e2 }} seconds it detonates, dealing {{ e1 }} (+{{ a1 }}) magic damage to all surrounding enemies.

    Placing two bombs on a unit detonates the first bomb early, stunning all enemies in the blast for {{ e4 }} seconds.","leveltip":{"label":["Damage","Stun Duration","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e4 }} -> {{ e4NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[10,9.5,9,8.5,8],"cooldownBurn":"10/9.5/9/8.5/8","cost":[60,65,70,75,80],"costBurn":"60/65/70/75/80","effect":[null,[75,115,165,230,300],[3,3,3,3,3],[7,7,7,7,7],[1.1,1.2,1.3,1.4,1.5],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"75/115/165/230/300","3","7","1.1/1.2/1.3/1.4/1.5","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.9,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900,900,900],"rangeBurn":"900","image":{"full":"ZileanQ.png","sprite":"spell13.png","group":"spell","x":144,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZileanW","name":"Rewind","description":"Zilean can prepare himself for future confrontations, reducing the cooldowns of his other basic abilities. ","tooltip":"Reduces Zilean's other basic spell cooldowns by {{ e2 }} seconds.","leveltip":{"label":["Cooldown"],"effect":["{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[14,12,10,8,6],"cooldownBurn":"14/12/10/8/6","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[20,30,40,50,60],[10,10,10,10,10],[35,35,35,35,35],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"20/30/40/50/60","10","35","0","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[600,600,600,600,600],"rangeBurn":"600","image":{"full":"ZileanW.png","sprite":"spell13.png","group":"spell","x":192,"y":144,"w":48,"h":48},"resource":"{{ e3 }} Mana"},{"id":"TimeWarp","name":"Time Warp","description":"Zilean bends time around any unit, decreasing an enemy's Movement Speed or increasing an ally's Movement Speed for a short time.","tooltip":"Zilean increases an allied champion's Movement Speed, or slows an enemy champion, by {{ e2 }}% for {{ e1 }} seconds.","leveltip":{"label":["Speed"],"effect":["{{ e2 }}% -> {{ e2NL }}%"]},"maxrank":5,"cooldown":[15,15,15,15,15],"cooldownBurn":"15","cost":[50,50,50,50,50],"costBurn":"50","effect":[null,[2.5,2.5,2.5,2.5,2.5],[40,55,70,85,99],[1.5,1.5,1.5,1.5,1.5],[8,8,8,8,8],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"2.5","40/55/70/85/99","1.5","8","0","0","0","0","0","0"],"vars":[],"costType":" Mana","maxammo":"-1","range":[700,700,700,700,700],"rangeBurn":"700","image":{"full":"TimeWarp.png","sprite":"spell13.png","group":"spell","x":240,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ChronoShift","name":"Chronoshift","description":"Zilean places a protective time rune on an allied champion, teleporting the champion back in time if they take lethal damage.","tooltip":"Zilean marks himself or an allied champion with a protective time rune for {{ e2 }} seconds. If the target would take lethal damage, they are instead transported back in time, arriving with {{ e1 }} (+{{ a1 }}) Health.","leveltip":{"label":["Health Restored","Cooldown","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":3,"cooldown":[120,90,60],"cooldownBurn":"120/90/60","cost":[125,150,175],"costBurn":"125/150/175","effect":[null,[600,850,1100],[5,5,5],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"600/850/1100","5","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":2,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[900,900,900],"rangeBurn":"900","image":{"full":"ChronoShift.png","sprite":"spell13.png","group":"spell","x":288,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"}]},"Zyra":{"id":143,"key":"Zyra","name":"Zyra","title":"Rise of the Thorns","spells":[{"id":"ZyraQ","name":"Deadly Spines","description":"Thick vines spread through the ground and explode into spines, dealing magic damage to enemies within the area. If cast near a seed, Deadly Spines grows a Thorn Spitter plant, which fires at enemies from afar.","tooltip":"Thick vines spread through the ground and explode into spines, dealing {{ e1 }} (+{{ a1 }}) magic damage to enemies within the area.

    Garden of Thorns: If Deadly Spines is cast near a seed, a Thorn Spitter grows, dealing {{ f1 }} (+{{ a2 }}) magic damage. A Thorn Spitter has 750 range and lasts {{ f2 }} seconds.","leveltip":{"label":["Damage","Cooldown"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ cooldown }} -> {{ cooldownNL }}"]},"maxrank":5,"cooldown":[7,6.5,6,5.5,5],"cooldownBurn":"7/6.5/6/5.5/5","cost":[70,70,70,70,70],"costBurn":"70","effect":[null,[60,95,130,165,200],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.6,"key":"a1"},{"link":"spelldamage","coeff":0.15,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[800,800,800,800,800],"rangeBurn":"800","image":{"full":"ZyraQ.png","sprite":"spell13.png","group":"spell","x":336,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZyraW","name":"Rampant Growth","description":"Zyra plants a seed, lasting up to 60 seconds. Deadly Spines and Grasping Roots cast near seeds will turn them into plants who fight for Zyra. Additionally, Rampant Growth passively grants her plants improved maximum Health.","tooltip":"Plants a seed, lasting {{ e6 }} seconds. If an enemy Champion steps on a seed, it dies. Seeds spawned by Rampant Growth grant vision in a small area, and if stepped on they grant True Sight of the enemy champion for {{ e3 }} seconds.

    Zyra stores a seed every {{ ammorechargetime }} seconds (Max: 8 seeds planted).

    Passive: Zyra's plants gain {{ e8 }}% extra maximum Health.","leveltip":{"label":["Plant Health Bonus","Seed Recharge Rate"],"effect":["{{ e8 }}% -> {{ e8NL }}%","{{ ammorechargetime }} -> {{ ammorechargetimeNL }}"]},"maxrank":5,"cooldown":[0,0,0,0,0],"cooldownBurn":"0","cost":[0,0,0,0,0],"costBurn":"0","effect":[null,[0,0,0,0,0],[0,0,0,0,0],[2,2,2,2,2],[0,0,0,0,0],[0,0,0,0,0],[60,60,60,60,60],[0,0,0,0,0],[10,20,30,40,50],[0.4,0.8,1.2,1.6,2],[0,0,0,0,0]],"effectBurn":[null,"0","0","2","0","0","60","0","10/20/30/40/50","0.4/0.8/1.2/1.6/2","0"],"vars":[],"costType":"1 Seed","maxammo":"2","range":[850,850,850,850,850],"rangeBurn":"850","image":{"full":"ZyraW.png","sprite":"spell13.png","group":"spell","x":384,"y":144,"w":48,"h":48},"resource":"1 Seed"},{"id":"ZyraE","name":"Grasping Roots","description":"Zyra sends forth vines through the ground to ensnare her target, dealing damage and rooting enemies they come across. If cast near a seed, Grasping Roots grows a Vine Lasher, whose short range attacks reduce enemy Movement Speed.","tooltip":"Sends forward vines dealing {{ e1 }} (+{{ a1 }}) magic damage and rooting enemies for {{ e5 }} second(s).

    Garden of Thorns: If Grasping Roots passes near a seed, a Vine Lasher grows dealing {{ f1 }} (+{{ a2 }}) magic damage and slowing enemies by {{ e2 }}% for {{ e3 }} seconds. A Vine Lasher has 400 range and lasts {{ f2 }} seconds.","leveltip":{"label":["Damage","Root Duration","Mana Cost"],"effect":["{{ e1 }} -> {{ e1NL }}","{{ e5 }} -> {{ e5NL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":5,"cooldown":[12,12,12,12,12],"cooldownBurn":"12","cost":[70,75,80,85,90],"costBurn":"70/75/80/85/90","effect":[null,[60,95,130,165,200],[30,30,30,30,30],[2,2,2,2,2],[4,4,4,0,0],[0.75,1,1.25,1.5,1.75],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]],"effectBurn":[null,"60/95/130/165/200","30","2","4/4/4/0/0","0.75/1/1.25/1.5/1.75","0","0","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.5,"key":"a1"},{"link":"spelldamage","coeff":0.15,"key":"a2"}],"costType":" Mana","maxammo":"-1","range":[1100,1100,1100,1100,1100],"rangeBurn":"1100","image":{"full":"ZyraE.png","sprite":"spell13.png","group":"spell","x":432,"y":144,"w":48,"h":48},"resource":"{{ cost }} Mana"},{"id":"ZyraR","name":"Stranglethorns","description":"Zyra summons a twisted thicket at her target location, dealing damage to enemies as it expands and knocking them airborne as it contracts.","tooltip":"Summons the fury of nature, growing a twisted thicket at target location which deals {{ e3 }} (+{{ a1 }}) magic damage to all enemies in the area as it expands. After 2 seconds, the vines snap upward knocking enemies into the air for {{ e1 }} second.

    Garden of Thorns: Plants within the thicket are enraged, attacking in a flurry for 150% total damage.","leveltip":{"label":["Damage","Cooldown","Mana Cost"],"effect":["{{ e3 }} -> {{ e3NL }}","{{ cooldown }} -> {{ cooldownNL }}","{{ cost }} -> {{ costNL }}"]},"maxrank":3,"cooldown":[130,120,110],"cooldownBurn":"130/120/110","cost":[100,120,140],"costBurn":"100/120/140","effect":[null,[1,1,1],[0,0,0],[180,265,350],[0,0,0],[0,0,0],[0,0,0],[150,150,150],[0,0,0],[0,0,0],[0,0,0]],"effectBurn":[null,"1","0","180/265/350","0","0","0","150","0","0","0"],"vars":[{"link":"spelldamage","coeff":0.7,"key":"a1"}],"costType":" Mana","maxammo":"-1","range":[700,700,700],"rangeBurn":"700","image":{"full":"ZyraR.png","sprite":"spell14.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"{{ cost }} Mana"}]}} ================================================ FILE: api_data/summoners.json ================================================ {"SummonerBarrier":{"id":21,"name":"Barrier","description":"Shields your champion from 115-455 damage (depending on champion level) for 2 seconds.","tooltip":"Temporarily shields {{ f1 }} damage from your champion for 2 seconds.","maxrank":1,"cooldown":[180],"cooldownBurn":"180","cost":[0],"costBurn":"0","effect":[null,[95],[20],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"95","20","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerBarrier","summonerLevel":4,"modes":["ARAM","CLASSIC","TUTORIAL","ODIN","ASCENSION","FIRSTBLOOD","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[1200],"rangeBurn":"1200","image":{"full":"SummonerBarrier.png","sprite":"spell0.png","group":"spell","x":0,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerBoost":{"id":1,"name":"Cleanse","description":"Removes all disables (excluding suppression) and summoner spell debuffs affecting your champion and lowers the duration of incoming disables by 65% for 3 seconds.","tooltip":"Removes all disables (excluding suppression) and summoner spell debuffs affecting your champion and reduces the duration of disables by 65% for the next {{ f1 }} seconds.","maxrank":1,"cooldown":[210],"cooldownBurn":"210","cost":[0],"costBurn":"0","effect":[null,[0.65],[3],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"0.65","3","0","0","0","0","0","0","0","0"],"vars":[{"link":"@text","coeff":3,"key":"f1"}],"key":"SummonerBoost","summonerLevel":6,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[200],"rangeBurn":"200","image":{"full":"SummonerBoost.png","sprite":"spell0.png","group":"spell","x":48,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerDarkStarChampSelect1":{"id":35,"name":"Disabled Summoner Spells","description":"Summoner spells are disabled in this mode.","tooltip":"","maxrank":1,"cooldown":[0],"cooldownBurn":"0","cost":[0],"costBurn":"0","effect":[null,[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"0","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerDarkStarChampSelect1","summonerLevel":1,"modes":["DARKSTAR"],"costType":"","maxammo":"-1","range":[2500],"rangeBurn":"2500","image":{"full":"SummonerDarkStarChampSelect1.png","sprite":"spell0.png","group":"spell","x":96,"y":0,"w":48,"h":48}},"SummonerDarkStarChampSelect2":{"id":36,"name":"Disabled Summoner Spells","description":"Summoner spells are disabled in this mode.","tooltip":"","maxrank":1,"cooldown":[0],"cooldownBurn":"0","cost":[0],"costBurn":"0","effect":[null,[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"0","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerDarkStarChampSelect2","summonerLevel":1,"modes":["DARKSTAR"],"costType":"","maxammo":"-1","range":[2500],"rangeBurn":"2500","image":{"full":"SummonerDarkStarChampSelect2.png","sprite":"spell0.png","group":"spell","x":144,"y":0,"w":48,"h":48}},"SummonerDot":{"id":14,"name":"Ignite","description":"Ignites target enemy champion, dealing 70-410 true damage (depending on champion level) over 5 seconds, grants you vision of the target, and reduces healing effects on them for the duration.","tooltip":"Ignite deals {{ f1 }} true damage to target enemy champion over 5 seconds, grants you vision of the target and applies Grievous Wounds for the duration.

    (Grievous Wounds reduces healing effects by 40%. This vision does not reveal stealthed enemies.)","maxrank":1,"cooldown":[210],"cooldownBurn":"210","cost":[0],"costBurn":"0","effect":[null,[5],[10],[4],[100],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"5","10","4","100","0","0","0","0","0","0"],"vars":[{"link":"@player.level","coeff":[70,90,110,130,150,170,190,210,230,250,270,290,310,330,350,370,390,410],"key":"f1"}],"key":"SummonerDot","summonerLevel":10,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[600],"rangeBurn":"600","image":{"full":"SummonerDot.png","sprite":"spell0.png","group":"spell","x":192,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerExhaust":{"id":3,"name":"Exhaust","description":"Exhausts target enemy champion, reducing their Movement Speed by 30%, and their damage dealt by 40% for 2.5 seconds.","tooltip":"Exhausts target enemy champion, reducing their Movement Speed by {{ f3 }}%, and their damage dealt by {{ f2 }}% for 2.5 seconds.","maxrank":1,"cooldown":[210],"cooldownBurn":"210","cost":[0],"costBurn":"0","effect":[null,[2.5],[40],[0],[0],[30],[0],[0],[0],[0],[0]],"effectBurn":[null,"2.5","40","0","0","30","0","0","0","0","0"],"vars":[],"key":"SummonerExhaust","summonerLevel":4,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[650],"rangeBurn":"650","image":{"full":"SummonerExhaust.png","sprite":"spell0.png","group":"spell","x":240,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerFlash":{"id":4,"name":"Flash","description":"Teleports your champion a short distance toward your cursor's location.","tooltip":"Teleports your champion a short distance toward your cursor's location.","maxrank":1,"cooldown":[300],"cooldownBurn":"300","cost":[0],"costBurn":"0","effect":[null,[400],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"400","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerFlash","summonerLevel":8,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[425],"rangeBurn":"425","image":{"full":"SummonerFlash.png","sprite":"spell0.png","group":"spell","x":288,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerHaste":{"id":6,"name":"Ghost","description":"Your champion gains increased Movement Speed and can move through units for 10 seconds. Grants a maximum of 28-45% (depending on champion level) Movement Speed after accelerating for 2 seconds.","tooltip":"Your champion gains increased Movement Speed and can move through units for 10 seconds. Grants a maximum of {{ f1 }}% Movement Speed after accelerating for 2 seconds.","maxrank":1,"cooldown":[180],"cooldownBurn":"180","cost":[0],"costBurn":"0","effect":[null,[27],[0],[2],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"27","0","2","0","0","0","0","0","0","0"],"vars":[{"link":"@text","coeff":27,"key":"f1"}],"key":"SummonerHaste","summonerLevel":1,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[200],"rangeBurn":"200","image":{"full":"SummonerHaste.png","sprite":"spell0.png","group":"spell","x":336,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerHeal":{"id":7,"name":"Heal","description":"Restores 90-345 Health (depending on champion level) and grants 30% Movement Speed for 1 second to you and target allied champion. This healing is halved for units recently affected by Summoner Heal.","tooltip":"Restores {{ f1 }} Health and grants 30% Movement Speed for 1 second to your champion and target allied champion. This healing is halved for units recently affected by Summoner Heal.

    If this spell cannot find a target, it will cast on the most wounded allied champion in range.","maxrank":1,"cooldown":[240],"cooldownBurn":"240","cost":[0],"costBurn":"0","effect":[null,[0.3],[75],[15],[0.5],[826],[0],[0],[0],[0],[0]],"effectBurn":[null,"0.3","75","15","0.5","826","0","0","0","0","0"],"vars":[{"link":"@player.level","coeff":[90,105,120,135,150,165,180,195,210,225,240,255,270,285,300,315,330,345],"key":"f1"}],"key":"SummonerHeal","summonerLevel":1,"modes":["CLASSIC","ODIN","TUTORIAL","ARAM","ASCENSION","FIRSTBLOOD","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[850],"rangeBurn":"850","image":{"full":"SummonerHeal.png","sprite":"spell0.png","group":"spell","x":384,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerMana":{"id":13,"name":"Clarity","description":"Restores 50% of your champion's maximum Mana. Also restores allies for 25% of their maximum Mana.","tooltip":"Restores {{ f1 }}% maximum Mana to your Champion and {{ f2 }}% to nearby allies.","maxrank":1,"cooldown":[240],"cooldownBurn":"240","cost":[0],"costBurn":"0","effect":[null,[50],[25],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"50","25","0","0","0","0","0","0","0","0"],"vars":[{"link":"@player.level","coeff":[190,220,250,280,310,340,370,400,430,460,490,520,550,580,610,640,670,700],"key":"f1"},{"link":"@player.level","coeff":[95,110,125,140,155,170,185,200,215,230,245,260,275,290,305,320,335,350],"key":"f2"}],"key":"SummonerMana","summonerLevel":1,"modes":["ODIN","ARAM","ASCENSION"],"costType":"No Cost","maxammo":"-1","range":[600],"rangeBurn":"600","image":{"full":"SummonerMana.png","sprite":"spell0.png","group":"spell","x":432,"y":0,"w":48,"h":48},"resource":"No Cost"},"SummonerPoroRecall":{"id":30,"name":"To the King!","description":"Quickly travel to the Poro King's side.","tooltip":"Passive: Hitting an enemy champion with a Poro gives your team a Poro Mark. Upon reaching 10 Poro Marks, your team summons the Poro King to fight alongside them. While the Poro King is active, no Poro Marks can be scored by either team.

    Active: Quickly dash to King Poro's side. Can only be cast while the Poro King is summoned for your team.

    ''Poros tug the heartstrings. The rest of you just comes along for the ride.''","maxrank":1,"cooldown":[10],"cooldownBurn":"10","cost":[0],"costBurn":"0","effect":[null,[3000],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"3000","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerPoroRecall","summonerLevel":1,"modes":["KINGPORO"],"costType":"No Cost","maxammo":"-1","range":[200],"rangeBurn":"200","image":{"full":"SummonerPoroRecall.png","sprite":"spell0.png","group":"spell","x":0,"y":48,"w":48,"h":48},"resource":"No Cost"},"SummonerPoroThrow":{"id":31,"name":"Poro Toss","description":"Toss a Poro at your enemies. If it hits, you can quickly travel to your target as a follow up.","tooltip":"Toss a Poro a long distance, dealing {{ f2 }} true damage to the first enemy unit hit, granting True Sight of the target. This ability can be recast for 3 seconds if it hits an enemy to dash to the target hit. Dashing to the target will reduce the cooldown of Poro Toss by 5 seconds.

    Poros are not blocked by spell shields or wind walls because they are animals, not spells!

    ''Poros are a model for Runeterran aerodynamics.''","maxrank":1,"cooldown":[20],"cooldownBurn":"20","cost":[0],"costBurn":"0","effect":[null,[20],[10],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"20","10","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerPoroThrow","summonerLevel":1,"modes":["KINGPORO"],"costType":"No Cost","maxammo":"-1","range":[2500],"rangeBurn":"2500","image":{"full":"SummonerPoroThrow.png","sprite":"spell0.png","group":"spell","x":48,"y":48,"w":48,"h":48},"resource":"No Cost"},"SummonerSiegeChampSelect1":{"id":33,"name":"Nexus Siege: Siege Weapon Slot","description":"In Nexus Siege, Summoner Spells are replaced with Siege Weapon Slots. Spend Crystal Shards to buy single-use Siege Weapons from the item shop, then use your Summoner Spell keys to activate them!","tooltip":"","maxrank":1,"cooldown":[0],"cooldownBurn":"0","cost":[0],"costBurn":"0","effect":[null,[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"0","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerSiegeChampSelect1","summonerLevel":1,"modes":["SIEGE"],"costType":"","maxammo":"-1","range":[2500],"rangeBurn":"2500","image":{"full":"SummonerSiegeChampSelect1.png","sprite":"spell0.png","group":"spell","x":96,"y":48,"w":48,"h":48}},"SummonerSiegeChampSelect2":{"id":34,"name":"Nexus Siege: Siege Weapon Slot","description":"In Nexus Siege, Summoner Spells are replaced with Siege Weapon Slots. Spend Crystal Shards to buy single-use Siege Weapons from the item shop, then use your Summoner Spell keys to activate them!","tooltip":"","maxrank":1,"cooldown":[0],"cooldownBurn":"0","cost":[0],"costBurn":"0","effect":[null,[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"0","0","0","0","0","0","0","0","0","0"],"vars":[],"key":"SummonerSiegeChampSelect2","summonerLevel":1,"modes":["SIEGE"],"costType":"","maxammo":"-1","range":[2500],"rangeBurn":"2500","image":{"full":"SummonerSiegeChampSelect2.png","sprite":"spell0.png","group":"spell","x":144,"y":48,"w":48,"h":48}},"SummonerSmite":{"id":11,"name":"Smite","description":"Deals 390-1000 true damage (depending on champion level) to target epic or large monster or enemy minion. Restores Health based on your maximum life when used against monsters.","tooltip":"Deals {{ f1 }} true damage to target epic or large monster or enemy minion. Against monsters, additionally restores {{ f6 }} (+{{ f7 }}) Health.

    Smite regains a charge every {{ f3 }} seconds, up to a maximum of 2 charges.","maxrank":1,"cooldown":[75],"cooldownBurn":"75","cost":[0],"costBurn":"0","effect":[null,[15],[0],[0],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"15","0","0","0","0","0","0","0","0","0"],"vars":[{"link":"@player.level","coeff":[390,410,430,450,480,510,540,570,600,640,680,720,760,800,850,900,950,1000],"key":"f1"}],"key":"SummonerSmite","summonerLevel":10,"modes":["CLASSIC","TUTORIAL","FIRSTBLOOD","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"2","range":[500],"rangeBurn":"500","image":{"full":"SummonerSmite.png","sprite":"spell0.png","group":"spell","x":192,"y":48,"w":48,"h":48},"resource":"No Cost"},"SummonerSnowball":{"id":32,"name":"Mark","description":"Throw a snowball in a straight line at your enemies. If it hits an enemy, they become marked, granting True Sight, and your champion can quickly travel to the marked target as a follow up.","tooltip":"Throw a snowball a long distance, dealing {{ f1 }} true damage to the first enemy unit hit and granting True Sight of the target. If it hits an enemy, this ability can be recast for {{ f2 }} seconds to Dash to the tagged unit, dealing an additional {{ f5 }} true damage. Dashing to the target will reduce the cooldown of Mark by {{ f3 }}%.

    Mark projectiles are not stopped by spell shields or projectile mitigation.","maxrank":1,"cooldown":[80],"cooldownBurn":"80","cost":[0],"costBurn":"0","effect":[null,[1200],[20],[10],[0.5],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"1200","20","10","0.5","0","0","0","0","0","0"],"vars":[],"key":"SummonerSnowball","summonerLevel":1,"modes":["ARAM","FIRSTBLOOD"],"costType":"No Cost","maxammo":"-1","range":[1600],"rangeBurn":"1600","image":{"full":"SummonerSnowball.png","sprite":"spell0.png","group":"spell","x":240,"y":48,"w":48,"h":48},"resource":"No Cost"},"SummonerTeleport":{"id":12,"name":"Teleport","description":"After channeling for 4.5 seconds, teleports your champion to target allied structure, minion, or ward.","tooltip":"After channeling for {{ f1 }} seconds, your champion teleports to target allied structure, minion, or ward.

    You may reactivate Teleport to cancel it, placing it on a {{ f3 }} second cooldown.","maxrank":1,"cooldown":[0],"cooldownBurn":"0","cost":[0],"costBurn":"0","effect":[null,[4.5],[200],[300],[0],[0],[0],[0],[0],[0],[0]],"effectBurn":[null,"4.5","200","300","0","0","0","0","0","0","0"],"vars":[{"link":"@text","coeff":4,"key":"f1"}],"key":"SummonerTeleport","summonerLevel":6,"modes":["CLASSIC","TUTORIAL","ASSASSINATE","URF","ARSR","DOOMBOTSTEEMO"],"costType":"No Cost","maxammo":"-1","range":[25000],"rangeBurn":"25000","image":{"full":"SummonerTeleport.png","sprite":"spell0.png","group":"spell","x":288,"y":48,"w":48,"h":48},"resource":"No Cost"}} ================================================ FILE: app.js ================================================ "use strict"; var express = require('express'); var http = require('http'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var bodyParser = require('body-parser'); var compress = require('compression'); //middle ware var overallData = require('./middleware/overall_data.js'); //routes var champion = require('./routes/champion'); var matchup = require('./routes/matchup'); var matchupJson = require('./routes/matchup_json'); var apiStatic = require('./routes/api_static'); var statistics = require('./routes/statistics'); var faq = require('./routes/faq'); var index = require('./routes/index'); var app = express(); app.get('/*', function(req, res, next) { // redirect to http instead of www if (req.headers.host.match(/^www/) !== null ) { res.redirect('http://' + req.headers.host.replace(/^www\./, '') + req.url); } else { next(); } }); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.use(compress()); app.use(favicon(__dirname + '/public/favicon.ico')); app.use(logger('dev')); app.use(bodyParser.json({limit: '2kb', extended: true})); app.use(bodyParser.urlencoded({limit: '2kb', extended: true})); app.use(express.static(path.join(__dirname, 'public'), {maxAge:86400000})); //one day //pages //set cache headers for page now that we are utilizing cloudflare app.use(function(req, res, next){ res.setHeader('Cache-Control', 'public, max-age=86400'); //cache pages for 1 minute, if needed I can purge cache from cloud flare next(); }); app.use(overallData); app.use('/champion', champion); app.use('/matchup', matchup); app.use('/matchupJson', matchupJson); app.use('/static', apiStatic); app.use('/statistics', statistics); app.use('/faq', faq); app.use('/', index); /// catch 404 and forwarding to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); /// error handlers // development error handler // will print stacktrace if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.statusCode = err.status; res.render('error', { pageData:{ appName: 'core', name:'error', title: 'We got ourselves a problem...' }, message: err.message, error: err }); }); } else { // production error handler // no stacktraces leaked to user app.use(function(err, req, res, next) { res.statusCode = err.status; res.render('error', { pageData:{ appName: 'core', name:'error', title: 'We got ourselves a wild teemo problem...' }, message: err.message, error: {} }); }); } module.exports = app; ================================================ FILE: bash.txt ================================================ Mongodump file mongodump --db championgg --collection webchampionpages --out ./db mongodump --db championgg --collection webchampionroles --out ./db mongodump --db championgg --collection webmatchuppages --out ./db mongodump --db championgg --collection weboverallroledatas --out ./db mongodump --db championgg --collection webhomepagesummaries --out ./db mongodump --db championgg --collection webstatisticspages --out ./db mongodump --db championgg --collection weboverallrolestats --out ./db git add -A && git commit -m "updated data" && git push cd /code/championweb git pull mongorestore --db championgg --collection webchampionpages --drop db/championgg/webchampionpages.bson mongorestore --db championgg --collection webchampionroles --drop db/championgg/webchampionroles.bson mongorestore --db championgg --collection webmatchuppages --drop db/championgg/webmatchuppages.bson mongorestore --db championgg --collection weboverallroledatas --drop db/championgg/weboverallroledatas.bson mongorestore --db championgg --collection webhomepagesummaries --drop db/championgg/webhomepagesummaries.bson mongorestore --db championgg --collection webstatisticspages --drop db/championgg/webstatisticspages.bson mongorestore --db championgg --collection weboverallroledatas --drop db/championgg/weboverallstats.bson cd bin && NODE_ENV=production pm2 restart www.js NODE_ENV=production pm2 restart www.js NODE_ENV=updating pm2 restart www.js NODE_ENV=serverUpdate pm2 restart www.js NODE_ENV=production pm2 start www.js -i max NODE_ENV=updating pm2 start www.js -i max NODE_ENV=serverUpdate pm2 start www.js -i max UPDATES=aggregation node performUpdate mongodump --db leaguetimes --collection votes ================================================ FILE: bin/update_server.sh ================================================ sudo npm install mongorestore --db championgg --collection webchampionpages --drop db/championgg/webchampionpages.bson mongorestore --db championgg --collection webchampionroles --drop db/championgg/webchampionroles.bson mongorestore --db championgg --collection webmatchuppages --drop db/championgg/webmatchuppages.bson mongorestore --db championgg --collection weboverallroledatas --drop db/championgg/weboverallroledatas.bson mongorestore --db championgg --collection weboverallstats --drop db/championgg/weboverallstats.bson mongorestore --db championgg --collection webhomepagesummaries --drop db/championgg/webhomepagesummaries.bson mongorestore --db championgg --collection webstatisticspages --drop db/championgg/webstatisticspages.bson cd bin && NODE_ENV=production pm2 restart www.js ================================================ FILE: bin/www.js ================================================ #!/usr/bin/env node "use strict"; var debug = require('debug')('my-application'); var app = require('../app'); var db = require('../db'); app.set('port', process.env.PORT || 80); app.listen(app.get('port'), function() { console.log('Express server listening on port ' + app.get('port')); }); ================================================ FILE: config/config.js ================================================ module.exports.config = { worker: { local: { githubRoot: 'https://raw.githubusercontent.com/joel1st/championweb/master/', githubRoute : 'https://raw.githubusercontent.com/joel1st/championweb/master/db/championgg/', files: [ 'webchampionpages.bson', 'webchampionpages.metadata.json', 'webchampionroles.bson', 'webchampionroles.metadata.json', 'webhomepagesummaries.bson', 'webhomepagesummaries.metadata.json', 'webmatchuppages.bson', 'webmatchuppages.metadata.json', 'weboverallroledatas.bson', 'weboverallroledatas.metadata.json', 'weboverallstats.bson', 'weboverallstats.metadata.json', 'webstatisticspages.bson', 'webstatisticspages.metadata.json' ], headline: 'headline.js', tmpFolder: 'tmp/', mongo: { host: 'localhost', port: '27017', db: 'championgg', user: '', password: '' }, queue: { prefix: 'championggQ_', host: 'localhost', pass: '', port: 6379 }, fastly: { api_key: process.env.FASTLY_API_KEY, purgeall: 'https://api.fastly.com/service/' + process.env.FASTLY_SERVICE_ID + '/purge_all' } }, production: { githubRoot: 'https://raw.githubusercontent.com/joel1st/championweb/master/', githubRoute : 'https://raw.githubusercontent.com/joel1st/championweb/master/db/championgg/', files: [ 'webchampionpages.bson', 'webchampionpages.metadata.json', 'webchampionroles.bson', 'webchampionroles.metadata.json', 'webhomepagesummaries.bson', 'webhomepagesummaries.metadata.json', 'webmatchuppages.bson', 'webmatchuppages.metadata.json', 'weboverallroledatas.bson', 'weboverallroledatas.metadata.json', 'weboverallstats.bson', 'weboverallstats.metadata.json', 'webstatisticspages.bson', 'webstatisticspages.metadata.json' ], headline: 'headline.js', tmpFolder: 'tmp/', mongo: { host: process.env.WORKER_MONGO_HOST, port: process.env.WORKER_MONGO_PORT, db: process.env.WORKER_MONGO_DB, user: process.env.WORKER_MONGO_USER, password: process.env.WORKER_MONGO_PASSWORD }, queue: { prefix: process.env.QUEUE_PREFIX || 'championggQ_', host: process.env.REDIS_HOST || 'localhost', pass: process.env.REDIS_PASS || '', port: process.env.REDIS_PORT || 6379 }, fastly: { api_key: process.env.FASTLY_API_KEY, purgeall: 'https://api.fastly.com/service/' + process.env.FASTLY_SERVICE_ID + '/purge_all' } } } }; ================================================ FILE: db/championgg/webchampionpages.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.webchampionpages" } ] } ================================================ FILE: db/championgg/webchampionroles.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.webchampionroles" } ] } ================================================ FILE: db/championgg/webhomepagesummaries.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.webhomepagesummaries" } ] } ================================================ FILE: db/championgg/webmatchuppages.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.webmatchuppages" } ] } ================================================ FILE: db/championgg/weboverallroledatas.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.weboverallroledatas" } ] } ================================================ FILE: db/championgg/weboverallstats.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.weboverallstats" } ] } ================================================ FILE: db/championgg/webstatisticspages.metadata.json ================================================ { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "championgg.webstatisticspages" } ] } ================================================ FILE: db.js ================================================ "use strict"; var mongoose = require('mongoose'); mongoose.connect(process.env.MONGO_DB || 'mongodb://localhost/championgg'); var db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); db.once('open', function () { console.log('Connection Made!'); }); ================================================ FILE: gruntfile.js ================================================ module.exports = function(grunt){ "use strict"; require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks); grunt.initConfig({ watch: { js: { files: [ 'routes/*.js', 'bin/*.js', 'logic/*.js', 'models/*.js', 'public/js/*.js', '*.js' ], tasks: ['jshint'] } }, pkg: grunt.file.readJSON('package.json'), jshint: { files: [ 'routes/*.js', 'bin/*.js', 'logic/*.js', 'models/*.js', 'public/js/*.js', '!public/js/master.min.js', '*.js' ], options: { // options here to override JSHint defaults node: true, loopfunc: true, globals: { jQuery: false, console: true, module: true, require: true } } }, concat: { js: { src: ['public/dist/js/angular.js', 'public/dist/js/angular-bootstrap.js', 'public/dist/js/dirDisqus.js', 'public/dist/js/chart.js', 'public/dist/js/tc-angular-chartjs.js', 'public/js/champion_data.js', 'public/js/chart_options.js', 'public/js/championgg_tooltip.js', 'public/js/app.js', 'public/js/champion_page.js', 'public/js/matchup_page.js', 'public/js/statistics_page.js'], dest: 'public/js/master.min.js' }, }, uglify: { build: { files: { 'public/js/master.min.js': 'public/js/master.min.js' } } }, cssmin: { build: { src: ['public/css/master.css', 'public/css/sprite.css'], dest: 'public/css/master.min.css' } } }); grunt.registerTask('production', ['concat', 'uglify','cssmin']); grunt.registerTask('default', []); }; ================================================ FILE: headline.js ================================================ // Message to display at the top of the site. // Standard message: We are currently aggregating patch 6.14 data - check back in 3 days! module.exports = ""; ================================================ FILE: logic/lower_case_champ.js ================================================ "use strict"; var champList = require('../api_data/champions.json'); /** * Converts the champion list keys to lower case. It then compares * the input to see if there is a match. (this is useful for checking, * if a url has entered a champion key - but doesn't match the correct casing). * @param {string} champName - the champ name to compare against the champList. * @return {string|undefined} - if a match is found, the champ key is returned, otherwise undefined. */ var lowerCaseChamp = function(champName) { for (var prop in champList) { if (prop.toLowerCase() === champName.toLowerCase()) { return prop; } } }; module.exports = lowerCaseChamp; ================================================ FILE: logic/produce_error.js ================================================ "use strict"; var errors = { champNotFound: 'That champ or role doesn\'t appear to exist!', pageNotFound: 'We couldn\'t find the page you are looking for - sorry!', serverMaintenance: 'For some reason we couldn\'t get the page to load - Chances are we\'re working on updating data - if it isn\'t fixed in the next few minutes please let us know!', invalidMatchup: 'That appears to be an invalid or old matchup!' }; /** * A function for generating errors. * @param {string} errorType - the key for the error type (which corresponds with the keys * in the error object above). * @param {number} errorNumber - the response number of the error (defaults to 404). * @return {object} - the error object. */ var produceError = function(errorType, errorNumber) { var err = new Error(errors[errorType]); err.status = errorNumber || 404; return err; }; module.exports = produceError; ================================================ FILE: logic/role_hash_table.js ================================================ "use strict"; /** * The varius role keys and values. * Used for determining legitimate roles for champion page routes. */ var roleList = { 'Top': 'TOP', 'Middle': 'MIDDLE', 'Support': 'DUO_SUPPORT', 'ADC': 'DUO_CARRY', 'Jungle': 'JUNGLE', 'top': 'TOP', 'middle': 'MIDDLE', 'support': 'DUO_SUPPORT', 'adc': 'DUO_CARRY', 'jungle': 'JUNGLE', 'adcsupport': 'ADCSUPPORT', 'synergy': 'SYNERGY' }; var roleKey = { 'TOP': 'Top', 'MIDDLE': 'Middle', 'DUO_SUPPORT': 'Support', 'DUO_CARRY': 'ADC', 'JUNGLE': 'Jungle', 'ADCSUPPORT': 'adcsupport', 'SYNERGY': 'synergy' }; exports.roleList = roleList; exports.roleKey = roleKey; ================================================ FILE: middleware/overall_data.js ================================================ var WebOverallStats = require('../models/web_overall_stats.js'); var produceError = require('../logic/produce_error.js'); /** * The core object is used in all views template for overall data. * Values from database are loaded from the webOverallStats * collection and added to core object. * @type {Object} */ var ddPatch = require('../api_data/dd_patch.json').ddPatch var core = { ddPatch: ddPatch, resetCache: ddPatch + Math.random().toFixed(6), masteryOrder: ['Offense','Defense','Utility'], headline: require('../headline.js') }; // Data retrieved from DB: // gamesAnalyzed:"3,549,640", // patch:"5.10", // patchHistory: ["5.6","5.7","5.8","5.9","5.10"] module.exports = function(req, res, next){ /** * Set the core object as a local for the view */ res.locals.core = core; /** * If no data has been retrieved from data base yet, * request overall stats data from collection and add it * to the core object. */ if (!core.championsAnalyzed){ WebOverallStats.findOne({}, function(err, data) { if (err) { return next(produceError('serverMaintenance', 503)); } else if (!data) { return next(produceError('serverMaintenance', 503)); } else { core.championsAnalyzed = data.championsAnalyzed; core.patch = data.patch; core.patchHistory = data.patchHistory; next(); } }); } else { next(); } }; ================================================ FILE: models/web_champion_page.js ================================================ var mongoose = require('mongoose'); var webChampionPage = new mongoose.Schema({ role: String, key: String, general: [{ title:String, titleLink: String, val:String, position:Number, change:Number }], overallPosition: { position: Number, change:Number }, championMatrix:[Number], patchPlay:[Number], gameLength:[Number], experienceRate:[Number], experienceSample:[Number], patchWin:[Number], dmgComposition:{ physicalDmg: Number, magicDmg: Number, trueDmg: Number }, items:{ mostGames: { items: [{ id:Number, name:String, }], games: Number, winPercent: Number }, highestWinPercent: { items: [{ id:Number, name:String, }], games: Number, winPercent: Number } }, firstItems:{ mostGames: { items: [{ id:Number, name:String, }], games: Number, winPercent: Number }, highestWinPercent: { items: [{ id:Number, name:String, }], games: Number, winPercent: Number } }, trinkets: [{ item: { id:Number, name:String, }, games: Number, winPercent: Number }], summoners:{ mostGames: { summoner1: { name:String, url: String }, summoner2: { name:String, url: String }, games: Number, winPercent: Number }, highestWinPercent: { summoner1: { name:String, url: String }, summoner2: { name:String, url: String }, games: Number, winPercent: Number } }, skills:{ skillInfo: [{ name: String, img: String, key: String }], mostGames: { order: [String], games: Number, winPercent: Number }, highestWinPercent: { order: [String], games: Number, winPercent: Number } }, masteries:{ mostGames: { masteries: [{ tree: String, total: Number, data: {row1: [{mastery:Number, points:Number}], row2: [{mastery:Number, points:Number}], row3: [{mastery:Number, points:Number}], row4: [{mastery:Number, points:Number}], row5: [{mastery:Number, points:Number}], row6: [{mastery:Number, points:Number}]} }], games: Number, winPercent: Number }, highestWinPercent: { masteries: [{ tree: String, total: Number, data: {row1: [{mastery:Number, points:Number}], row2: [{mastery:Number, points:Number}], row3: [{mastery:Number, points:Number}], row4: [{mastery:Number, points:Number}], row5: [{mastery:Number, points:Number}], row6: [{mastery:Number, points:Number}]} }], games: Number, winPercent: Number } }, runes:{ mostGames: { runes: [{ id: Number, number: Number, name: String, img: String, description: String, }], games: Number, winPercent: Number }, highestWinPercent: { runes: [{ id: Number, number: Number, name: String, img: String, description: String, }], games: Number, winPercent: Number } }, unique:{ //champions like viktor mostGames: { order: [Number], games: Number, winPercent: Number }, highestWinPercent: { order: [Number], games: Number, winPercent: Number } }, matchups:[{ // For all champions key:String, statScore:Number, games:Number, winRate:Number, winRateChange:Number }], adcsupport:[{ // only support/ad key:String, statScore:Number, games:Number, winRate:Number, winRateChange:Number }], synergy:[{ // only support/ad key:String, statScore:Number, games:Number, winRate:Number, winRateChange:Number }] }); module.exports = mongoose.model('WebChampionPage', webChampionPage); ================================================ FILE: models/web_champion_roles.js ================================================ var mongoose = require('mongoose'); /** * Used to build up the long champ list on the homepage and the * individual champion roles on the left hand side of the champion pages. */ var webChampionRoles = new mongoose.Schema({ key: String, name: String, lastUpdated: Number, roles:[{ role:String, title:String, games:Number, percentPlayed:Number }] }); module.exports = mongoose.model('WebChampionRoles', webChampionRoles); ================================================ FILE: models/web_home_page_summaries.js ================================================ var mongoose = require('mongoose'); var webHomePageSummaries = new mongoose.Schema({ id: Number, data: [{ title: String, total: Number, mostImproved:{ key: String, name: String, difference: Number, overall: Number }, leastImproved:{ key: String, name: String, difference: Number, overall: Number }, highestWinRate:{ key: String, name: String, value: Number }, lowestWinRate:{ key: String, name: String, value: Number }, bestOverall:{ key: String, name: String, value: Number }, worstOverall:{ key: String, name: String, value: Number } }] }); module.exports = mongoose.model('WebHomePageSummaries', webHomePageSummaries); ================================================ FILE: models/web_matchup_page.js ================================================ var mongoose = require('mongoose'); var webMatchupPage = new mongoose.Schema({ champ1: { id: Number, key: String, name: String, role: String, roleTitle: String, performance: Number }, champ2: { id: Number, key: String, name: String, role: String, roleTitle: String, performance: Number }, role: String, dateAdded: Number, totalGames:Number, general: [{ title: String, champ1:{ val: Number, change: Number, score: Number //score to use is champ 1 is quering page }, champ2:{ val: Number, change: Number, score: Number //score to use if champ 2 is quering page }, }], championMatrix:{ labels:[String], champ1:[Number], champ2:[Number] }, goldLength:{ champ1:[Number], champ2:[Number] } }); module.exports = mongoose.model('WebMatchupPage', webMatchupPage); ================================================ FILE: models/web_overall_role_data.js ================================================ var mongoose = require('mongoose'); var webOverallRoleData = new mongoose.Schema({ role:String, totalNumber: Number, matrixLabels:[String], patchPlay: [Number] }); module.exports = mongoose.model('WebOverallRoleData', webOverallRoleData); ================================================ FILE: models/web_overall_stats.js ================================================ var mongoose = require('mongoose'); var webOverallStats = new mongoose.Schema({ patchHistory: [String], patch: String, championsAnalyzed: String, }); module.exports = mongoose.model('WebOverallStats', webOverallStats); ================================================ FILE: models/web_statistics_page.js ================================================ var mongoose = require('mongoose'); var webStatisticsPage = new mongoose.Schema({ key: String, title: String, role: String, general: { winPercent: Number, playPercent: Number, banRate: Number, experience: Number, kills: Number, deaths: Number, assists: Number, totalDamageDealtToChampions: Number, totalDamageTaken: Number, totalHeal: Number, largestKillingSpree: Number, minionsKilled: Number, neutralMinionsKilledTeamJungle: Number, neutralMinionsKilledEnemyJungle: Number, goldEarned: Number, overallPosition: Number, overallPositionChange: Number } }); module.exports = mongoose.model('WebStatisticsPage', webStatisticsPage); ================================================ FILE: package.json ================================================ { "name": "application-name", "version": "0.0.1", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "body-parser": "1.9.2", "compression": "1.2.0", "debug": "2.1.0 ", "ejs": "1.0.0", "express": "4.10.2", "mongoose": "3.8.19", "morgan": "1.5.0 ", "q": "1.4.1", "serve-favicon": "2.1.7" }, "devDependencies": { "grunt": "~0.4.0", "grunt-contrib-cssmin": "*", "grunt-contrib-uglify": "*", "grunt-contrib-watch": "*", "grunt-contrib-concat": "*", "grunt-contrib-jshint": "*", "grunt-htmlhint": "*", "matchdep": "*" } } ================================================ FILE: public/cpmstar/cpmstar_siteskin_iframebuster.html ================================================ ================================================ FILE: public/css/master.css ================================================ body{ position:relative; } .tooltip-hover{ z-index:88888888888; position:absolute; opacity:0; left:0; right:0; display:block; text-align:center; width:80%; max-width:300px; min-width:120px; transition:opacity 0.2s; } .tooltip-hover > .primary-content{ padding:10px; background-color:rgba(1,1,1,0.85); color:white; border-radius: 3px; z-index:99999999; } .arrow-down { width: 0; height: 0; margin:auto; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 10px solid rgba(1,1,1,0.85); } .center-google{ text-align:center; position:relative; } .mastery-row{ display: table; margin-left: auto; margin-right: auto; } a { color: #89f5a2; } .nav>li { position: relative; /* display: block; */ display: inline-block; } a:hover, a:focus { color: #FCFF90; /* text-decoration: underline; */ } .primary-hue{ /* background-color: rgba(205, 255, 240, 0.04); */ /* max-width: 1400px; */ margin: auto; /* background: url('../img/bg.jpg'); */; } [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; } .table { table-layout: fixed; } ::-moz-selection { /* Code for Firefox */ color: white; background: #A5FFBA; } ::selection { color: white; background: #A5FFBA; } .table-striped>tbody>tr:nth-child(odd)>td, .table-striped>tbody>tr:nth-child(odd)>th { background-color: #222425; } .visible-xxs{ display:none; } .col-xxs-1, .col-xxs-2, .col-xxs-3, .col-xxs-4, .col-xxs-5, .col-xxs-6, .col-xxs-7, .col-xxs-8, .col-xxs-9, .col-xxs-10, .col-xxs-11, .col-xxs-12 { min-height: 1px; padding-left: 15px; padding-right: 15px; position: relative; } @media (max-width: 600px) { .visible-xxs{ display:block; } .col-xxs-1, .col-xxs-2, .col-xxs-3, .col-xxs-4, .col-xxs-5, .col-xxs-6, .col-xxs-7, .col-xxs-8, .col-xxs-9, .col-xxs-10, .col-xxs-11 { float: left; } .col-xxs-1 { width: 8.333333333333332%; } .col-xxs-2 { width: 16.666666666666664%; } .col-xxs-3 { width: 25%; } .col-xxs-4 { width: 33.33333333333333%; } .col-xxs-5 { width: 41.66666666666667%; } .col-xxs-6 { width: 50%; } .col-xxs-7 { width: 58.333333333333336%; } .col-xxs-8 { width: 66.66666666666666%; } .col-xxs-9 { width: 75%; } .col-xxs-10 { width: 83.33333333333334%; } .col-xxs-11 { width: 91.66666666666666%; } .col-xxs-12 { width: 100%; } .col-xxs-push-1 { left: 8.333333333333332%; } .col-xxs-push-2 { left: 16.666666666666664%; } .col-xxs-push-3 { left: 25%; } .col-xss-push-4 { left: 33.33333333333333%; } .col-xxs-push-5 { left: 41.66666666666667%; } .col-xxs-push-6 { left: 50%; } .col-xxs-push-7 { left: 58.333333333333336%; } .col-xxs-push-8 { left: 66.66666666666666%; } .col-xxs-push-9 { left: 75%; } .col-xxs-push-10 { left: 83.33333333333334%; } .col-xxs-push-11 { left: 91.66666666666666%; } .col-xxs-pull-1 { right: 8.333333333333332%; } .col-xxs-pull-2 { right: 16.666666666666664%; } .col-xxs-pull-3 { right: 25%; } .col-xxs-pull-4 { right: 33.33333333333333%; } .col-xxs-pull-5 { right: 41.66666666666667%; } .col-xxs-pull-6 { right: 50%; } .col-xxs-pull-7 { right: 58.333333333333336%; } .col-xxs-pull-8 { right: 66.66666666666666%; } .col-xxs-pull-9 { right: 75%; } .col-xxs-pull-10 { right: 83.33333333333334%; } .col-xxs-pull-11 { right: 91.66666666666666%; } .col-xxs-offset-1 { margin-left: 8.333333333333332%; } .col-xxs-offset-2 { margin-left: 16.666666666666664%; } .col-xxs-offset-3 { margin-left: 25%; } .col-xxs-offset-4 { margin-left: 33.33333333333333%; } .col-xxs-offset-5 { margin-left: 41.66666666666667%; } .col-xxs-offset-6 { margin-left: 50%; } .col-xxs-offset-7 { margin-left: 58.333333333333336%; } .col-xxs-offset-8 { margin-left: 66.66666666666666%; } .col-xxs-offset-9 { margin-left: 75%; } .col-xxs-offset-10 { margin-left: 83.33333333333334%; } .col-xxs-offset-11 { margin-left: 91.66666666666666%; } } /* cyrillic-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/ek4gzZ-GeXAPcSbHtCeQI_esZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } /* cyrillic */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } /* latin-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/fg2nPs59wPnJ0blURyMU3PesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } /* cyrillic-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/77FXFjRbGzN4aCrSFhlh3hJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } /* cyrillic */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/isZ-wbCXNKAbnjo6_TwHThJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/UX6i4JxQDm3fVTc1CPuwqhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/jSN2CGVDbcVyCnfJfjSdfBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/PwZc-YbIL414wB9rB1IAPRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } /* latin-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/97uahxiqZRoncBaCEI3aWxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), url(http://fonts.gstatic.com/s/roboto/v14/d-6IYplOFocCacKzxwXSOBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#E0E0E0;} h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:#ffffff} body{ background: url('../img/bg.jpg'); background-color: #181c1c; } .logo-holder{ float:left; /* padding:10px; */ width:33%; margin-top: 2px; } .search-holder{ float:left; /* margin-top: 10px; */ padding:10px; width: 25%; position: relative; top: -14px; left: 58px; } .navigation-holder{ float:right; padding:10px; padding-right:0px; width: 38%; } .search-fb-holder{ margin:auto; width: 230px; } /* .fb-like{ display:block!important; height: 20px !important; width:60px; position: absolute !important; }*/ .analysis-holder{ margin: auto; margin-top: 18px; text-transform: capitalize; /* width: 300px; */ color: #717777; text-align: right; padding-right: 20px; } .analysis-holder strong{ font-size:1.2em; color:#EDEDEF; margin-left:3px; display: inline-block; } .analysis-holder .spacer{ margin-left: 7px; margin-right:7px; } .input-group { display: block; position: relative; top: 20px; margin: auto; } .dropdown-search{ position: relative; top: -11px; } h1{ color:#89f5a2; text-align: center; margin-bottom: 18px; } h2 { color: #FFED68; font-size: 1.42em; margin-top: 5px; text-align: center; } .first-column{ width:37% } .second-column{ width:21% } .third-column{ width:21% } .fourth-column{ width:21% } .navbar-brand img{ margin-top: -6px; margin-left: 34px; width: 75%; max-width: 330px; } .navbar-nav { float: left; margin-top: 0px; margin-right:15px; /* margin-bottom: -4px; */ margin-left: 20px; display:inline-block; } .nav>li>a:hover, .nav>li>a:focus { text-decoration: none; background-color: #15181A; /* border-left: 1px solid rgb(43, 49, 52); */ /* border-right: 1px solid rgb(43,49,52); */ } .champion-area h2{ margin-bottom:20px; margin-top: 20px } .champion-area h2.line-chart-header{ margin-bottom:10px; } .navbar-inverse { /* background-color: #222222; */ border:none; } .social-media{ display: block; /* position: absolute; */ /* top: -12px; */ /* float: right; */ /* top: 12px; */ /* right: 10px; */ /* left: 13px; */ float: right; margin-top: 10px; } .champion-area div h2:first-child { margin-top: 12px; } .navbar { /* min-height: 85px; */ margin-bottom: 0px; text-transform: uppercase; font-weight: bold; background-color: transparent; } .champion-home-list{ width: 97%; margin: auto; padding-top: 30px; } .main-container{ max-width:1400px; width: 95%; margin:auto; background-color: rgba(176, 219, 255, 0.03); /* margin-top: 15px; */ box-shadow: 0px 0px 15px rgba(0, 0, 20, 0.21); border: 1px solid #121617; /* border-left: 1px solid rgb(35, 42, 44); */ /* border-right: 1px solid rgb(35,42,44); */ } .matchup-chart-holder{ width:70%; margin:auto; } .navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:hover, .navbar-inverse .navbar-nav>.active>a:focus { color: #F7F7F7; background: none; } .nav li { margin-left: 40px; } .navbar-inverse .navbar-nav>li>a:hover, .navbar-inverse .navbar-nav>li>a:focus { color: #89f5a2; background-color: transparent; transition:color 0.15s; } .matchupSearch{ background-color: #181C1D; padding: 6px; padding-top: 9px; padding-left: 12px; font-size: 13px; border: 1px solid rgb(51, 51, 51); width: 90%; margin-bottom: 2px; margin-right: 16px; position: relative; top: 1px; color:white; } .matchups .row{ margin-left:0px; margin-right:0px; z-index: 99999999; position: relative; /* border-bottom: 1px solid #2C2C2C; */ } .matchupSort{ background-color: #333; border: 1px solid grey; color: white; padding: 5px; margin-left:3px; } .nav li.first-button{ margin-left:0px; } .navbar-inverse .navbar-nav>li>a { color: #F7F7F7; } .update-happening{ /* margin-top: 15px; */ /* text-align: center; */ /* width: 100%; */ /* margin-bottom: 15px; */ /* display: inline-block; */ /* width: 50%; */ text-align: center; top: 10px; position: relative; z-index:-9999999; /* float: left; */ /* width: 90%; */ /* width: 100%; */ /* margin-left: 100px; */ } .champion-area{ border-color: #0F0F0F; /*background-color: rgba(32, 32, 34, 0.76)*/; width:100%; /* margin-top:15px; */ } .navigation-elem{ /* width: 95%; */ max-width: 1400px; /* background-color: rgb(28, 33, 35); */ border-bottom: 1px solid rgb(38, 43, 47); border-top: 1px solid rgb(27, 34, 34); margin: auto; } .fb_iframe_widget { display: inline-block; position: relative; top: -5px; } .inner-nav{ max-width: 1395px; margin: auto; width: 97%; position: relative; } .champion-area .row{ margin-top: 25px; } .btn-success { color: #fff; background-color: #89f5a2; border-color: #4cae4c; } .btn-success:hover { color: #fff; background-color: #89f5a2; border-color: #4cae4c; } #query{ background-color: #1E2325; border: 1px solid rgb(43, 49, 52); font-weight: 400; color: rgb(255, 255, 255); } div.champion-area .container-fluid .row > div { border-right: 1px solid rgb(50, 55, 60); } div.champion-area .container-fluid .row > div:last-child, div.matchups .container-fluid .row > div:last-child { border-right: none; } div.champion-area .container-fluid .row{ padding-bottom: 18px; } .matchups { /* background-color: #1D1D1F; */ } .champ-img{ display:block; margin:auto; margin-top:5px; } .champ-index-img{ border-right: 1px solid rgb(43, 49, 52); } .champion-list-header{ margin-left: 50px; margin-bottom: 10px; display: block; font-size: 1.18em !important; text-transform: uppercase; } .summary .champion-list-header{ /* margin-left: 25px; */ text-align:center; margin-left:0px; margin-bottom:6px; } .large-header{ /* margin-top: 25px; */ /* background-color: rgb(24, 28, 29); */ padding: 20px 12%; background-image: url('/img/header-bg.jpg'); /* background-repeat: no-repeat; */ background-position-y: -80px; /* border-bottom:1px solid rgb(23, 27, 31); */ /* max-width: 1340px; */ margin: auto; /* display: none; */ } .large-header h2{ font-size:1.75em; line-height:1.6em } .glyphicon-search{ color: rgb(43, 43, 43); } a.twitter-share-button{ color:rgba(0,0,0,0.1); } .champion-profile ul, .champion-statistics ul{ padding:0px; margin-left:0px; } h2.champion-stats{ margin-top: 30px;margin-bottom: 14px; } .champion-statistics ul li{ list-style:none; } .champion-profile ul li{ list-style: none; text-align: center; } .champion-profile h3{ margin-bottom:0px; } .selected-role{ border: 1px solid rgb(50, 57, 60); padding: 10px; background-color: rgba(31, 36, 39, 1); margin-top:13px; } .selected-role h3{ margin-bottom:0px; margin-top:0px; color:#89f5a2; } .champion-profile ul li a:hover{ text-decoration: none; } .champion-profile ul li a:hover h3{ color:#89f5a2; } .summary{ /* margin-top: 45px; */ /* width: 98%; */ /* margin-bottom: 55px; */ padding-top: 15px; /* background-color: rgba(24, 28, 29, 1); */ /* padding-bottom: 18px; */ /* padding-left: 1%; */ padding-right: 1%; max-width: 1350px; margin: auto; } .champion-name{ font-weight:bold; font-size:1.2em; color:#E0E0E0; } .stats-role-title{ color:#FFE08C; } .glyphicon-arrow-up, .top-half, .green-death-arrow{ color:#89f5a2; } .glyphicon-arrow-up.Deaths-title{ color: #F17D59; } .glyphicon-arrow-down, .bottom-half, .red-death-arrow{ color: #F17D59; } .glyphicon-arrow-down.Deaths-title{ color: #89f5a2; } .above-average{ background-color:#89f5a2; } .below-average{ background-color:#F17D59; } .average{ background-color:#bdb97e; } .same-position::after { content: "-"; color: #555; font-size:1.2em; margin-right:3px; margin-left:1px; font-weight: bold; } .table>thead>tr>th, .table>tbody>tr>th, .table>tfoot>tr>th, .table>thead>tr>td, .table>tbody>tr>td, .table>tfoot>tr>td { padding: 8px; line-height: 1.42857143; vertical-align: middle; border-top: none; } .champion-statistics .table-striped>tbody>tr:nth-child(odd)>td, .table-striped>tbody>tr:nth-child(odd)>th { background-color: rgba(31, 36, 39, 1); } .champion-statistics .table-striped>tbody>tr.highlighted>td { background-color: rgb(41, 48, 52); } .matchups .table-striped>tbody>tr:nth-child(odd)>td, .table-striped>tbody>tr:nth-child(odd)>th { background-color: #212325; } .champion-statistics tr>td:first-child, .summary-side tr>td:first-child, .matchups tr>td:first-child, #matchups tr>td:first-child{ color: #FFE08C; padding-left: 15px; } .champion-statistics tr>td:nth-child(3), .champion-statistics tr>td:nth-child(4), .champion-statistics tr>th:nth-child(3), .champion-statistics tr>th:nth-child(4){ text-align:center; } .champion-statistics tr:last-child>td:first-child { font-weight:bold; text-transform:uppercase; color:#89f5a2; } .champion-statistics tbody tr:last-child { border: 1px solid #2E3638; } .champion-statistics tr>td:nth-child(2) { font-weight: bold; } .table>thead>tr>th { border-bottom: 1px solid rgb(43, 49, 52); } .btn{ border-radius:0px; } .form-control, .input-group-btn button{ height: 42px; /* margin-bottom: 4px; */ } .form-control:focus { /* border-color: #66afe9; */ outline: 0; -webkit-box-shadow: 0 0 10px rgba(5, 5, 5, 0.6); box-shadow: 0 0 10px rgba(5, 5, 5, 0.6); } .background-colourisation{ background-color:rgba(126, 211, 255, 0.015); } .navbar, .matchups, .champion-area{ padding-left: 10px; padding-right: 10px; border-radius: 0px; } .navbar { min-height:75px; /* width: 95%; */ /* background-color: rgb(27, 32, 34); */ max-width: 1400px; /* width: 95%; */ /* height: 100%; */ background-color: rgba(25, 30, 31, 1); padding-bottom: 15px; margin: auto; border-bottom: 1px solid rgb(38, 43, 47); } .donate{ position:absolute; right: 30px; top: 5px; text-transform:none; font-size:0.8em; } .counter-row{ margin-top:25px; } .counter-column{ border-right: 1px solid rgb(55, 59, 66); } .matchup-settings{ border: 1px solid rgb(55, 59, 66); padding:10px; display:inline-block; } .matchups h2{ margin-bottom:20px; display:inline-block; } .counter-matchups{ /* background-color: #242427; */ padding: 6px 4px 7px; margin-bottom: 0px; /* position:relative; */ transition:background-color 0.05s; } .counter-matchups:hover{ background-color: #151A1A; } .counter-matchups > div{ padding-left: 8px; } .counter-matchups h3{ display: inline-block; position: absolute; font-size: 1.25em; left: 48px; top: -21px; } .matchups .col-sm-3{ padding-right:0px; } .view-more-comment{ position:absolute; left: 74px; top: 21px; cursor:pointer; transition:color 0.15s; } .view-more-comment, .view-more-stats, .view-linked-page{ color:#FAFAFA; } .view-more-comment:hover, .view-more-stats:hover, .view-linked-page:hover{ color:rgb(255, 83, 83); } .chart-holder{ width:90%;margin:auto; } .middle-graphic-holder{ width:95%;margin:auto; } .damage-dealt{ height:25px; position:relative; } .damage-dealt > div{ position:absolute; height:100%; } .matchup-values-width{ width:19%; } .matchup-title-width{ width:24%; } .matchup-champ-img-width{ width:34%; text-align:center; } .matchup-div-header{ display:inline-block; } .view-more-stats{ position:absolute; left: 50px; top: 21px; cursor:pointer; transition:color 0.15s; } .view-linked-page { position: absolute; left: 96px; top: 21px; cursor: pointer; transition: color 0.15s; } .matchups{ padding-bottom:20px; } .matchup-champion-info{ width:27.5%;display:inline-block;position:relative;top: 3px; } .matchup-champion-info h3:hover{ color:#89f5a2; } .progress{ margin-bottom:0px; width: 80%; margin:auto; background-color: #404146; height:18px; position:relative; } .statistic-rating{ overflow: visible; display: inline; position: absolute; text-shadow: -1px 1px 1px black, 1px 0px 1px rgba(1,1,1,0.8); color: white; margin: auto; left: 20%; } .left+.show-more{ color:#F17D59; } .right+.show-more{ color:#89f5a2; } .show-more{ height: 40px; text-align: center; padding: 8px; cursor: pointer; width: 100%; background-color: rgb(32, 37, 40); z-index: 99999999; position: relative; border: 1px solid rgb(24, 27, 31); border-top:none; } .show-more:hover{ background-color: rgb(25, 29, 29); color: white; border: 1px solid rgb(27, 31, 37); border-top: none; } .show-more:active{ background-color: rgb(37, 42, 45); } .important-message{ color: #FD6F6F; /* margin-right:15px; */ margin-right: 160px; } .matchup-stats{ display:inline-block; margin-right:1px; width:42%; padding-right:8px; /* text-align:center; */ } .matchup-wrapper{ width:100%; text-align:center; margin-top: 12px; } .matchup-stats small{ margin-left: 10%; } .matchup-stat-area{ background-color: rgba(22, 22, 24, 0.36); margin-right:5px; margin-top: 15px; } .total-rating{ float: left; padding: 2px; width: 30%; height: 19px; position:relative; /* margin-left: 2px; */ /* margin-right: 5px; */ color: white; border-radius: 3px; } .winrating-area{ position: relative; width:28%; display:inline-block; text-align:center; } .winrating-area strong{ display: block; position:relative; top: -8px; font-size:1.3em; } #matchups h2 { margin-bottom:20px;margin-top:25px } .champ-user-rating small{ position: relative; top: -4px; } .overall-score-rating{ width:23%;display:inline-block;position:relative;top:-15px; } ul.user-rating li{ display:inline-block; width: 18%;height:19px;background-color: #404146;margin-left:1px;cursor:pointer; transition:background-color 0.15s, border 0.35s; border:2px solid transparent; border-radius:1px; } .physical-dmg{ background-color:#FF5353; } .magic-dmg{ background-color:rgba(101, 228, 245, 1); } .true-dmg{ background-color:rgba(107, 107, 107, 1); } .stat-sorter{ float:left; width: 42%;text-align:center; } .winrate-sorter{ float:left; width: 28%;text-align:center; } .search-sorter{ float:left; width: 29%;text-align:center; } .winrate-sorter, .stat-sorter{ cursor:pointer; padding: 8px 0px; } .user-sorter:not(.selected-sorter):hover, .overall-sorter:not(.selected-sorter):hover, .stat-sorter:not(.selected-sorter):hover{ color:white; background-color: rgb(26, 29, 30); } .selected-sorter{ font-weight:bold; color:white; background-color: rgb(37, 43, 47);} .radar-legend, .line-legend, .pie-legend { text-align:center; list-style-type: none; padding-left: 0px; } .pie-legend { text-align:left; } .radar-legend li, .line-legend li{ display:inline-block; padding:10px; } .pie-legend li { display: block; } .radar-legend li span, .line-legend li span, .pie-legend li span { width: 12px; height: 12px; position: relative; top: 4px; display: block; float: left; margin-right: 6px; } .disqus{ padding-right:6px; padding-left:14px; } .first-summary{ width:16%; } .second-summary{ width:42%; } .third-summary{ width:42%; } .summary-side .matchup-champion { transform: scale(0.7); margin-left: -4px } .summary-side .table .champion-name{ position: absolute; left: 42px; top: 11px; font-size: 1em; } .summary-side .table .champion-name:hover{ color: #FCFF90; } .summary-value{ background-color: rgb(27, 32, 33); right: 10px; position: absolute; top: 15px; font-size:0.9em; } .summary-side .table-striped>tbody>tr:nth-child(odd)>td, .summary-side .table-striped>tbody>tr:nth-child(odd)>td .summary-value { background-color: #191E1F; } .summary-side tr>td:nth-child(2), .summary-side tr>td:nth-child(3){ position:relative; } .summary-side tr>th:nth-child(2), .summary-side tr>th:nth-child(3){ text-align:center; } .summary-side tr>td:nth-child(1), .summary-side tr>td:nth-child(2){ border-right: 1px solid rgb(43, 49, 52); } .win-summary{ width:36.75%; padding: 1.25%; float:left; } .overall-summary{ width:30.5%; padding: 1.25%; float:left; } .change-summary{ width:32.75%; padding: 1.25%; float:left; } .viktor-skills, .trinket-stats{ margin-top: 35px; position: relative; left: 6px; } .viktor-skills small, .trinket-stats small{ position: relative; left: -7px; } .viktor-skill, .trinket-single{ width:25%; position:relative; display: inline-block } .viktor-skill img, .trinket-single img{ width:36px; height:36px; } .viktor-key, .trinket-key{ position:absolute; top:4px; } /* ul.user-rating li:hover { background-color:white; }*/ .build-wrapper, .summoner-wrapper{ margin:auto; text-align:center; } .build-wrapper small{ font-weight:bold; margin-left:2px; margin-right:2px; } .possible-build, .possible-summoner { display:inline-block; max-width:36px; max-height:36px; } .build-text, .summoner-text{ margin-top: 10px; display: block; text-align: center; margin:bottom:5px; white-space: nowrap; } .build-text strong, .summoner-text strong{ color: rgb(136, 244, 161); } .skill-order .build-text{ margin-top:20px; } .rune-img img{ width:42px; height:42px; margin-left: 10px; margin-right: 10px; } span.rune-title { color: rgb(101, 228, 245) } .rune-type-area{ background-color: #202528; } .rune-img strong { margin-right: 12px; } h4{ color:#89f5a2; margin-bottom:25px; text-align:center; } .description{ font-size:0.85em; } .description>.highlight{ color: #89f5a2 !important; font-size: 1.15em; font-weight: bold; border: 1px solid; padding: 2px; margin: 5px; } div.matchup-table{ margin: 0 auto; display: block; margin-top: 10px; margin-bottom: -8px; } .dropdown-menu { position: absolute; background-color:transparent; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 160px; padding: 3px 0; margin: 0 0; font-size: 14px; text-align: left; list-style: none; border: 1px solid #2F2F2F; border-radius: 0px; -webkit-box-shadow: 0 6px 20px rgba(0,0,0,.875); box-shadow: 0 6px 20px rgba(0,0,0,.875); } .dropdown-menu>li>a { display: block; padding: 3px 20px; clear: both; font-weight: 400; line-height: 1.42857143; color: #CBCBCB; text-transform:none; white-space: nowrap; background-color: rgb(33, 34, 36); } .dropdown-menu>.active>a, .dropdown-menu>.active>a:hover, .dropdown-menu>.active>a:focus { color: #fff; text-decoration: none; background-color: #3FBB83; outline: 0; } .minimum-games{ background-color: #242527; border: 1px solid rgb(71, 76, 81); font-weight: 400; color: rgb(236, 236, 236); } /* Possibly Use */ .counter-matchups { background-color:none; border-bottom: 1px solid #292D34; } matchups div:nth-child(odd) > .counter-matchups { background-color: rgba(26, 29, 29, 0.44); /* background-color: rgba(31, 42, 41, 1); */ } matchups div:nth-child(odd) > .counter-matchups:hover { background-color: #16191A; } .left:first-child{ border-top: 1px solid #F17D59; } .right:first-child{ border-top: 1px solid #89f5a2; } #stats{ width: 98.5%; margin: auto; margin-top: 20px; } #stats thead{ background-color: rgb(13, 144, 114); font-weight: bold; } #stats thead tr td{ cursor:pointer; color:white; } #stats thead tr td:hover{ background-color:rgb(10, 118, 93); } #stats thead .selected-column { background-color:rgb(10, 118, 93); color: #FFE08C; } #stats tbody .selected-column { background-color: rgba(37, 44, 45, 0.92); color: #FFE08C; } #stats .down .selected-column::after { content: '\25B2'; } #stats .up .selected-column::after { content: '\25BC'; } #stats td{ text-align:center; box-sizing: content-box; } #stats tbody td{ padding-top: 12px; padding-bottom: 12px; font-weight:bold; } #stats tr>td:nth-child(2){ text-align:left; padding-left:11px; } #stats .table-striped>tbody>tr:nth-child(odd)>td, #stats .table-striped>tbody>tr:nth-child(odd)>th { background-color: #191D20; } #stats .table-striped>tbody>tr:nth-child(even)>td, #stats .table-striped>tbody>tr:nth-child(even)>th { background-color: #1F2325; } #stats .table>thead>tr>th, #stats .table>tbody>tr>th, #stats .table>tfoot>tr>th, #stats .table>thead>tr>td, #stats .table>tbody>tr>td, #stats .table>tfoot>tr>td { border-left: 1px solid rgba(0, 0, 0, 0.21); border-right: 1px solid rgba(0,0,0,0.21); } #stats div.matchup-champion{ transform: scale(0.75); } span.stat-champ-title{ position: relative; /* top: -6px; */ font-weight:bold; } #header-fixed { position: fixed; top: 0px; display:none; z-index: 9999999; } #matchups .progress-bar{ background-color:rgba(101, 228, 245, 1); } #matchups .synergy .progress-bar{ background-color: rgba(107, 219, 147, 1); } #matchups .progress{ margin-bottom:20px; background-color:#FF5353; } #matchups .synergy .progress{ background-color: #383838; } .champ-height { min-height: 175px; text-align: center; width: 12%; float: left; } iframe.center{ display: block; margin: auto; position:relative; } .matchup-progress-bars { margin-bottom: 30px; margin-top: 25px; background-color: rgba(20, 26, 29, 0.27); padding-top: 10px; padding-bottom: 10px; } .matchup-progress-bars h4{ margin-bottom:7px; margin-top:14px; } .matchup-progress-bars h2{ margin-bottom:18px; margin-top:7px; } h1.champ1, .victor1{ color:rgb(101, 228, 245); } h1.champ2, .victor2{ color:#FF5353; } .matchup-header{ text-align:center; } .reddit-titles { padding:4px 4px 4px 12px } .container-reddit{ margin-bottom:8px; /* background-color: #1B2021; */ } .container-reddit h4{ text-align:left;margin-bottom:5px;margin-top:2px } .reddit-summary{ cursor:pointer;margin-right:15px;font-weight:bold;color:rgb(215, 215, 215); } .reddit-summary:hover{ color:white; } .reddit-comments{ margin-right:15px; color:#428bca; } .reddit-date{ color:#f7f7f7 } .reddit-summary-area{ background-color: rgba(1, 1, 1, 0.12);padding:12px;margin-top:4px;border:1px solid #333;color:#C7C7C7; } .footer-attr{ margin-top: 15px; padding: 15px 15%; font-size: 0.84em; color: #A3A3A3; text-align: center; } .navbar .container-fluid{ /* max-width: 1400px; */ /* width:95%; */ /* height: 100%; */ /* background-color: rgba(176, 219, 255, 0.03); */ /* padding-bottom: 15px; */ } .matchup-champion{ width: 36px; height: 36px; margin-top: -5px; display: inline-block; /* float: left; */ margin-left: -7px; margin-bottom: -12px; padding: 0px; /* margin: 0px; */ /* position: initial; */ } .home-champion{ width: 48px; height: 48px; margin:auto; margin-bottom: 10px; display: block; position: relative; transform: scale(1.25); } .summary h3{ text-align:center; color: rgba(101, 245, 179, 1); font-size:1em; font-size: 1.4em; } .bottom-champ{ margin-top: 14px; } .stat-summary-box{ margin-bottom:25px; width:19%;display:inline-block; } .stat-summary-box h3{ text-align:center; padding-bottom: 8px; padding-top: 6px; /* background-color: rgba(25, 29, 32, 1); */ border: 1px solid #3F986F; font-weight: bold; font-size: 1.1em; text-transform: uppercase; } .stat-summary-container{ text-align:center; } .stat-summary-container>div{ border-left: 1px solid rgb(39, 46, 48); } .stat-summary-container.first-summary>div{ border-left: 0px; } .summary-highlight{ color: #FFED68; } /*Skill Order*/ .skill-order{ /*background-color:black;*/ width:100%; } .skill-order > div:nth-of-type(odd){ background-color: rgb(32, 37, 40); } .skill-order > div:first-child{ background-color: rgb(22, 27, 27); text-align: center; font-weight: bold; font-size:1.1em; } .skill-order > div:first-child div span{ position:relative; top:6px; } .selected span{ color:black; font-size:0.8em; font-weight: bold; } .skill{ width:100%; float:left; overflow:hidden; } .skill-order img, .img-placeholder{ width:38px; height:38px; float:left; } .skill-selections{ overflow:hidden; } .skill-selections > div { width:5.555%; padding-left:2px; padding-right:2px; height:38px; border-left: 1px solid rgba(50, 57, 60, 0.66); border-bottom: 1px solid rgba(50, 57, 60, 0.66); border-collapse: collapse; float:left; } .skill-selections > div.selected { background-color:rgb(101, 228, 245); border: 1px solid black; } /* masteries */ .mastery0, .mastery1, .mastery2{ display: inline-block; cursor:default; position:relative; padding:10px; background-size:120%; border:1px solid rgb(55, 59, 66); margin-left:10px; } .mastery-header{ font-weight:bold; font-size:1.1em; text-align:center; } .mastery0{ background-image:url('/img/mastery0.jpg?v=5.22.3'); } .mastery1{ background-image:url('/img/mastery1.jpg?v=5.22.3'); } .mastery2{ background-image:url('/img/mastery2.jpg?v=5.22.3'); } .mastery-container{ border-right:none !important; position:relative; text-align: center; } .mastery-icon{ margin: 3px; border: 1px solid grey; width: 40px; height: 40px; display: block; background-size: initial; position:relative; opacity:0.35; float: left; z-index:88; } .mastery-icon .points {display: block;width:100%;height: 8px;padding: 0px;margin: 0px;background-color:rgba(0, 0, 0, 0.83);position:absolute;bottom:0px;} .mastery-icon .point {display: block;position: relative;background-color:yellow;width: 4px;height: 4px;margin-left: 3px;top: 3px;float: left;} .mastery-spacer{ width: 40px; height: 40px; margin: 3px; position:relative; display: block; float: left; } .mastery-active{ border: 2px solid black; opacity:1; } .mastery-icon.double-mastery-left-indent{ margin-left: 124px; } .mastery-icon.mastery-left-indent{ margin-left: 62px; } .mastery-icon.tripple-mastery-left-indent{ margin-left: 186px; } .mastery-points{ display:none; } .mastery-selected-3 .mastery-points{ width:100%; height:10px; background-color:black; position: absolute; display:block; } .faq-column{ padding: 30px; padding-top: 5px; } .faq{ padding:10px; padding-top:0px; } .faq h2{ margin-top:40px; text-align:left; } .faq p{ line-height:1.1em; line-height: 1.9em; color: rgb(187, 187, 187); } .navbar-nav>li>a { font-weight: bold; text-transform: uppercase; } .matchup-champion.dropdown-img{ margin-left: -9px; margin-right: 8px; margin-bottom: -4px; margin-top: 0px; } .counter-matchups .matchup-champion { margin-top: 0px; margin-left: 0px; margin-bottom: 0px; margin-left: -4px; } /* Responsive */ @media (min-width: 992px) and (max-width: 1267px) { .progress{ width:95%; } } @media (min-width: 1060px) and (max-width: 1240px) { .navigation-holder .nav li{ margin-left:5px; } .navigation-holder{ width:44%; } .search-holder{ width:24%; left:0px; } .logo-holder{ width:30%; } } @media (min-width: 775px) and (max-width: 1060px) { .logo-holder{ width:50%; } .navigation-holder{ width:100%; } .navbar .nav{ text-align: center; margin-top:8px; float:none; margin-top: 4px; margin-bottom: 15px; } .navbar-nav>li { float: none; } .large-header{ margin-top:0px; padding:8px 10%; } .champion-area{ margin-top: -25px; } } @media (max-width: 1267px) { .win-summary{ width:58%; } .overall-summary{ width:42%; } .change-summary{ width:100%; } } @media (max-width: 1300px) { .important-message{ margin-right:0px; } } @media(max-width:1060px){ .analysis-holder{ text-align:center; margin-top: 5px; margin-bottom: -7px; } .navbar{ margin-bottom:12px; } .navbar-nav { margin-bottom:0px; } .update-happening{ float:left; width:100%; top:0px; margin-bottom:7px; } } @media (max-width: 900px) { .win-summary{ width:100%; } .overall-summary{ width:100%; } .change-summary{ width:100%; } } @media (min-width: 768px) { .navbar-nav>li>a { padding-top: 12px; padding-bottom: 12px; } } @media (max-width: 775px) { .logo-holder{ width:100%; text-align:center; } .search-holder{ width:100%; margin-top: -12px; left:0px; } .navigation-holder{ width:100%; padding:0; } .navbar-brand { float:none; } .navbar-nav>li { float: none; margin-left: 15px; } .navbar .nav{ text-align: center; float:none; margin-top: 4px; margin-bottom: 15px; } .large-header{ margin-top:0px; padding:4px 10%; } .champion-area .row{ margin-top:0px; } .champ-height{ width:18%; } .champion-list-header{ margin-left:25px; } } @media (max-width: 992px) { .counter-column { border-right: 0; } .matchup-area{ padding-right:0px; padding-left:0px; } .matchup-area.row, .matchup-area .row{ margin-right:0px; margin-left:0px; } .matchup-block{ padding-right:0px; padding-left:0px; } #matchups h2 { margin-bottom:5px;margin-top:5px } } @media (max-width: 600px) { .social-media{ float:none; margin-left:20px; } .stat-sorter{ width:40%; } .winrate-sorter{ width:20%; } .search-sorter{ width:35%; } .matchup-champion-info{ width:35%; } .matchup-stats{ width:40%; } .winrating-area{ width:20%; } .nav li.first-button{ margin-left:5px; } .matchup-wrapper{ margin-bottom:-30px; } .matchups h2{ margin-top:20px; margin-bottom:12px; } .logo-holder { padding: 10px 0px 0px 0px; } div.champion-area .container-fluid .row > div { border-right: 0; } .counter-column { padding:0; } .navbar, .matchups, .champion-area { padding-left: 0; padding-right: 0; } .progress{ width:95%; } div.champion-area .container-fluid .row > div:last-child, div.matchups .container-fluid .row > div:last-child { padding: 0px; } .counter-matchups{ padding-top: 14px; padding-bottom: 15px; } .matchup-stats small{ margin-left:0; } .large-header{ margin-top:0px; padding:6px 4%; background-position-y: 0px; } .large-header h2{ font-size:1.4em; } .champ-height{ width:24%; } .stat-summary-box { width: 32%; margin-bottom:15px; } .first-column, .second-column, .third-column, .fourth-column{ width:25% } .champion-area div h2:first-child { margin-top: 30px; } } @media (max-width: 495px) { .champ-height{ width:32%; } } @media (max-width: 410px) { .navbar-nav{ margin:0px; } .nav li { margin-left: 2px; } .nav li.first-button{ margin-left:7px; } .overall-score-rating{ padding-left:0px; width:22%; } .logo-holder img{ width:80%; } } @media (max-width: 360px) { .nav li.first-button{ margin-left:7px; } .overall-score-rating{ padding-left:0px; width:15%; } .matchup-champion-info{ width:42%; } .matchup-stats{ width:30%; } .winrating-area{ width:20%; } } ================================================ FILE: public/css/sprite.css ================================================ /* This CSS was generated by champion.gg's asset_generator */ .matchup-champion{ background-image:url('../img/small_champion.jpg?v=7.11.1');} .home-champion{ background-image:url('../img/champion.jpg?v=7.11.1');}.mastery-icon{ background-image:url('http://ddragon.leagueoflegends.com/cdn/7.11.1/img/sprite/gray_mastery0.png'); } .mastery-active{ background-image:url('http://ddragon.leagueoflegends.com/cdn/7.11.1/img/sprite/mastery0.png');}#home .Aatrox { background-position:-0px -0px; }#home .Ahri { background-position:-48px -0px; }#home .Akali { background-position:-96px -0px; }#home .Alistar { background-position:-144px -0px; }#home .Amumu { background-position:-192px -0px; }#home .Anivia { background-position:-240px -0px; }#home .Annie { background-position:-288px -0px; }#home .Ashe { background-position:-336px -0px; }#home .AurelionSol { background-position:-384px -0px; }#home .Azir { background-position:-432px -0px; }#home .Bard { background-position:-0px -48px; }#home .Blitzcrank { background-position:-48px -48px; }#home .Brand { background-position:-96px -48px; }#home .Braum { background-position:-144px -48px; }#home .Caitlyn { background-position:-192px -48px; }#home .Camille { background-position:-240px -48px; }#home .Cassiopeia { background-position:-288px -48px; }#home .Chogath { background-position:-336px -48px; }#home .Corki { background-position:-384px -48px; }#home .Darius { background-position:-432px -48px; }#home .Diana { background-position:-0px -96px; }#home .Draven { background-position:-48px -96px; }#home .DrMundo { background-position:-96px -96px; }#home .Ekko { background-position:-144px -96px; }#home .Elise { background-position:-192px -96px; }#home .Evelynn { background-position:-240px -96px; }#home .Ezreal { background-position:-288px -96px; }#home .Fiddlesticks { background-position:-336px -96px; }#home .Fiora { background-position:-384px -96px; }#home .Fizz { background-position:-432px -96px; }#home .Galio { background-position:-0px -144px; }#home .Gangplank { background-position:-48px -144px; }#home .Garen { background-position:-96px -144px; }#home .Gnar { background-position:-144px -144px; }#home .Gragas { background-position:-192px -144px; }#home .Graves { background-position:-240px -144px; }#home .Hecarim { background-position:-288px -144px; }#home .Heimerdinger { background-position:-336px -144px; }#home .Illaoi { background-position:-384px -144px; }#home .Irelia { background-position:-432px -144px; }#home .Ivern { background-position:-0px -192px; }#home .Janna { background-position:-48px -192px; }#home .JarvanIV { background-position:-96px -192px; }#home .Jax { background-position:-144px -192px; }#home .Jayce { background-position:-192px -192px; }#home .Jhin { background-position:-240px -192px; }#home .Jinx { background-position:-288px -192px; }#home .Kalista { background-position:-336px -192px; }#home .Karma { background-position:-384px -192px; }#home .Karthus { background-position:-432px -192px; }#home .Kassadin { background-position:-0px -240px; }#home .Katarina { background-position:-48px -240px; }#home .Kayle { background-position:-96px -240px; }#home .Kennen { background-position:-144px -240px; }#home .Khazix { background-position:-192px -240px; }#home .Kindred { background-position:-240px -240px; }#home .Kled { background-position:-288px -240px; }#home .KogMaw { background-position:-336px -240px; }#home .Leblanc { background-position:-384px -240px; }#home .LeeSin { background-position:-432px -240px; }#home .Leona { background-position:-0px -288px; }#home .Lissandra { background-position:-48px -288px; }#home .Lucian { background-position:-96px -288px; }#home .Lulu { background-position:-144px -288px; }#home .Lux { background-position:-192px -288px; }#home .Malphite { background-position:-240px -288px; }#home .Malzahar { background-position:-288px -288px; }#home .Maokai { background-position:-336px -288px; }#home .MasterYi { background-position:-384px -288px; }#home .MissFortune { background-position:-432px -288px; }#home .MonkeyKing { background-position:-0px -336px; }#home .Mordekaiser { background-position:-48px -336px; }#home .Morgana { background-position:-96px -336px; }#home .Nami { background-position:-144px -336px; }#home .Nasus { background-position:-192px -336px; }#home .Nautilus { background-position:-240px -336px; }#home .Nidalee { background-position:-288px -336px; }#home .Nocturne { background-position:-336px -336px; }#home .Nunu { background-position:-384px -336px; }#home .Olaf { background-position:-432px -336px; }#home .Orianna { background-position:-0px -384px; }#home .Pantheon { background-position:-48px -384px; }#home .Poppy { background-position:-96px -384px; }#home .Quinn { background-position:-144px -384px; }#home .Rakan { background-position:-192px -384px; }#home .Rammus { background-position:-240px -384px; }#home .RekSai { background-position:-288px -384px; }#home .Renekton { background-position:-336px -384px; }#home .Rengar { background-position:-384px -384px; }#home .Riven { background-position:-432px -384px; }#home .Rumble { background-position:-0px -432px; }#home .Ryze { background-position:-48px -432px; }#home .Sejuani { background-position:-96px -432px; }#home .Shaco { background-position:-144px -432px; }#home .Shen { background-position:-192px -432px; }#home .Shyvana { background-position:-240px -432px; }#home .Singed { background-position:-288px -432px; }#home .Sion { background-position:-336px -432px; }#home .Sivir { background-position:-384px -432px; }#home .Skarner { background-position:-432px -432px; }#home .Sona { background-position:-0px -480px; }#home .Soraka { background-position:-48px -480px; }#home .Swain { background-position:-96px -480px; }#home .Syndra { background-position:-144px -480px; }#home .TahmKench { background-position:-192px -480px; }#home .Taliyah { background-position:-240px -480px; }#home .Talon { background-position:-288px -480px; }#home .Taric { background-position:-336px -480px; }#home .Teemo { background-position:-384px -480px; }#home .Thresh { background-position:-432px -480px; }#home .Tristana { background-position:-0px -528px; }#home .Trundle { background-position:-48px -528px; }#home .Tryndamere { background-position:-96px -528px; }#home .TwistedFate { background-position:-144px -528px; }#home .Twitch { background-position:-192px -528px; }#home .Udyr { background-position:-240px -528px; }#home .Urgot { background-position:-288px -528px; }#home .Varus { background-position:-336px -528px; }#home .Vayne { background-position:-384px -528px; }#home .Veigar { background-position:-432px -528px; }#home .Velkoz { background-position:-0px -576px; }#home .Vi { background-position:-48px -576px; }#home .Viktor { background-position:-96px -576px; }#home .Vladimir { background-position:-144px -576px; }#home .Volibear { background-position:-192px -576px; }#home .Warwick { background-position:-240px -576px; }#home .Xayah { background-position:-288px -576px; }#home .Xerath { background-position:-336px -576px; }#home .XinZhao { background-position:-384px -576px; }#home .Yasuo { background-position:-432px -576px; }#home .Yorick { background-position:-0px -624px; }#home .Zac { background-position:-48px -624px; }#home .Zed { background-position:-96px -624px; }#home .Ziggs { background-position:-144px -624px; }#home .Zilean { background-position:-192px -624px; }#home .Zyra { background-position:-240px -624px; }.Aatrox { background-position:-0px -0px; }.Ahri { background-position:-36px -0px; }.Akali { background-position:-72px -0px; }.Alistar { background-position:-108px -0px; }.Amumu { background-position:-144px -0px; }.Anivia { background-position:-180px -0px; }.Annie { background-position:-216px -0px; }.Ashe { background-position:-252px -0px; }.AurelionSol { background-position:-288px -0px; }.Azir { background-position:-324px -0px; }.Bard { background-position:-0px -36px; }.Blitzcrank { background-position:-36px -36px; }.Brand { background-position:-72px -36px; }.Braum { background-position:-108px -36px; }.Caitlyn { background-position:-144px -36px; }.Camille { background-position:-180px -36px; }.Cassiopeia { background-position:-216px -36px; }.Chogath { background-position:-252px -36px; }.Corki { background-position:-288px -36px; }.Darius { background-position:-324px -36px; }.Diana { background-position:-0px -72px; }.Draven { background-position:-36px -72px; }.DrMundo { background-position:-72px -72px; }.Ekko { background-position:-108px -72px; }.Elise { background-position:-144px -72px; }.Evelynn { background-position:-180px -72px; }.Ezreal { background-position:-216px -72px; }.Fiddlesticks { background-position:-252px -72px; }.Fiora { background-position:-288px -72px; }.Fizz { background-position:-324px -72px; }.Galio { background-position:-0px -108px; }.Gangplank { background-position:-36px -108px; }.Garen { background-position:-72px -108px; }.Gnar { background-position:-108px -108px; }.Gragas { background-position:-144px -108px; }.Graves { background-position:-180px -108px; }.Hecarim { background-position:-216px -108px; }.Heimerdinger { background-position:-252px -108px; }.Illaoi { background-position:-288px -108px; }.Irelia { background-position:-324px -108px; }.Ivern { background-position:-0px -144px; }.Janna { background-position:-36px -144px; }.JarvanIV { background-position:-72px -144px; }.Jax { background-position:-108px -144px; }.Jayce { background-position:-144px -144px; }.Jhin { background-position:-180px -144px; }.Jinx { background-position:-216px -144px; }.Kalista { background-position:-252px -144px; }.Karma { background-position:-288px -144px; }.Karthus { background-position:-324px -144px; }.Kassadin { background-position:-0px -180px; }.Katarina { background-position:-36px -180px; }.Kayle { background-position:-72px -180px; }.Kennen { background-position:-108px -180px; }.Khazix { background-position:-144px -180px; }.Kindred { background-position:-180px -180px; }.Kled { background-position:-216px -180px; }.KogMaw { background-position:-252px -180px; }.Leblanc { background-position:-288px -180px; }.LeeSin { background-position:-324px -180px; }.Leona { background-position:-0px -216px; }.Lissandra { background-position:-36px -216px; }.Lucian { background-position:-72px -216px; }.Lulu { background-position:-108px -216px; }.Lux { background-position:-144px -216px; }.Malphite { background-position:-180px -216px; }.Malzahar { background-position:-216px -216px; }.Maokai { background-position:-252px -216px; }.MasterYi { background-position:-288px -216px; }.MissFortune { background-position:-324px -216px; }.MonkeyKing { background-position:-0px -252px; }.Mordekaiser { background-position:-36px -252px; }.Morgana { background-position:-72px -252px; }.Nami { background-position:-108px -252px; }.Nasus { background-position:-144px -252px; }.Nautilus { background-position:-180px -252px; }.Nidalee { background-position:-216px -252px; }.Nocturne { background-position:-252px -252px; }.Nunu { background-position:-288px -252px; }.Olaf { background-position:-324px -252px; }.Orianna { background-position:-0px -288px; }.Pantheon { background-position:-36px -288px; }.Poppy { background-position:-72px -288px; }.Quinn { background-position:-108px -288px; }.Rakan { background-position:-144px -288px; }.Rammus { background-position:-180px -288px; }.RekSai { background-position:-216px -288px; }.Renekton { background-position:-252px -288px; }.Rengar { background-position:-288px -288px; }.Riven { background-position:-324px -288px; }.Rumble { background-position:-0px -324px; }.Ryze { background-position:-36px -324px; }.Sejuani { background-position:-72px -324px; }.Shaco { background-position:-108px -324px; }.Shen { background-position:-144px -324px; }.Shyvana { background-position:-180px -324px; }.Singed { background-position:-216px -324px; }.Sion { background-position:-252px -324px; }.Sivir { background-position:-288px -324px; }.Skarner { background-position:-324px -324px; }.Sona { background-position:-0px -360px; }.Soraka { background-position:-36px -360px; }.Swain { background-position:-72px -360px; }.Syndra { background-position:-108px -360px; }.TahmKench { background-position:-144px -360px; }.Taliyah { background-position:-180px -360px; }.Talon { background-position:-216px -360px; }.Taric { background-position:-252px -360px; }.Teemo { background-position:-288px -360px; }.Thresh { background-position:-324px -360px; }.Tristana { background-position:-0px -396px; }.Trundle { background-position:-36px -396px; }.Tryndamere { background-position:-72px -396px; }.TwistedFate { background-position:-108px -396px; }.Twitch { background-position:-144px -396px; }.Udyr { background-position:-180px -396px; }.Urgot { background-position:-216px -396px; }.Varus { background-position:-252px -396px; }.Vayne { background-position:-288px -396px; }.Veigar { background-position:-324px -396px; }.Velkoz { background-position:-0px -432px; }.Vi { background-position:-36px -432px; }.Viktor { background-position:-72px -432px; }.Vladimir { background-position:-108px -432px; }.Volibear { background-position:-144px -432px; }.Warwick { background-position:-180px -432px; }.Xayah { background-position:-216px -432px; }.Xerath { background-position:-252px -432px; }.XinZhao { background-position:-288px -432px; }.Yasuo { background-position:-324px -432px; }.Yorick { background-position:-0px -468px; }.Zac { background-position:-36px -468px; }.Zed { background-position:-72px -468px; }.Ziggs { background-position:-108px -468px; }.Zilean { background-position:-144px -468px; }.Zyra { background-position:-180px -468px; }.mastery-6111 { background-position:-0px -0px; }.mastery-6114 { background-position:-48px -0px; }.mastery-6121 { background-position:-96px -0px; }.mastery-6122 { background-position:-144px -0px; }.mastery-6123 { background-position:-192px -0px; }.mastery-6131 { background-position:-240px -0px; }.mastery-6134 { background-position:-288px -0px; }.mastery-6141 { background-position:-336px -0px; }.mastery-6142 { background-position:-384px -0px; }.mastery-6143 { background-position:-432px -0px; }.mastery-6151 { background-position:-0px -48px; }.mastery-6154 { background-position:-48px -48px; }.mastery-6161 { background-position:-96px -48px; }.mastery-6162 { background-position:-144px -48px; }.mastery-6164 { background-position:-192px -48px; }.mastery-6211 { background-position:-0px -144px; }.mastery-6212 { background-position:-48px -144px; }.mastery-6221 { background-position:-96px -144px; }.mastery-6222 { background-position:-192px -144px; }.mastery-6223 { background-position:-144px -144px; }.mastery-6231 { background-position:-240px -144px; }.mastery-6232 { background-position:-288px -144px; }.mastery-6241 { background-position:-336px -144px; }.mastery-6242 { background-position:-384px -144px; }.mastery-6243 { background-position:-432px -144px; }.mastery-6251 { background-position:-0px -192px; }.mastery-6252 { background-position:-48px -192px; }.mastery-6261 { background-position:-96px -192px; }.mastery-6262 { background-position:-144px -192px; }.mastery-6263 { background-position:-192px -192px; }.mastery-6311 { background-position:-240px -48px; }.mastery-6312 { background-position:-288px -48px; }.mastery-6321 { background-position:-336px -48px; }.mastery-6322 { background-position:-384px -48px; }.mastery-6323 { background-position:-432px -48px; }.mastery-6331 { background-position:-0px -96px; }.mastery-6332 { background-position:-48px -96px; }.mastery-6341 { background-position:-96px -96px; }.mastery-6342 { background-position:-144px -96px; }.mastery-6343 { background-position:-192px -96px; }.mastery-6351 { background-position:-240px -96px; }.mastery-6352 { background-position:-288px -96px; }.mastery-6361 { background-position:-336px -96px; }.mastery-6362 { background-position:-384px -96px; }.mastery-6363 { background-position:-432px -96px; } ================================================ FILE: public/dist/js/angular-bootstrap.js ================================================ /* * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ * Version: 0.12.1 - 2015-02-20 * License: MIT */ angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); angular.module('ui.bootstrap.transition', []) /** * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. * @param {DOMElement} element The DOMElement that will be animated. * @param {string|object|function} trigger The thing that will cause the transition to start: * - As a string, it represents the css class to be added to the element. * - As an object, it represents a hash of style attributes to be applied to the element. * - As a function, it represents a function to be called that will cause the transition to occur. * @return {Promise} A promise that is resolved when the transition finishes. */ .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { var $transition = function(element, trigger, options) { options = options || {}; var deferred = $q.defer(); var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; var transitionEndHandler = function(event) { $rootScope.$apply(function() { element.unbind(endEventName, transitionEndHandler); deferred.resolve(element); }); }; if (endEventName) { element.bind(endEventName, transitionEndHandler); } // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur $timeout(function() { if ( angular.isString(trigger) ) { element.addClass(trigger); } else if ( angular.isFunction(trigger) ) { trigger(element); } else if ( angular.isObject(trigger) ) { element.css(trigger); } //If browser does not support transitions, instantly resolve if ( !endEventName ) { deferred.resolve(element); } }); // Add our custom cancel function to the promise that is returned // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, // i.e. it will therefore never raise a transitionEnd event for that transition deferred.promise.cancel = function() { if ( endEventName ) { element.unbind(endEventName, transitionEndHandler); } deferred.reject('Transition cancelled'); }; return deferred.promise; }; // Work out the name of the transitionEnd event var transElement = document.createElement('trans'); var transitionEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'transition': 'transitionend' }; var animationEndEventNames = { 'WebkitTransition': 'webkitAnimationEnd', 'MozTransition': 'animationend', 'OTransition': 'oAnimationEnd', 'transition': 'animationend' }; function findEndEventName(endEventNames) { for (var name in endEventNames){ if (transElement.style[name] !== undefined) { return endEventNames[name]; } } } $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); $transition.animationEndEventName = findEndEventName(animationEndEventNames); return $transition; }]); angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) .directive('collapse', ['$transition', function ($transition) { return { link: function (scope, element, attrs) { var initialAnimSkip = true; var currentTransition; function doTransition(change) { var newTransition = $transition(element, change); if (currentTransition) { currentTransition.cancel(); } currentTransition = newTransition; newTransition.then(newTransitionDone, newTransitionDone); return newTransition; function newTransitionDone() { // Make sure it's this transition, otherwise, leave it alone. if (currentTransition === newTransition) { currentTransition = undefined; } } } function expand() { if (initialAnimSkip) { initialAnimSkip = false; expandDone(); } else { element.removeClass('collapse').addClass('collapsing'); doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); } } function expandDone() { element.removeClass('collapsing'); element.addClass('collapse in'); element.css({height: 'auto'}); } function collapse() { if (initialAnimSkip) { initialAnimSkip = false; collapseDone(); element.css({height: 0}); } else { // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value element.css({ height: element[0].scrollHeight + 'px' }); //trigger reflow so a browser realizes that height was updated from auto to a specific value var x = element[0].offsetWidth; element.removeClass('collapse in').addClass('collapsing'); doTransition({ height: 0 }).then(collapseDone); } } function collapseDone() { element.removeClass('collapsing'); element.addClass('collapse'); } scope.$watch(attrs.collapse, function (shouldCollapse) { if (shouldCollapse) { collapse(); } else { expand(); } }); } }; }]); angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) .constant('accordionConfig', { closeOthers: true }) .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; if ( closeOthers ) { angular.forEach(this.groups, function (group) { if ( group !== openGroup ) { group.isOpen = false; } }); } }; // This is called from the accordion-group directive to add itself to the accordion this.addGroup = function(groupScope) { var that = this; this.groups.push(groupScope); groupScope.$on('$destroy', function (event) { that.removeGroup(groupScope); }); }; // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); if ( index !== -1 ) { this.groups.splice(index, 1); } }; }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. .directive('accordion', function () { return { restrict:'EA', controller:'AccordionController', transclude: true, replace: false, templateUrl: 'template/accordion/accordion.html' }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion .directive('accordionGroup', function() { return { require:'^accordion', // We need this directive to be inside an accordion restrict:'EA', transclude:true, // It transcludes the contents of the directive into the template replace: true, // The element containing the directive will be replaced with the template templateUrl:'template/accordion/accordion-group.html', scope: { heading: '@', // Interpolate the heading attribute onto this scope isOpen: '=?', isDisabled: '=?' }, controller: function() { this.setHeading = function(element) { this.heading = element; }; }, link: function(scope, element, attrs, accordionCtrl) { accordionCtrl.addGroup(scope); scope.$watch('isOpen', function(value) { if ( value ) { accordionCtrl.closeOthers(scope); } }); scope.toggleOpen = function() { if ( !scope.isDisabled ) { scope.isOpen = !scope.isOpen; } }; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML // // Heading containing HTML - // .directive('accordionHeading', function() { return { restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, require: '^accordionGroup', link: function(scope, element, attr, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] accordionGroupCtrl.setHeading(transclude(scope, function() {})); } }; }) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element //
    // // ... //
    .directive('accordionTransclude', function() { return { require: '^accordionGroup', link: function(scope, element, attr, controller) { scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { if ( heading ) { element.html(''); element.append(heading); } }); } }; }); angular.module('ui.bootstrap.alert', []) .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { $scope.closeable = 'close' in $attrs; this.close = $scope.close; }]) .directive('alert', function () { return { restrict:'EA', controller:'AlertController', templateUrl:'template/alert/alert.html', transclude:true, replace:true, scope: { type: '@', close: '&' } }; }) .directive('dismissOnTimeout', ['$timeout', function($timeout) { return { require: 'alert', link: function(scope, element, attrs, alertCtrl) { $timeout(function(){ alertCtrl.close(); }, parseInt(attrs.dismissOnTimeout, 10)); } }; }]); angular.module('ui.bootstrap.bindHtml', []) .directive('bindHtmlUnsafe', function () { return function (scope, element, attr) { element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { element.html(value || ''); }); }; }); angular.module('ui.bootstrap.buttons', []) .constant('buttonConfig', { activeClass: 'active', toggleEvent: 'click' }) .controller('ButtonsController', ['buttonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) .directive('btnRadio', function () { return { require: ['btnRadio', 'ngModel'], controller: 'ButtonsController', link: function (scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; //model -> UI ngModelCtrl.$render = function () { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); }; //ui->model element.bind(buttonsCtrl.toggleEvent, function () { var isActive = element.hasClass(buttonsCtrl.activeClass); if (!isActive || angular.isDefined(attrs.uncheckable)) { scope.$apply(function () { ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); ngModelCtrl.$render(); }); } }); } }; }) .directive('btnCheckbox', function () { return { require: ['btnCheckbox', 'ngModel'], controller: 'ButtonsController', link: function (scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } function getFalseValue() { return getCheckboxValue(attrs.btnCheckboxFalse, false); } function getCheckboxValue(attributeValue, defaultValue) { var val = scope.$eval(attributeValue); return angular.isDefined(val) ? val : defaultValue; } //model -> UI ngModelCtrl.$render = function () { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model element.bind(buttonsCtrl.toggleEvent, function () { scope.$apply(function () { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); }); } }; }); /** * @ngdoc overview * @name ui.bootstrap.carousel * * @description * AngularJS version of an image carousel. * */ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) .controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) { var self = this, slides = self.slides = $scope.slides = [], currentIndex = -1, currentInterval, isPlaying; self.currentSlide = null; var destroyed = false; /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { var nextIndex = slides.indexOf(nextSlide); //Decide direction if it's not given if (direction === undefined) { direction = nextIndex > currentIndex ? 'next' : 'prev'; } if (nextSlide && nextSlide !== self.currentSlide) { if ($scope.$currentTransition) { $scope.$currentTransition.cancel(); //Timeout so ng-class in template has time to fix classes for finished slide $timeout(goNext); } else { goNext(); } } function goNext() { // Scope has been destroyed, stop here. if (destroyed) { return; } //If we have a slide to transition from and we have a transition type and we're allowed, go if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime nextSlide.$element.addClass(direction); var reflow = nextSlide.$element[0].offsetWidth; //force reflow //Set all other slides to stop doing their stuff for the new transition angular.forEach(slides, function(slide) { angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); }); angular.extend(nextSlide, {direction: direction, active: true, entering: true}); angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); $scope.$currentTransition = $transition(nextSlide.$element, {}); //We have to create new pointers inside a closure since next & current will change (function(next,current) { $scope.$currentTransition.then( function(){ transitionDone(next, current); }, function(){ transitionDone(next, current); } ); }(nextSlide, self.currentSlide)); } else { transitionDone(nextSlide, self.currentSlide); } self.currentSlide = nextSlide; currentIndex = nextIndex; //every time you change slides, reset the timer restartTimer(); } function transitionDone(next, current) { angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); $scope.$currentTransition = null; } }; $scope.$on('$destroy', function () { destroyed = true; }); /* Allow outside people to call indexOf on slides array */ self.indexOfSlide = function(slide) { return slides.indexOf(slide); }; $scope.next = function() { var newIndex = (currentIndex + 1) % slides.length; //Prevent this user-triggered transition from occurring if there is already one in progress if (!$scope.$currentTransition) { return self.select(slides[newIndex], 'next'); } }; $scope.prev = function() { var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; //Prevent this user-triggered transition from occurring if there is already one in progress if (!$scope.$currentTransition) { return self.select(slides[newIndex], 'prev'); } }; $scope.isActive = function(slide) { return self.currentSlide === slide; }; $scope.$watch('interval', restartTimer); $scope.$on('$destroy', resetTimer); function restartTimer() { resetTimer(); var interval = +$scope.interval; if (!isNaN(interval) && interval > 0) { currentInterval = $interval(timerFn, interval); } } function resetTimer() { if (currentInterval) { $interval.cancel(currentInterval); currentInterval = null; } } function timerFn() { var interval = +$scope.interval; if (isPlaying && !isNaN(interval) && interval > 0) { $scope.next(); } else { $scope.pause(); } } $scope.play = function() { if (!isPlaying) { isPlaying = true; restartTimer(); } }; $scope.pause = function() { if (!$scope.noPause) { isPlaying = false; resetTimer(); } }; self.addSlide = function(slide, element) { slide.$element = element; slides.push(slide); //if this is the first slide or the slide is set to active, select it if(slides.length === 1 || slide.active) { self.select(slides[slides.length-1]); if (slides.length == 1) { $scope.play(); } } else { slide.active = false; } }; self.removeSlide = function(slide) { //get the index of the slide inside the carousel var index = slides.indexOf(slide); slides.splice(index, 1); if (slides.length > 0 && slide.active) { if (index >= slides.length) { self.select(slides[index-1]); } else { self.select(slides[index]); } } else if (currentIndex > index) { currentIndex--; } }; }]) /** * @ngdoc directive * @name ui.bootstrap.carousel.directive:carousel * @restrict EA * * @description * Carousel is the outer container for a set of image 'slides' to showcase. * * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. * @param {boolean=} noTransition Whether to disable transitions on the carousel. * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). * * @example .carousel-indicators { top: auto; bottom: 15px; } */ .directive('carousel', [function() { return { restrict: 'EA', transclude: true, replace: true, controller: 'CarouselController', require: 'carousel', templateUrl: 'template/carousel/carousel.html', scope: { interval: '=', noTransition: '=', noPause: '=' } }; }]) /** * @ngdoc directive * @name ui.bootstrap.carousel.directive:slide * @restrict EA * * @description * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. * * @param {boolean=} active Model binding, whether or not this slide is currently active. * * @example
    Interval, in milliseconds:
    Enter a negative number to stop the interval.
    function CarouselDemoCtrl($scope) { $scope.myInterval = 5000; } .carousel-indicators { top: auto; bottom: 15px; }
    */ .directive('slide', function() { return { require: '^carousel', restrict: 'EA', transclude: true, replace: true, templateUrl: 'template/carousel/slide.html', scope: { active: '=?' }, link: function (scope, element, attrs, carouselCtrl) { carouselCtrl.addSlide(scope, element); //when the scope is destroyed then remove the slide from the current slides array scope.$on('$destroy', function() { carouselCtrl.removeSlide(scope); }); scope.$watch('active', function(active) { if (active) { carouselCtrl.select(scope); } }); } }; }); angular.module('ui.bootstrap.dateparser', []) .service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { this.parsers = {}; var formatCodeToRegex = { 'yyyy': { regex: '\\d{4}', apply: function(value) { this.year = +value; } }, 'yy': { regex: '\\d{2}', apply: function(value) { this.year = +value + 2000; } }, 'y': { regex: '\\d{1,4}', apply: function(value) { this.year = +value; } }, 'MMMM': { regex: $locale.DATETIME_FORMATS.MONTH.join('|'), apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } }, 'MMM': { regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } }, 'MM': { regex: '0[1-9]|1[0-2]', apply: function(value) { this.month = value - 1; } }, 'M': { regex: '[1-9]|1[0-2]', apply: function(value) { this.month = value - 1; } }, 'dd': { regex: '[0-2][0-9]{1}|3[0-1]{1}', apply: function(value) { this.date = +value; } }, 'd': { regex: '[1-2]?[0-9]{1}|3[0-1]{1}', apply: function(value) { this.date = +value; } }, 'EEEE': { regex: $locale.DATETIME_FORMATS.DAY.join('|') }, 'EEE': { regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') } }; function createParser(format) { var map = [], regex = format.split(''); angular.forEach(formatCodeToRegex, function(data, code) { var index = format.indexOf(code); if (index > -1) { format = format.split(''); regex[index] = '(' + data.regex + ')'; format[index] = '$'; // Custom symbol to define consumed part of format for (var i = index + 1, n = index + code.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); map.push({ index: index, apply: data.apply }); } }); return { regex: new RegExp('^' + regex.join('') + '$'), map: orderByFilter(map, 'index') }; } this.parse = function(input, format) { if ( !angular.isString(input) || !format ) { return input; } format = $locale.DATETIME_FORMATS[format] || format; if ( !this.parsers[format] ) { this.parsers[format] = createParser(format); } var parser = this.parsers[format], regex = parser.regex, map = parser.map, results = input.match(regex); if ( results && results.length ) { var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; for( var i = 1, n = results.length; i < n; i++ ) { var mapper = map[i-1]; if ( mapper.apply ) { mapper.apply.call(fields, results[i]); } } if ( isValid(fields.year, fields.month, fields.date) ) { dt = new Date( fields.year, fields.month, fields.date, fields.hours); } return dt; } }; // Check if date is valid for specific month (and year for February). // Month: 0 = Jan, 1 = Feb, etc function isValid(year, month, date) { if ( month === 1 && date > 28) { return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); } if ( month === 3 || month === 5 || month === 8 || month === 10) { return date < 31; } return true; } }]); angular.module('ui.bootstrap.position', []) /** * A set of utility methods that can be use to retrieve position of DOM elements. * It is meant to be used where we need to absolute-position DOM elements in * relation to other, existing elements (this is the case for tooltips, popovers, * typeahead suggestions etc.). */ .factory('$position', ['$document', '$window', function ($document, $window) { function getStyle(el, cssprop) { if (el.currentStyle) { //IE return el.currentStyle[cssprop]; } else if ($window.getComputedStyle) { return $window.getComputedStyle(el)[cssprop]; } // finally try and get inline style return el.style[cssprop]; } /** * Checks if a given element is statically positioned * @param element - raw DOM element */ function isStaticPositioned(element) { return (getStyle(element, 'position') || 'static' ) === 'static'; } /** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ var parentOffsetEl = function (element) { var docDomEl = $document[0]; var offsetParent = element.offsetParent || docDomEl; while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { offsetParent = offsetParent.offsetParent; } return offsetParent || docDomEl; }; return { /** * Provides read-only equivalent of jQuery's position function: * http://api.jquery.com/position/ */ position: function (element) { var elBCR = this.offset(element); var offsetParentBCR = { top: 0, left: 0 }; var offsetParentEl = parentOffsetEl(element[0]); if (offsetParentEl != $document[0]) { offsetParentBCR = this.offset(angular.element(offsetParentEl)); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; } var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left }; }, /** * Provides read-only equivalent of jQuery's offset function: * http://api.jquery.com/offset/ */ offset: function (element) { var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) }; }, /** * Provides coordinates for the targetEl in relation to hostEl */ positionElements: function (hostEl, targetEl, positionStr, appendToBody) { var positionStrParts = positionStr.split('-'); var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; var hostElPos, targetElWidth, targetElHeight, targetElPos; hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); targetElWidth = targetEl.prop('offsetWidth'); targetElHeight = targetEl.prop('offsetHeight'); var shiftWidth = { center: function () { return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; }, left: function () { return hostElPos.left; }, right: function () { return hostElPos.left + hostElPos.width; } }; var shiftHeight = { center: function () { return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; }, top: function () { return hostElPos.top; }, bottom: function () { return hostElPos.top + hostElPos.height; } }; switch (pos0) { case 'right': targetElPos = { top: shiftHeight[pos1](), left: shiftWidth[pos0]() }; break; case 'left': targetElPos = { top: shiftHeight[pos1](), left: hostElPos.left - targetElWidth }; break; case 'bottom': targetElPos = { top: shiftHeight[pos0](), left: shiftWidth[pos1]() }; break; default: targetElPos = { top: hostElPos.top - targetElHeight, left: shiftWidth[pos1]() }; break; } return targetElPos; } }; }]); angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) .constant('datepickerConfig', { formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', datepickerMode: 'day', minMode: 'day', maxMode: 'year', showWeeks: true, startingDay: 0, yearRange: 20, minDate: null, maxDate: null }) .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; // Modes chain this.modes = ['day', 'month', 'year']; // Configuration attributes angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; }); // Watchable date attributes angular.forEach(['minDate', 'maxDate'], function( key ) { if ( $attrs[key] ) { $scope.$parent.$watch($parse($attrs[key]), function(value) { self[key] = value ? new Date(value) : null; self.refreshView(); }); } else { self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; } }); $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); $scope.isActive = function(dateObject) { if (self.compare(dateObject.date, self.activeDate) === 0) { $scope.activeDateId = dateObject.uid; return true; } return false; }; this.init = function( ngModelCtrl_ ) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = function() { self.render(); }; }; this.render = function() { if ( ngModelCtrl.$modelValue ) { var date = new Date( ngModelCtrl.$modelValue ), isValid = !isNaN(date); if ( isValid ) { this.activeDate = date; } else { $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { if ( this.element ) { this._refreshView(); var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); } }; this.createDateObject = function(date, format) { var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; return { date: date, label: dateFilter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), current: this.compare(date, new Date()) === 0 }; }; this.isDisabled = function( date ) { return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); }; // Split array into smaller arrays this.split = function(arr, size) { var arrays = []; while (arr.length > 0) { arrays.push(arr.splice(0, size)); } return arrays; }; $scope.select = function( date ) { if ( $scope.datepickerMode === self.minMode ) { var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); ngModelCtrl.$setViewValue( dt ); ngModelCtrl.$render(); } else { self.activeDate = date; $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; } }; $scope.move = function( direction ) { var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), month = self.activeDate.getMonth() + direction * (self.step.months || 0); self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; $scope.toggleMode = function( direction ) { direction = direction || 1; if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { return; } $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; }; // Key event mapper $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; var focusElement = function() { $timeout(function() { self.element[0].focus(); }, 0 , false); }; // Listen for focus requests from popup directive $scope.$on('datepicker.focus', focusElement); $scope.keydown = function( evt ) { var key = $scope.keys[evt.which]; if ( !key || evt.shiftKey || evt.altKey ) { return; } evt.preventDefault(); evt.stopPropagation(); if (key === 'enter' || key === 'space') { if ( self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); focusElement(); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; }]) .directive( 'datepicker', function () { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/datepicker.html', scope: { datepickerMode: '=?', dateDisabled: '&' }, require: ['datepicker', '?^ngModel'], controller: 'DatepickerController', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if ( ngModelCtrl ) { datepickerCtrl.init( ngModelCtrl ); } } }; }) .directive('daypicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/day.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { scope.showWeeks = ctrl.showWeeks; ctrl.step = { months: 1 }; ctrl.element = element; var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; function getDaysInMonth( year, month ) { return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; } function getDates(startDate, n) { var dates = new Array(n), current = new Date(startDate), i = 0; current.setHours(12); // Prevent repeated dates because of timezone bug while ( i < n ) { dates[i++] = new Date(current); current.setDate( current.getDate() + 1 ); } return dates; } ctrl._refreshView = function() { var year = ctrl.activeDate.getFullYear(), month = ctrl.activeDate.getMonth(), firstDayOfMonth = new Date(year, month, 1), difference = ctrl.startingDay - firstDayOfMonth.getDay(), numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, firstDate = new Date(firstDayOfMonth); if ( numDisplayedFromPreviousMonth > 0 ) { firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); } // 42 is the number of days on a six-month calendar var days = getDates(firstDate, 42); for (var i = 0; i < 42; i ++) { days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { secondary: days[i].getMonth() !== month, uid: scope.uniqueId + '-' + i }); } scope.labels = new Array(7); for (var j = 0; j < 7; j++) { scope.labels[j] = { abbr: dateFilter(days[j].date, ctrl.formatDayHeader), full: dateFilter(days[j].date, 'EEEE') }; } scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); scope.rows = ctrl.split(days, 7); if ( scope.showWeeks ) { scope.weekNumbers = []; var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), numWeeks = scope.rows.length; while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} } }; ctrl.compare = function(date1, date2) { return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); }; function getISO8601WeekNumber(date) { var checkDate = new Date(date); checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday var time = checkDate.getTime(); checkDate.setMonth(0); // Compare with Jan 1 checkDate.setDate(1); return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; } ctrl.handleKeyDown = function( key, evt ) { var date = ctrl.activeDate.getDate(); if (key === 'left') { date = date - 1; // up } else if (key === 'up') { date = date - 7; // down } else if (key === 'right') { date = date + 1; // down } else if (key === 'down') { date = date + 7; } else if (key === 'pageup' || key === 'pagedown') { var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); ctrl.activeDate.setMonth(month, 1); date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); } else if (key === 'home') { date = 1; } else if (key === 'end') { date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); } ctrl.activeDate.setDate(date); }; ctrl.refreshView(); } }; }]) .directive('monthpicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/month.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { ctrl.step = { years: 1 }; ctrl.element = element; ctrl._refreshView = function() { var months = new Array(12), year = ctrl.activeDate.getFullYear(); for ( var i = 0; i < 12; i++ ) { months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { uid: scope.uniqueId + '-' + i }); } scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); scope.rows = ctrl.split(months, 3); }; ctrl.compare = function(date1, date2) { return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); }; ctrl.handleKeyDown = function( key, evt ) { var date = ctrl.activeDate.getMonth(); if (key === 'left') { date = date - 1; // up } else if (key === 'up') { date = date - 3; // down } else if (key === 'right') { date = date + 1; // down } else if (key === 'down') { date = date + 3; } else if (key === 'pageup' || key === 'pagedown') { var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); ctrl.activeDate.setFullYear(year); } else if (key === 'home') { date = 0; } else if (key === 'end') { date = 11; } ctrl.activeDate.setMonth(date); }; ctrl.refreshView(); } }; }]) .directive('yearpicker', ['dateFilter', function (dateFilter) { return { restrict: 'EA', replace: true, templateUrl: 'template/datepicker/year.html', require: '^datepicker', link: function(scope, element, attrs, ctrl) { var range = ctrl.yearRange; ctrl.step = { years: range }; ctrl.element = element; function getStartingYear( year ) { return parseInt((year - 1) / range, 10) * range + 1; } ctrl._refreshView = function() { var years = new Array(range); for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { uid: scope.uniqueId + '-' + i }); } scope.title = [years[0].label, years[range - 1].label].join(' - '); scope.rows = ctrl.split(years, 5); }; ctrl.compare = function(date1, date2) { return date1.getFullYear() - date2.getFullYear(); }; ctrl.handleKeyDown = function( key, evt ) { var date = ctrl.activeDate.getFullYear(); if (key === 'left') { date = date - 1; // up } else if (key === 'up') { date = date - 5; // down } else if (key === 'right') { date = date + 1; // down } else if (key === 'down') { date = date + 5; } else if (key === 'pageup' || key === 'pagedown') { date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; } else if (key === 'home') { date = getStartingYear( ctrl.activeDate.getFullYear() ); } else if (key === 'end') { date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; } ctrl.activeDate.setFullYear(date); }; ctrl.refreshView(); } }; }]) .constant('datepickerPopupConfig', { datepickerPopup: 'yyyy-MM-dd', currentText: 'Today', clearText: 'Clear', closeText: 'Done', closeOnDateSelection: true, appendToBody: false, showButtonBar: true }) .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { return { restrict: 'EA', require: 'ngModel', scope: { isOpen: '=?', currentText: '@', clearText: '@', closeText: '@', dateDisabled: '&' }, link: function(scope, element, attrs, ngModel) { var dateFormat, closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; scope.getText = function( key ) { return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; }; attrs.$observe('datepickerPopup', function(value) { dateFormat = value || datepickerPopupConfig.datepickerPopup; ngModel.$render(); }); // popup element used to display calendar var popupEl = angular.element('
    '); popupEl.attr({ 'ng-model': 'date', 'ng-change': 'dateSelection()' }); function cameltoDash( string ){ return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); } // datepicker element var datepickerEl = angular.element(popupEl.children()[0]); if ( attrs.datepickerOptions ) { angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { datepickerEl.attr( cameltoDash(option), value ); }); } scope.watchData = {}; angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { if ( attrs[key] ) { var getAttribute = $parse(attrs[key]); scope.$parent.$watch(getAttribute, function(value){ scope.watchData[key] = value; }); datepickerEl.attr(cameltoDash(key), 'watchData.' + key); // Propagate changes from datepicker to outside if ( key === 'datepickerMode' ) { var setAttribute = getAttribute.assign; scope.$watch('watchData.' + key, function(value, oldvalue) { if ( value !== oldvalue ) { setAttribute(scope.$parent, value); } }); } } }); if (attrs.dateDisabled) { datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); } function parseDate(viewValue) { if (!viewValue) { ngModel.$setValidity('date', true); return null; } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { ngModel.$setValidity('date', true); return viewValue; } else if (angular.isString(viewValue)) { var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); if (isNaN(date)) { ngModel.$setValidity('date', false); return undefined; } else { ngModel.$setValidity('date', true); return date; } } else { ngModel.$setValidity('date', false); return undefined; } } ngModel.$parsers.unshift(parseDate); // Inner change scope.dateSelection = function(dt) { if (angular.isDefined(dt)) { scope.date = dt; } ngModel.$setViewValue(scope.date); ngModel.$render(); if ( closeOnDateSelection ) { scope.isOpen = false; element[0].focus(); } }; element.bind('input change keyup', function() { scope.$apply(function() { scope.date = ngModel.$modelValue; }); }); // Outter change ngModel.$render = function() { var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; element.val(date); scope.date = parseDate( ngModel.$modelValue ); }; var documentClickBind = function(event) { if (scope.isOpen && event.target !== element[0]) { scope.$apply(function() { scope.isOpen = false; }); } }; var keydown = function(evt, noApply) { scope.keydown(evt); }; element.bind('keydown', keydown); scope.keydown = function(evt) { if (evt.which === 27) { evt.preventDefault(); evt.stopPropagation(); scope.close(); } else if (evt.which === 40 && !scope.isOpen) { scope.isOpen = true; } }; scope.$watch('isOpen', function(value) { if (value) { scope.$broadcast('datepicker.focus'); scope.position = appendToBody ? $position.offset(element) : $position.position(element); scope.position.top = scope.position.top + element.prop('offsetHeight'); $document.bind('click', documentClickBind); } else { $document.unbind('click', documentClickBind); } }); scope.select = function( date ) { if (date === 'today') { var today = new Date(); if (angular.isDate(ngModel.$modelValue)) { date = new Date(ngModel.$modelValue); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { date = new Date(today.setHours(0, 0, 0, 0)); } } scope.dateSelection( date ); }; scope.close = function() { scope.isOpen = false; element[0].focus(); }; var $popup = $compile(popupEl)(scope); // Prevent jQuery cache memory leak (template is now redundant after linking) popupEl.remove(); if ( appendToBody ) { $document.find('body').append($popup); } else { element.after($popup); } scope.$on('$destroy', function() { $popup.remove(); element.unbind('keydown', keydown); $document.unbind('click', documentClickBind); }); } }; }]) .directive('datepickerPopupWrap', function() { return { restrict:'EA', replace: true, transclude: true, templateUrl: 'template/datepicker/popup.html', link:function (scope, element, attrs) { element.bind('click', function(event) { event.preventDefault(); event.stopPropagation(); }); } }; }); angular.module('ui.bootstrap.dropdown', []) .constant('dropdownConfig', { openClass: 'open' }) .service('dropdownService', ['$document', function($document) { var openScope = null; this.open = function( dropdownScope ) { if ( !openScope ) { $document.bind('click', closeDropdown); $document.bind('keydown', escapeKeyBind); } if ( openScope && openScope !== dropdownScope ) { openScope.isOpen = false; } openScope = dropdownScope; }; this.close = function( dropdownScope ) { if ( openScope === dropdownScope ) { openScope = null; $document.unbind('click', closeDropdown); $document.unbind('keydown', escapeKeyBind); } }; var closeDropdown = function( evt ) { // This method may still be called during the same mouse event that // unbound this event handler. So check openScope before proceeding. if (!openScope) { return; } var toggleElement = openScope.getToggleElement(); if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { return; } openScope.$apply(function() { openScope.isOpen = false; }); }; var escapeKeyBind = function( evt ) { if ( evt.which === 27 ) { openScope.focusToggleElement(); closeDropdown(); } }; }]) .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { var self = this, scope = $scope.$new(), // create a child scope so we are not polluting original one openClass = dropdownConfig.openClass, getIsOpen, setIsOpen = angular.noop, toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; this.init = function( element ) { self.$element = element; if ( $attrs.isOpen ) { getIsOpen = $parse($attrs.isOpen); setIsOpen = getIsOpen.assign; $scope.$watch(getIsOpen, function(value) { scope.isOpen = !!value; }); } }; this.toggle = function( open ) { return scope.isOpen = arguments.length ? !!open : !scope.isOpen; }; // Allow other directives to watch status this.isOpen = function() { return scope.isOpen; }; scope.getToggleElement = function() { return self.toggleElement; }; scope.focusToggleElement = function() { if ( self.toggleElement ) { self.toggleElement[0].focus(); } }; scope.$watch('isOpen', function( isOpen, wasOpen ) { $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); if ( isOpen ) { scope.focusToggleElement(); dropdownService.open( scope ); } else { dropdownService.close( scope ); } setIsOpen($scope, isOpen); if (angular.isDefined(isOpen) && isOpen !== wasOpen) { toggleInvoker($scope, { open: !!isOpen }); } }); $scope.$on('$locationChangeSuccess', function() { scope.isOpen = false; }); $scope.$on('$destroy', function() { scope.$destroy(); }); }]) .directive('dropdown', function() { return { controller: 'DropdownController', link: function(scope, element, attrs, dropdownCtrl) { dropdownCtrl.init( element ); } }; }) .directive('dropdownToggle', function() { return { require: '?^dropdown', link: function(scope, element, attrs, dropdownCtrl) { if ( !dropdownCtrl ) { return; } dropdownCtrl.toggleElement = element; var toggleDropdown = function(event) { event.preventDefault(); if ( !element.hasClass('disabled') && !attrs.disabled ) { scope.$apply(function() { dropdownCtrl.toggle(); }); } }; element.bind('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { element.attr('aria-expanded', !!isOpen); }); scope.$on('$destroy', function() { element.unbind('click', toggleDropdown); }); } }; }); angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ .factory('$$stackedMap', function () { return { createNew: function () { var stack = []; return { add: function (key, value) { stack.push({ key: key, value: value }); }, get: function (key) { for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { return stack[i]; } } }, keys: function() { var keys = []; for (var i = 0; i < stack.length; i++) { keys.push(stack[i].key); } return keys; }, top: function () { return stack[stack.length - 1]; }, remove: function (key) { var idx = -1; for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, removeTop: function () { return stack.splice(stack.length - 1, 1)[0]; }, length: function () { return stack.length; } }; } }; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('modalBackdrop', ['$timeout', function ($timeout) { return { restrict: 'EA', replace: true, templateUrl: 'template/modal/backdrop.html', link: function (scope, element, attrs) { scope.backdropClass = attrs.backdropClass || ''; scope.animate = false; //trigger CSS transitions $timeout(function () { scope.animate = true; }); } }; }]) .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { return { restrict: 'EA', scope: { index: '@', animate: '=' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'template/modal/window.html'; }, link: function (scope, element, attrs) { element.addClass(attrs.windowClass || ''); scope.size = attrs.size; $timeout(function () { // trigger CSS transitions scope.animate = true; /** * Auto-focusing of a freshly-opened modal element causes any child elements * with the autofocus attribute to lose focus. This is an issue on touch * based devices which will show and then hide the onscreen keyboard. * Attempts to refocus the autofocus element via JavaScript will not reopen * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus * the modal element if the modal does not contain an autofocus element. */ if (!element[0].querySelectorAll('[autofocus]').length) { element[0].focus(); } }); scope.close = function (evt) { var modal = $modalStack.getTop(); if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; } }; }]) .directive('modalTransclude', function () { return { link: function($scope, $element, $attrs, controller, $transclude) { $transclude($scope.$parent, function(clone) { $element.empty(); $element.append(clone); }); } }; }) .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); var $modalStack = {}; function backdropIndex() { var topBackdropIndex = -1; var opened = openedWindows.keys(); for (var i = 0; i < opened.length; i++) { if (openedWindows.get(opened[i]).value.backdrop) { topBackdropIndex = i; } } return topBackdropIndex; } $rootScope.$watch(backdropIndex, function(newBackdropIndex){ if (backdropScope) { backdropScope.index = newBackdropIndex; } }); function removeModalWindow(modalInstance) { var body = $document.find('body').eq(0); var modalWindow = openedWindows.get(modalInstance).value; //clean up the stack openedWindows.remove(modalInstance); //remove window DOM element removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { modalWindow.modalScope.$destroy(); body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); checkRemoveBackdrop(); }); } function checkRemoveBackdrop() { //remove backdrop if no longer needed if (backdropDomEl && backdropIndex() == -1) { var backdropScopeRef = backdropScope; removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { backdropScopeRef.$destroy(); backdropScopeRef = null; }); backdropDomEl = undefined; backdropScope = undefined; } } function removeAfterAnimate(domEl, scope, emulateTime, done) { // Closing animation scope.animate = false; var transitionEndEventName = $transition.transitionEndEventName; if (transitionEndEventName) { // transition out var timeout = $timeout(afterAnimating, emulateTime); domEl.bind(transitionEndEventName, function () { $timeout.cancel(timeout); afterAnimating(); scope.$apply(); }); } else { // Ensure this call is async $timeout(afterAnimating); } function afterAnimating() { if (afterAnimating.done) { return; } afterAnimating.done = true; domEl.remove(); if (done) { done(); } } } $document.bind('keydown', function (evt) { var modal; if (evt.which === 27) { modal = openedWindows.top(); if (modal && modal.value.keyboard) { evt.preventDefault(); $rootScope.$apply(function () { $modalStack.dismiss(modal.key, 'escape key press'); }); } } }); $modalStack.open = function (modalInstance, modal) { openedWindows.add(modalInstance, { deferred: modal.deferred, modalScope: modal.scope, backdrop: modal.backdrop, keyboard: modal.keyboard }); var body = $document.find('body').eq(0), currBackdropIndex = backdropIndex(); if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.index = currBackdropIndex; var angularBackgroundDomEl = angular.element('
    '); angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); body.append(backdropDomEl); } var angularDomEl = angular.element('
    '); angularDomEl.attr({ 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, 'size': modal.size, 'index': openedWindows.length() - 1, 'animate': 'animate' }).html(modal.content); var modalDomEl = $compile(angularDomEl)(modal.scope); openedWindows.top().value.modalDomEl = modalDomEl; body.append(modalDomEl); body.addClass(OPENED_MODAL_CLASS); }; $modalStack.close = function (modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance); } }; $modalStack.dismiss = function (modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.deferred.reject(reason); removeModalWindow(modalInstance); } }; $modalStack.dismissAll = function (reason) { var topModal = this.getTop(); while (topModal) { this.dismiss(topModal.key, reason); topModal = this.getTop(); } }; $modalStack.getTop = function () { return openedWindows.top(); }; return $modalStack; }]) .provider('$modal', function () { var $modalProvider = { options: { backdrop: true, //can be also false or 'static' keyboard: true }, $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, {cache: $templateCache}).then(function (result) { return result.data; }); } function getResolvePromises(resolves) { var promisesArr = []; angular.forEach(resolves, function (value) { if (angular.isFunction(value) || angular.isArray(value)) { promisesArr.push($q.when($injector.invoke(value))); } }); return promisesArr; } $modal.open = function (modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = { result: modalResultDeferred.promise, opened: modalOpenedDeferred.promise, close: function (result) { $modalStack.close(modalInstance, result); }, dismiss: function (reason) { $modalStack.dismiss(modalInstance, reason); } }; //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions); modalOptions.resolve = modalOptions.resolve || {}; //verify options if (!modalOptions.template && !modalOptions.templateUrl) { throw new Error('One of template or templateUrl options is required.'); } var templateAndResolvePromise = $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { var modalScope = (modalOptions.scope || $rootScope).$new(); modalScope.$close = modalInstance.close; modalScope.$dismiss = modalInstance.dismiss; var ctrlInstance, ctrlLocals = {}; var resolveIter = 1; //controllers if (modalOptions.controller) { ctrlLocals.$scope = modalScope; ctrlLocals.$modalInstance = modalInstance; angular.forEach(modalOptions.resolve, function (value, key) { ctrlLocals[key] = tplAndVars[resolveIter++]; }); ctrlInstance = $controller(modalOptions.controller, ctrlLocals); if (modalOptions.controllerAs) { modalScope[modalOptions.controllerAs] = ctrlInstance; } } $modalStack.open(modalInstance, { scope: modalScope, deferred: modalResultDeferred, content: tplAndVars[0], backdrop: modalOptions.backdrop, keyboard: modalOptions.keyboard, backdropClass: modalOptions.backdropClass, windowClass: modalOptions.windowClass, windowTemplateUrl: modalOptions.windowTemplateUrl, size: modalOptions.size }); }, function resolveError(reason) { modalResultDeferred.reject(reason); }); templateAndResolvePromise.then(function () { modalOpenedDeferred.resolve(true); }, function () { modalOpenedDeferred.reject(false); }); return modalInstance; }; return $modal; }] }; return $modalProvider; }); angular.module('ui.bootstrap.pagination', []) .controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { var self = this, ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; this.init = function(ngModelCtrl_, config) { ngModelCtrl = ngModelCtrl_; this.config = config; ngModelCtrl.$render = function() { self.render(); }; if ($attrs.itemsPerPage) { $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { self.itemsPerPage = parseInt(value, 10); $scope.totalPages = self.calculateTotalPages(); }); } else { this.itemsPerPage = config.itemsPerPage; } }; this.calculateTotalPages = function() { var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); return Math.max(totalPages || 0, 1); }; this.render = function() { $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; }; $scope.selectPage = function(page) { if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { ngModelCtrl.$setViewValue(page); ngModelCtrl.$render(); } }; $scope.getText = function( key ) { return $scope[key + 'Text'] || self.config[key + 'Text']; }; $scope.noPrevious = function() { return $scope.page === 1; }; $scope.noNext = function() { return $scope.page === $scope.totalPages; }; $scope.$watch('totalItems', function() { $scope.totalPages = self.calculateTotalPages(); }); $scope.$watch('totalPages', function(value) { setNumPages($scope.$parent, value); // Readonly variable if ( $scope.page > value ) { $scope.selectPage(value); } else { ngModelCtrl.$render(); } }); }]) .constant('paginationConfig', { itemsPerPage: 10, boundaryLinks: false, directionLinks: true, firstText: 'First', previousText: 'Previous', nextText: 'Next', lastText: 'Last', rotate: true }) .directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { return { restrict: 'EA', scope: { totalItems: '=', firstText: '@', previousText: '@', nextText: '@', lastText: '@' }, require: ['pagination', '?ngModel'], controller: 'PaginationController', templateUrl: 'template/pagination/pagination.html', replace: true, link: function(scope, element, attrs, ctrls) { var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { return; // do nothing if no ng-model } // Setup configuration parameters var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; paginationCtrl.init(ngModelCtrl, paginationConfig); if (attrs.maxSize) { scope.$parent.$watch($parse(attrs.maxSize), function(value) { maxSize = parseInt(value, 10); paginationCtrl.render(); }); } // Create page object used in template function makePage(number, text, isActive) { return { number: number, text: text, active: isActive }; } function getPages(currentPage, totalPages) { var pages = []; // Default page limits var startPage = 1, endPage = totalPages; var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); // recompute if maxSize if ( isMaxSized ) { if ( rotate ) { // Current page is displayed in the middle of the visible ones startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); endPage = startPage + maxSize - 1; // Adjust if limit is exceeded if (endPage > totalPages) { endPage = totalPages; startPage = endPage - maxSize + 1; } } else { // Visible pages are paginated with maxSize startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; // Adjust last page if limit is exceeded endPage = Math.min(startPage + maxSize - 1, totalPages); } } // Add page number links for (var number = startPage; number <= endPage; number++) { var page = makePage(number, number, number === currentPage); pages.push(page); } // Add links to move between page sets if ( isMaxSized && ! rotate ) { if ( startPage > 1 ) { var previousPageSet = makePage(startPage - 1, '...', false); pages.unshift(previousPageSet); } if ( endPage < totalPages ) { var nextPageSet = makePage(endPage + 1, '...', false); pages.push(nextPageSet); } } return pages; } var originalRender = paginationCtrl.render; paginationCtrl.render = function() { originalRender(); if (scope.page > 0 && scope.page <= scope.totalPages) { scope.pages = getPages(scope.page, scope.totalPages); } }; } }; }]) .constant('pagerConfig', { itemsPerPage: 10, previousText: '« Previous', nextText: 'Next »', align: true }) .directive('pager', ['pagerConfig', function(pagerConfig) { return { restrict: 'EA', scope: { totalItems: '=', previousText: '@', nextText: '@' }, require: ['pager', '?ngModel'], controller: 'PaginationController', templateUrl: 'template/pagination/pager.html', replace: true, link: function(scope, element, attrs, ctrls) { var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if (!ngModelCtrl) { return; // do nothing if no ng-model } scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; paginationCtrl.init(ngModelCtrl, pagerConfig); } }; }]); /** * The following features are still outstanding: animation as a * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ .provider( '$tooltip', function () { // The default options tooltip and popover. var defaultOptions = { placement: 'top', animation: true, popupDelay: 0 }; // Default hide triggers for each show trigger var triggerMap = { 'mouseenter': 'mouseleave', 'click': 'click', 'focus': 'blur' }; // The options specified to the provider globally. var globalOptions = {}; /** * `options({})` allows global configuration of all tooltips in the * application. * * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { * // place tooltips left instead of top by default * $tooltipProvider.options( { placement: 'left' } ); * }); */ this.options = function( value ) { angular.extend( globalOptions, value ); }; /** * This allows you to extend the set of trigger mappings available. E.g.: * * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); */ this.setTriggers = function setTriggers ( triggers ) { angular.extend( triggerMap, triggers ); }; /** * This is a helper function for translating camel-case to snake-case. */ function snake_case(name){ var regexp = /[A-Z]/g; var separator = '-'; return name.replace(regexp, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } /** * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { return function $tooltip ( type, prefix, defaultTriggerShow ) { var options = angular.extend( {}, defaultOptions, globalOptions ); /** * Returns an object of show and hide triggers. * * If a trigger is supplied, * it is used to show the tooltip; otherwise, it will use the `trigger` * option passed to the `$tooltipProvider.options` method; else it will * default to the trigger supplied to this directive factory. * * The hide trigger is based on the show trigger. If the `trigger` option * was passed to the `$tooltipProvider.options` method, it will use the * mapped trigger from `triggerMap` or the passed trigger if the map is * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ function getTriggers ( trigger ) { var show = trigger || options.trigger || defaultTriggerShow; var hide = triggerMap[show] || show; return { show: show, hide: hide }; } var directiveName = snake_case( type ); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); var template = '
    '+ '
    '; return { restrict: 'EA', compile: function (tElem, tAttrs) { var tooltipLinker = $compile( template ); return function link ( scope, element, attrs ) { var tooltip; var tooltipLinkedScope; var transitionTimeout; var popupTimeout; var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; var triggers = getTriggers( undefined ); var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); var ttScope = scope.$new(true); var positionTooltip = function () { var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); ttPosition.top += 'px'; ttPosition.left += 'px'; // Now set the calculated positioning. tooltip.css( ttPosition ); }; // By default, the tooltip is not open. // TODO add ability to start tooltip opened ttScope.isOpen = false; function toggleTooltipBind () { if ( ! ttScope.isOpen ) { showTooltipBind(); } else { hideTooltipBind(); } } // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { return; } prepareTooltip(); if ( ttScope.popupDelay ) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. if (!popupTimeout) { popupTimeout = $timeout( show, ttScope.popupDelay, false ); popupTimeout.then(function(reposition){reposition();}); } } else { show()(); } } function hideTooltipBind () { scope.$apply(function () { hide(); }); } // Show the tooltip popup element. function show() { popupTimeout = null; // If there is a pending remove transition, we must cancel it, lest the // tooltip be mysteriously removed. if ( transitionTimeout ) { $timeout.cancel( transitionTimeout ); transitionTimeout = null; } // Don't show empty tooltips. if ( ! ttScope.content ) { return angular.noop; } createTooltip(); // Set the initial positioning. tooltip.css({ top: 0, left: 0, display: 'block' }); ttScope.$digest(); positionTooltip(); // And show the tooltip. ttScope.isOpen = true; ttScope.$digest(); // digest required as $apply is not called // Return positioning function as promise callback for correct // positioning after draw. return positionTooltip; } // Hide the tooltip popup element. function hide() { // First things first: we don't show it anymore. ttScope.isOpen = false; //if tooltip is going to be shown after delay, we must cancel this $timeout.cancel( popupTimeout ); popupTimeout = null; // And now we remove it from the DOM. However, if we have animation, we // need to wait for it to expire beforehand. // FIXME: this is a placeholder for a port of the transitions library. if ( ttScope.animation ) { if (!transitionTimeout) { transitionTimeout = $timeout(removeTooltip, 500); } } else { removeTooltip(); } } function createTooltip() { // There can only be one tooltip element per directive shown at once. if (tooltip) { removeTooltip(); } tooltipLinkedScope = ttScope.$new(); tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { if ( appendToBody ) { $document.find( 'body' ).append( tooltip ); } else { element.after( tooltip ); } }); } function removeTooltip() { transitionTimeout = null; if (tooltip) { tooltip.remove(); tooltip = null; } if (tooltipLinkedScope) { tooltipLinkedScope.$destroy(); tooltipLinkedScope = null; } } function prepareTooltip() { prepPlacement(); prepPopupDelay(); } /** * Observe the relevant attributes. */ attrs.$observe( type, function ( val ) { ttScope.content = val; if (!val && ttScope.isOpen ) { hide(); } }); attrs.$observe( prefix+'Title', function ( val ) { ttScope.title = val; }); function prepPlacement() { var val = attrs[ prefix + 'Placement' ]; ttScope.placement = angular.isDefined( val ) ? val : options.placement; } function prepPopupDelay() { var val = attrs[ prefix + 'PopupDelay' ]; var delay = parseInt( val, 10 ); ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; } var unregisterTriggers = function () { element.unbind(triggers.show, showTooltipBind); element.unbind(triggers.hide, hideTooltipBind); }; function prepTriggers() { var val = attrs[ prefix + 'Trigger' ]; unregisterTriggers(); triggers = getTriggers( val ); if ( triggers.show === triggers.hide ) { element.bind( triggers.show, toggleTooltipBind ); } else { element.bind( triggers.show, showTooltipBind ); element.bind( triggers.hide, hideTooltipBind ); } } prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; // if a tooltip is attached to we need to remove it on // location change as its parent scope will probably not be destroyed // by the change. if ( appendToBody ) { scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { if ( ttScope.isOpen ) { hide(); } }); } // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { $timeout.cancel( transitionTimeout ); $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); ttScope = null; }); }; } }; }; }]; }) .directive( 'tooltipPopup', function () { return { restrict: 'EA', replace: true, scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, templateUrl: 'template/tooltip/tooltip-popup.html' }; }) .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); }]) .directive( 'tooltipHtmlUnsafePopup', function () { return { restrict: 'EA', replace: true, scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' }; }) .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html popovers, and selector delegatation. */ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) .directive( 'popoverPopup', function () { return { restrict: 'EA', replace: true, scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, templateUrl: 'template/popover/popover.html' }; }) .directive( 'popover', [ '$tooltip', function ( $tooltip ) { return $tooltip( 'popover', 'popover', 'click' ); }]); angular.module('ui.bootstrap.progressbar', []) .constant('progressConfig', { animate: true, max: 100 }) .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { var self = this, animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; this.bars = []; $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; this.addBar = function(bar, element) { if ( !animate ) { element.css({'transition': 'none'}); } this.bars.push(bar); bar.$watch('value', function( value ) { bar.percent = +(100 * value / $scope.max).toFixed(2); }); bar.$on('$destroy', function() { element = null; self.removeBar(bar); }); }; this.removeBar = function(bar) { this.bars.splice(this.bars.indexOf(bar), 1); }; }]) .directive('progress', function() { return { restrict: 'EA', replace: true, transclude: true, controller: 'ProgressController', require: 'progress', scope: {}, templateUrl: 'template/progressbar/progress.html' }; }) .directive('bar', function() { return { restrict: 'EA', replace: true, transclude: true, require: '^progress', scope: { value: '=', type: '@' }, templateUrl: 'template/progressbar/bar.html', link: function(scope, element, attrs, progressCtrl) { progressCtrl.addBar(scope, element); } }; }) .directive('progressbar', function() { return { restrict: 'EA', replace: true, transclude: true, controller: 'ProgressController', scope: { value: '=', type: '@' }, templateUrl: 'template/progressbar/progressbar.html', link: function(scope, element, attrs, progressCtrl) { progressCtrl.addBar(scope, angular.element(element.children()[0])); } }; }); angular.module('ui.bootstrap.rating', []) .constant('ratingConfig', { max: 5, stateOn: null, stateOff: null }) .controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { var ngModelCtrl = { $setViewValue: angular.noop }; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); $scope.range = this.buildTemplateObjects(ratingStates); }; this.buildTemplateObjects = function(states) { for (var i = 0, n = states.length; i < n; i++) { states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); } return states; }; $scope.rate = function(value) { if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { ngModelCtrl.$setViewValue(value); ngModelCtrl.$render(); } }; $scope.enter = function(value) { if ( !$scope.readonly ) { $scope.value = value; } $scope.onHover({value: value}); }; $scope.reset = function() { $scope.value = ngModelCtrl.$viewValue; $scope.onLeave(); }; $scope.onKeydown = function(evt) { if (/(37|38|39|40)/.test(evt.which)) { evt.preventDefault(); evt.stopPropagation(); $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); } }; this.render = function() { $scope.value = ngModelCtrl.$viewValue; }; }]) .directive('rating', function() { return { restrict: 'EA', require: ['rating', 'ngModel'], scope: { readonly: '=?', onHover: '&', onLeave: '&' }, controller: 'RatingController', templateUrl: 'template/rating/rating.html', replace: true, link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if ( ngModelCtrl ) { ratingCtrl.init( ngModelCtrl ); } } }; }); /** * @ngdoc overview * @name ui.bootstrap.tabs * * @description * AngularJS version of the tabs directive. */ angular.module('ui.bootstrap.tabs', []) .controller('TabsetController', ['$scope', function TabsetCtrl($scope) { var ctrl = this, tabs = ctrl.tabs = $scope.tabs = []; ctrl.select = function(selectedTab) { angular.forEach(tabs, function(tab) { if (tab.active && tab !== selectedTab) { tab.active = false; tab.onDeselect(); } }); selectedTab.active = true; selectedTab.onSelect(); }; ctrl.addTab = function addTab(tab) { tabs.push(tab); // we can't run the select function on the first tab // since that would select it twice if (tabs.length === 1) { tab.active = true; } else if (tab.active) { ctrl.select(tab); } }; ctrl.removeTab = function removeTab(tab) { var index = tabs.indexOf(tab); //Select a new tab if the tab to be removed is selected and not destroyed if (tab.active && tabs.length > 1 && !destroyed) { //If this is the last tab, select the previous tab. else, the next tab. var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; ctrl.select(tabs[newActiveIndex]); } tabs.splice(index, 1); }; var destroyed; $scope.$on('$destroy', function() { destroyed = true; }); }]) /** * @ngdoc directive * @name ui.bootstrap.tabs.directive:tabset * @restrict EA * * @description * Tabset is the outer container for the tabs directive * * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. * @param {boolean=} justified Whether or not to use justified styling for the tabs. * * @example First Content! Second Content!
    First Vertical Content! Second Vertical Content! First Justified Content! Second Justified Content!
    */ .directive('tabset', function() { return { restrict: 'EA', transclude: true, replace: true, scope: { type: '@' }, controller: 'TabsetController', templateUrl: 'template/tabs/tabset.html', link: function(scope, element, attrs) { scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; } }; }) /** * @ngdoc directive * @name ui.bootstrap.tabs.directive:tab * @restrict EA * * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. * @param {string=} select An expression to evaluate when the tab is selected. * @param {boolean=} active A binding, telling whether or not this tab is selected. * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. * * @description * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. * * @example

    First Tab Alert me! Second Tab, with alert callback and html heading! {{item.content}}
    function TabsDemoCtrl($scope) { $scope.items = [ { title:"Dynamic Title 1", content:"Dynamic Item 0" }, { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } ]; $scope.alertMe = function() { setTimeout(function() { alert("You've selected the alert tab!"); }); }; };
    */ /** * @ngdoc directive * @name ui.bootstrap.tabs.directive:tabHeading * @restrict EA * * @description * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. * * @example HTML in my titles?! And some content, too! Icon heading?!? That's right. */ .directive('tab', ['$parse', function($parse) { return { require: '^tabset', restrict: 'EA', replace: true, templateUrl: 'template/tabs/tab.html', transclude: true, scope: { active: '=?', heading: '@', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' }, controller: function() { //Empty controller so other directives can require being 'under' a tab }, compile: function(elm, attrs, transclude) { return function postLink(scope, elm, attrs, tabsetCtrl) { scope.$watch('active', function(active) { if (active) { tabsetCtrl.select(scope); } }); scope.disabled = false; if ( attrs.disabled ) { scope.$parent.$watch($parse(attrs.disabled), function(value) { scope.disabled = !! value; }); } scope.select = function() { if ( !scope.disabled ) { scope.active = true; } }; tabsetCtrl.addTab(scope); scope.$on('$destroy', function() { tabsetCtrl.removeTab(scope); }); //We need to transclude later, once the content container is ready. //when this link happens, we're inside a tab heading. scope.$transcludeFn = transclude; }; } }; }]) .directive('tabHeadingTransclude', [function() { return { restrict: 'A', require: '^tab', link: function(scope, elm, attrs, tabCtrl) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { elm.html(''); elm.append(heading); } }); } }; }]) .directive('tabContentTransclude', function() { return { restrict: 'A', require: '^tabset', link: function(scope, elm, attrs) { var tab = scope.$eval(attrs.tabContentTransclude); //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. tab.$transcludeFn(tab.$parent, function(contents) { angular.forEach(contents, function(node) { if (isTabHeading(node)) { //Let tabHeadingTransclude know. tab.headingElement = node; } else { elm.append(node); } }); }); } }; function isTabHeading(node) { return node.tagName && ( node.hasAttribute('tab-heading') || node.hasAttribute('data-tab-heading') || node.tagName.toLowerCase() === 'tab-heading' || node.tagName.toLowerCase() === 'data-tab-heading' ); } }) ; angular.module('ui.bootstrap.timepicker', []) .constant('timepickerConfig', { hourStep: 1, minuteStep: 1, showMeridian: true, meridians: null, readonlyInput: false, mousewheel: true }) .controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { var selected = new Date(), ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; this.init = function( ngModelCtrl_, inputs ) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; if ( mousewheel ) { this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; this.setupInputEvents( hoursInputEl, minutesInputEl ); }; var hourStep = timepickerConfig.hourStep; if ($attrs.hourStep) { $scope.$parent.$watch($parse($attrs.hourStep), function(value) { hourStep = parseInt(value, 10); }); } var minuteStep = timepickerConfig.minuteStep; if ($attrs.minuteStep) { $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { minuteStep = parseInt(value, 10); }); } // 12H / 24H mode $scope.showMeridian = timepickerConfig.showMeridian; if ($attrs.showMeridian) { $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { $scope.showMeridian = !!value; if ( ngModelCtrl.$error.time ) { // Evaluate from template var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); if (angular.isDefined( hours ) && angular.isDefined( minutes )) { selected.setHours( hours ); refresh(); } } else { updateTemplate(); } }); } // Get $scope.hours in 24H mode if valid function getHoursFromTemplate ( ) { var hours = parseInt( $scope.hours, 10 ); var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); if ( !valid ) { return undefined; } if ( $scope.showMeridian ) { if ( hours === 12 ) { hours = 0; } if ( $scope.meridian === meridians[1] ) { hours = hours + 12; } } return hours; } function getMinutesFromTemplate() { var minutes = parseInt($scope.minutes, 10); return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; } function pad( value ) { return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; } // Respond on mousewheel spin this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; } //pick correct delta variable depending on event var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; return (e.detail || delta > 0); }; hoursInputEl.bind('mousewheel wheel', function(e) { $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); e.preventDefault(); }); minutesInputEl.bind('mousewheel wheel', function(e) { $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); e.preventDefault(); }); }; this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { if ( $scope.readonlyInput ) { $scope.updateHours = angular.noop; $scope.updateMinutes = angular.noop; return; } var invalidate = function(invalidHours, invalidMinutes) { ngModelCtrl.$setViewValue( null ); ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; } if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; } }; $scope.updateHours = function() { var hours = getHoursFromTemplate(); if ( angular.isDefined(hours) ) { selected.setHours( hours ); refresh( 'h' ); } else { invalidate(true); } }; hoursInputEl.bind('blur', function(e) { if ( !$scope.invalidHours && $scope.hours < 10) { $scope.$apply( function() { $scope.hours = pad( $scope.hours ); }); } }); $scope.updateMinutes = function() { var minutes = getMinutesFromTemplate(); if ( angular.isDefined(minutes) ) { selected.setMinutes( minutes ); refresh( 'm' ); } else { invalidate(undefined, true); } }; minutesInputEl.bind('blur', function(e) { if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { $scope.$apply( function() { $scope.minutes = pad( $scope.minutes ); }); } }); }; this.render = function() { var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; if ( isNaN(date) ) { ngModelCtrl.$setValidity('time', false); $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } else { if ( date ) { selected = date; } makeValid(); updateTemplate(); } }; // Call internally when we know that model is valid. function refresh( keyboardChange ) { makeValid(); ngModelCtrl.$setViewValue( new Date(selected) ); updateTemplate( keyboardChange ); } function makeValid() { ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; } function updateTemplate( keyboardChange ) { var hours = selected.getHours(), minutes = selected.getMinutes(); if ( $scope.showMeridian ) { hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system } $scope.hours = keyboardChange === 'h' ? hours : pad(hours); $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } function addMinutes( minutes ) { var dt = new Date( selected.getTime() + minutes * 60000 ); selected.setHours( dt.getHours(), dt.getMinutes() ); refresh(); } $scope.incrementHours = function() { addMinutes( hourStep * 60 ); }; $scope.decrementHours = function() { addMinutes( - hourStep * 60 ); }; $scope.incrementMinutes = function() { addMinutes( minuteStep ); }; $scope.decrementMinutes = function() { addMinutes( - minuteStep ); }; $scope.toggleMeridian = function() { addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); }; }]) .directive('timepicker', function () { return { restrict: 'EA', require: ['timepicker', '?^ngModel'], controller:'TimepickerController', replace: true, scope: {}, templateUrl: 'template/timepicker/timepicker.html', link: function(scope, element, attrs, ctrls) { var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if ( ngModelCtrl ) { timepickerCtrl.init( ngModelCtrl, element.find('input') ); } } }; }); angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) /** * A helper service that can parse typeahead's syntax (string provided by users) * Extracted to a separate service for ease of unit testing */ .factory('typeaheadParser', ['$parse', function ($parse) { // 00000111000000000000022200000000000000003333333333333330000000000044000 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; return { parse:function (input) { var match = input.match(TYPEAHEAD_REGEXP); if (!match) { throw new Error( 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + ' but got "' + input + '".'); } return { itemName:match[3], source:$parse(match[4]), viewMapper:$parse(match[2] || match[1]), modelMapper:$parse(match[1]) }; } }; }]) .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; return { require:'ngModel', link:function (originalScope, element, attrs, modelCtrl) { //SUPPORTED ATTRIBUTES (OPTIONS) //minimal no of characters that needs to be entered before typeahead kicks-in var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; //minimal wait time after last character typed before typehead kicks-in var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; //should it restrict model values to the ones selected from the popup only? var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; //binding to a variable that indicates if matches are being retrieved asynchronously var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; //a callback executed when a match is selected var onSelectCallback = $parse(attrs.typeaheadOnSelect); var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; //INTERNAL VARIABLES //model setter executed upon match selection var $setModelValue = $parse(attrs.ngModel).assign; //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.typeahead); var hasFocus; //create a child scope for the typeahead directive so we are not polluting original scope //with typeahead-specific data (matches, query etc.) var scope = originalScope.$new(); originalScope.$on('$destroy', function(){ scope.$destroy(); }); // WAI-ARIA var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); element.attr({ 'aria-autocomplete': 'list', 'aria-expanded': false, 'aria-owns': popupId }); //pop-up element used to display matches var popUpEl = angular.element('
    '); popUpEl.attr({ id: popupId, matches: 'matches', active: 'activeIdx', select: 'select(activeIdx)', query: 'query', position: 'position' }); //custom item template if (angular.isDefined(attrs.typeaheadTemplateUrl)) { popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); } var resetMatches = function() { scope.matches = []; scope.activeIdx = -1; element.attr('aria-expanded', false); }; var getMatchId = function(index) { return popupId + '-option-' + index; }; // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. // This attribute is added or removed automatically when the `activeIdx` changes. scope.$watch('activeIdx', function(index) { if (index < 0) { element.removeAttr('aria-activedescendant'); } else { element.attr('aria-activedescendant', getMatchId(index)); } }); var getMatchesAsync = function(inputValue) { var locals = {$viewValue: inputValue}; isLoadingSetter(originalScope, true); $q.when(parserResult.source(originalScope, locals)).then(function(matches) { //it might happen that several async queries were in progress if a user were typing fast //but we are interested only in responses that correspond to the current view value var onCurrentRequest = (inputValue === modelCtrl.$viewValue); if (onCurrentRequest && hasFocus) { if (matches.length > 0) { scope.activeIdx = focusFirst ? 0 : -1; scope.matches.length = 0; //transform labels for(var i=0; i= minSearch) { if (waitTime > 0) { cancelPreviousTimeout(); scheduleSearchWithTimeout(inputValue); } else { getMatchesAsync(inputValue); } } else { isLoadingSetter(originalScope, false); cancelPreviousTimeout(); resetMatches(); } if (isEditable) { return inputValue; } else { if (!inputValue) { // Reset in case user had typed something previously. modelCtrl.$setValidity('editable', true); return inputValue; } else { modelCtrl.$setValidity('editable', false); return undefined; } } }); modelCtrl.$formatters.push(function (modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; if (inputFormatter) { locals.$model = modelValue; return inputFormatter(originalScope, locals); } else { //it might happen that we don't have enough info to properly render input value //we need to check for this situation and simply return model value if we can't apply custom formatting locals[parserResult.itemName] = modelValue; candidateViewValue = parserResult.viewMapper(originalScope, locals); locals[parserResult.itemName] = undefined; emptyViewValue = parserResult.viewMapper(originalScope, locals); return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; } }); scope.select = function (activeIdx) { //called from within the $digest() cycle var locals = {}; var model, item; locals[parserResult.itemName] = item = scope.matches[activeIdx].model; model = parserResult.modelMapper(originalScope, locals); $setModelValue(originalScope, model); modelCtrl.$setValidity('editable', true); onSelectCallback(originalScope, { $item: item, $model: model, $label: parserResult.viewMapper(originalScope, locals) }); resetMatches(); //return focus to the input element if a match was selected via a mouse click event // use timeout to avoid $rootScope:inprog error $timeout(function() { element[0].focus(); }, 0, false); }; //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) element.bind('keydown', function (evt) { //typeahead is open and an "interesting" key was pressed if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { return; } // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { return; } evt.preventDefault(); if (evt.which === 40) { scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; scope.$digest(); } else if (evt.which === 38) { scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; scope.$digest(); } else if (evt.which === 13 || evt.which === 9) { scope.$apply(function () { scope.select(scope.activeIdx); }); } else if (evt.which === 27) { evt.stopPropagation(); resetMatches(); scope.$digest(); } }); element.bind('blur', function (evt) { hasFocus = false; }); // Keep reference to click handler to unbind it. var dismissClickHandler = function (evt) { if (element[0] !== evt.target) { resetMatches(); scope.$digest(); } }; $document.bind('click', dismissClickHandler); originalScope.$on('$destroy', function(){ $document.unbind('click', dismissClickHandler); if (appendToBody) { $popup.remove(); } }); var $popup = $compile(popUpEl)(scope); if (appendToBody) { $document.find('body').append($popup); } else { element.after($popup); } } }; }]) .directive('typeaheadPopup', function () { return { restrict:'EA', scope:{ matches:'=', query:'=', active:'=', position:'=', select:'&' }, replace:true, templateUrl:'template/typeahead/typeahead-popup.html', link:function (scope, element, attrs) { scope.templateUrl = attrs.templateUrl; scope.isOpen = function () { return scope.matches.length > 0; }; scope.isActive = function (matchIdx) { return scope.active == matchIdx; }; scope.selectActive = function (matchIdx) { scope.active = matchIdx; }; scope.selectMatch = function (activeIdx) { scope.select({activeIdx:activeIdx}); }; } }; }) .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { return { restrict:'EA', scope:{ index:'=', match:'=', query:'=' }, link:function (scope, element, attrs) { var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ element.replaceWith($compile(tplContent.trim())(scope)); }); } }; }]) .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } return function(matchItem, query) { return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; }; }); ================================================ FILE: public/dist/js/angular.js ================================================ /** * @license AngularJS v1.3.15 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, document, undefined) {'use strict'; /** * @description * * This object provides a utility for producing rich Error messages within * Angular. It can be called as follows: * * var exampleMinErr = minErr('example'); * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); * * The above creates an instance of minErr in the example namespace. The * resulting error will have a namespaced error code of example.one. The * resulting error will replace {0} with the value of foo, and {1} with the * value of bar. The object is not restricted in the number of arguments it can * take. * * If fewer arguments are specified than necessary for interpolation, the extra * interpolation markers will be preserved in the final string. * * Since data will be parsed statically during a build step, some restrictions * are applied with respect to how minErr instances are created and called. * Instances should have names of the form namespaceMinErr for a minErr created * using minErr('namespace') . Error codes, namespaces and template strings * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; return function() { var code = arguments[0], prefix = '[' + (module ? module + ':' : '') + code + '] ', template = arguments[1], templateArgs = arguments, message, i; message = prefix + template.replace(/\{\d+\}/g, function(match) { var index = +match.slice(1, -1), arg; if (index + 2 < templateArgs.length) { return toDebugString(templateArgs[index + 2]); } return match; }); message = message + '\nhttp://errors.angularjs.org/1.3.15/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + encodeURIComponent(toDebugString(arguments[i])); } return new ErrorConstructor(message); }; } /* We need to tell jshint what variables are being exported */ /* global angular: true, msie: true, jqLite: true, jQuery: true, slice: true, splice: true, push: true, toString: true, ngMinErr: true, angularModule: true, uid: true, REGEX_STRING_REGEXP: true, VALIDITY_STATE_PROPERTY: true, lowercase: true, uppercase: true, manualLowercase: true, manualUppercase: true, nodeName_: true, isArrayLike: true, forEach: true, sortedKeys: true, forEachSorted: true, reverseParams: true, nextUid: true, setHashKey: true, extend: true, int: true, inherit: true, noop: true, identity: true, valueFn: true, isUndefined: true, isDefined: true, isObject: true, isString: true, isNumber: true, isDate: true, isArray: true, isFunction: true, isRegExp: true, isWindow: true, isScope: true, isFile: true, isFormData: true, isBlob: true, isBoolean: true, isPromiseLike: true, trim: true, escapeForRegexp: true, isElement: true, makeMap: true, includes: true, arrayRemove: true, copy: true, shallowCopy: true, equals: true, csp: true, concat: true, sliceArgs: true, bind: true, toJsonReplacer: true, toJson: true, fromJson: true, startingTag: true, tryDecodeURIComponent: true, parseKeyValue: true, toKeyValue: true, encodeUriSegment: true, encodeUriQuery: true, angularInit: true, bootstrap: true, getTestability: true, snake_case: true, bindJQuery: true, assertArg: true, assertArgFn: true, assertNotHasOwnProperty: true, getter: true, getBlockNodes: true, hasOwnProperty: true, createMap: true, NODE_TYPE_ELEMENT: true, NODE_TYPE_TEXT: true, NODE_TYPE_COMMENT: true, NODE_TYPE_DOCUMENT: true, NODE_TYPE_DOCUMENT_FRAGMENT: true, */ //////////////////////////////////// /** * @ngdoc module * @name ng * @module ng * @description * * # ng (core module) * The ng module is loaded by default when an AngularJS application is started. The module itself * contains the essential components for an AngularJS application to function. The table below * lists a high level breakdown of each of the services/factories, filters, directives and testing * components available within this core module. * *
    */ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // The name of a form control's ValidityState property. // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; /** * @ngdoc function * @name angular.lowercase * @module ng * @kind function * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase * @module ng * @kind function * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; var manualLowercase = function(s) { /* jshint bitwise: false */ return isString(s) ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) : s; }; var manualUppercase = function(s) { /* jshint bitwise: false */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; }; // String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish // locale, for this reason we need to detect this case and redefine lowercase/uppercase methods // with correct but slower alternatives. if ('i' !== 'I'.toLowerCase()) { lowercase = manualLowercase; uppercase = manualUppercase; } var msie, // holds major version number for IE, or NaN if UA is not IE. jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, splice = [].splice, push = [].push, toString = Object.prototype.toString, ngMinErr = minErr('ng'), /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, uid = 0; /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ msie = document.documentMode; /** * @private * @param {*} obj * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, * String ...) */ function isArrayLike(obj) { if (obj == null || isWindow(obj)) { return false; } var length = obj.length; if (obj.nodeType === NODE_TYPE_ELEMENT && length) { return true; } return isString(obj) || isArray(obj) || length === 0 || typeof length === 'number' && length > 0 && (length - 1) in obj; } /** * @ngdoc function * @name angular.forEach * @module ng * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` * is the value of an object property or an array element, `key` is the object property key or * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. * * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. * * Unlike ES262's * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just * return the value provided. * ```js var values = {name: 'misko', gender: 'male'}; var log = []; angular.forEach(values, function(value, key) { this.push(key + ': ' + value); }, log); expect(log).toEqual(['name: misko', 'gender: male']); ``` * * @param {Object|Array} obj Object to iterate over. * @param {Function} iterator Iterator function. * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ function forEach(obj, iterator, context) { var key, length; if (obj) { if (isFunction(obj)) { for (key in obj) { // Need to check if hasOwnProperty exists, // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { iterator.call(context, obj[key], key, obj); } } } else if (isArray(obj) || isArrayLike(obj)) { var isPrimitive = typeof obj !== 'object'; for (key = 0, length = obj.length; key < length; key++) { if (isPrimitive || key in obj) { iterator.call(context, obj[key], key, obj); } } } else if (obj.forEach && obj.forEach !== forEach) { obj.forEach(iterator, context, obj); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } } } return obj; } function sortedKeys(obj) { return Object.keys(obj).sort(); } function forEachSorted(obj, iterator, context) { var keys = sortedKeys(obj); for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } return keys; } /** * when using forEach the params are value, key, but it is often useful to have key, value. * @param {function(string, *)} iteratorFn * @returns {function(*, string)} */ function reverseParams(iteratorFn) { return function(value, key) { iteratorFn(key, value); }; } /** * A consistent way of creating unique IDs in angular. * * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before * we hit number precision issues in JavaScript. * * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M * * @returns {number} an unique alpha-numeric string */ function nextUid() { return ++uid; } /** * Set or clear the hashkey for an object. * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { if (h) { obj.$$hashKey = h; } else { delete obj.$$hashKey; } } /** * @ngdoc function * @name angular.extend * @module ng * @kind function * * @description * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. * Note: Keep in mind that `angular.extend` does not support recursive merge (deep copy). * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst) { var h = dst.$$hashKey; for (var i = 1, ii = arguments.length; i < ii; i++) { var obj = arguments[i]; if (obj) { var keys = Object.keys(obj); for (var j = 0, jj = keys.length; j < jj; j++) { var key = keys[j]; dst[key] = obj[key]; } } } setHashKey(dst, h); return dst; } function int(str) { return parseInt(str, 10); } function inherit(parent, extra) { return extend(Object.create(parent), extra); } /** * @ngdoc function * @name angular.noop * @module ng * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the * functional style. ```js function foo(callback) { var result = calculateResult(); (callback || angular.noop)(result); } ``` */ function noop() {} noop.$inject = []; /** * @ngdoc function * @name angular.identity * @module ng * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the * functional style. * ```js function transformer(transformationFn, value) { return (transformationFn || angular.identity)(value); }; ``` * @param {*} value to be returned. * @returns {*} the value passed in. */ function identity($) {return $;} identity.$inject = []; function valueFn(value) {return function() {return value;};} /** * @ngdoc function * @name angular.isUndefined * @module ng * @kind function * * @description * Determines if a reference is undefined. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ function isUndefined(value) {return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined * @module ng * @kind function * * @description * Determines if a reference is defined. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ function isDefined(value) {return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject * @module ng * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not * considered to be objects. Note that JavaScript arrays are objects. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ function isObject(value) { // http://jsperf.com/isobject4 return value !== null && typeof value === 'object'; } /** * @ngdoc function * @name angular.isString * @module ng * @kind function * * @description * Determines if a reference is a `String`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ function isString(value) {return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber * @module ng * @kind function * * @description * Determines if a reference is a `Number`. * * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. * * If you wish to exclude these then you can use the native * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) * method. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ function isNumber(value) {return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate * @module ng * @kind function * * @description * Determines if a value is a date. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ function isDate(value) { return toString.call(value) === '[object Date]'; } /** * @ngdoc function * @name angular.isArray * @module ng * @kind function * * @description * Determines if a reference is an `Array`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ var isArray = Array.isArray; /** * @ngdoc function * @name angular.isFunction * @module ng * @kind function * * @description * Determines if a reference is a `Function`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ function isFunction(value) {return typeof value === 'function';} /** * Determines if a value is a regular expression object. * * @private * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `RegExp`. */ function isRegExp(value) { return toString.call(value) === '[object RegExp]'; } /** * Checks if `obj` is a window object. * * @private * @param {*} obj Object to check * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { return obj && obj.window === obj; } function isScope(obj) { return obj && obj.$evalAsync && obj.$watch; } function isFile(obj) { return toString.call(obj) === '[object File]'; } function isFormData(obj) { return toString.call(obj) === '[object FormData]'; } function isBlob(obj) { return toString.call(obj) === '[object Blob]'; } function isBoolean(value) { return typeof value === 'boolean'; } function isPromiseLike(obj) { return obj && isFunction(obj.then); } var trim = function(value) { return isString(value) ? value.trim() : value; }; // Copied from: // http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 // Prereq: s is a string. var escapeForRegexp = function(s) { return s.replace(/([-()\[\]{}+?*.$\^|,:#= 0) array.splice(index, 1); return value; } /** * @ngdoc function * @name angular.copy * @module ng * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. * * * If no destination is supplied, a copy of the object or array is created. * * If a destination is provided, all of its elements (for arrays) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. * * If `source` is identical to 'destination' an exception will be thrown. * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. * @param {(Object|Array)=} destination Destination into which the source is copied. If * provided, must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example
    Name:
    E-mail:
    Gender: male female
    form = {{user | json}}
    master = {{master | json}}
    */ function copy(source, destination, stackSource, stackDest) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', "Can't copy! Making copies of Window or Scope instances is not supported."); } if (!destination) { destination = source; if (source) { if (isArray(source)) { destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); destination.lastIndex = source.lastIndex; } else if (isObject(source)) { var emptyObject = Object.create(Object.getPrototypeOf(source)); destination = copy(source, emptyObject, stackSource, stackDest); } } } else { if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); stackSource = stackSource || []; stackDest = stackDest || []; if (isObject(source)) { var index = stackSource.indexOf(source); if (index !== -1) return stackDest[index]; stackSource.push(source); stackDest.push(destination); } var result; if (isArray(source)) { destination.length = 0; for (var i = 0; i < source.length; i++) { result = copy(source[i], null, stackSource, stackDest); if (isObject(source[i])) { stackSource.push(source[i]); stackDest.push(result); } destination.push(result); } } else { var h = destination.$$hashKey; if (isArray(destination)) { destination.length = 0; } else { forEach(destination, function(value, key) { delete destination[key]; }); } for (var key in source) { if (source.hasOwnProperty(key)) { result = copy(source[key], null, stackSource, stackDest); if (isObject(source[key])) { stackSource.push(source[key]); stackDest.push(result); } destination[key] = result; } } setHashKey(destination,h); } } return destination; } /** * Creates a shallow copy of an object, an array or a primitive. * * Assumes that there are no proto properties for objects. */ function shallowCopy(src, dst) { if (isArray(src)) { dst = dst || []; for (var i = 0, ii = src.length; i < ii; i++) { dst[i] = src[i]; } } else if (isObject(src)) { dst = dst || {}; for (var key in src) { if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { dst[key] = src[key]; } } } return dst || src; } /** * @ngdoc function * @name angular.equals * @module ng * @kind function * * @description * Determines if two objects or two values are equivalent. Supports value types, regular * expressions, arrays and objects. * * Two objects or values are considered equivalent if at least one of the following is true: * * * Both objects or values pass `===` comparison. * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * * During a property comparison, properties of `function` type and properties with names * that begin with `$` are ignored. * * Scope and DOMWindow objects are being compared only by identify (`===`). * * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. */ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; if (t1 == t2) { if (t1 == 'object') { if (isArray(o1)) { if (!isArray(o2)) return false; if ((length = o1.length) == o2.length) { for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; } return true; } } else if (isDate(o1)) { if (!isDate(o2)) return false; return equals(o1.getTime(), o2.getTime()); } else if (isRegExp(o1)) { return isRegExp(o2) ? o1.toString() == o2.toString() : false; } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2) || isDate(o2) || isRegExp(o2)) return false; keySet = {}; for (key in o1) { if (key.charAt(0) === '$' || isFunction(o1[key])) continue; if (!equals(o1[key], o2[key])) return false; keySet[key] = true; } for (key in o2) { if (!keySet.hasOwnProperty(key) && key.charAt(0) !== '$' && o2[key] !== undefined && !isFunction(o2[key])) return false; } return true; } } } return false; } var csp = function() { if (isDefined(csp.isActive_)) return csp.isActive_; var active = !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]')); if (!active) { try { /* jshint -W031, -W054 */ new Function(''); /* jshint +W031, +W054 */ } catch (e) { active = true; } } return (csp.isActive_ = active); }; function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); } function sliceArgs(args, startIndex) { return slice.call(args, startIndex || 0); } /* jshint -W101 */ /** * @ngdoc function * @name angular.bind * @module ng * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for * `fn`). You can supply optional `args` that are prebound to the function. This feature is also * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). * * @param {Object} self Context which `fn` should be evaluated in. * @param {function()} fn Function to be bound. * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ /* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { return curryArgs.length ? function() { return arguments.length ? fn.apply(self, concat(curryArgs, arguments, 0)) : fn.apply(self, curryArgs); } : function() { return arguments.length ? fn.apply(self, arguments) : fn.call(self); }; } else { // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) return fn; } } function toJsonReplacer(key, value) { var val = value; if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; } else if (value && document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; } return val; } /** * @ngdoc function * @name angular.toJson * @module ng * @kind function * * @description * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. * @param {boolean|number=} pretty If set to true, the JSON output will contain newlines and whitespace. * If set to an integer, the JSON output will contain that many spaces per indentation (the default is 2). * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { if (typeof obj === 'undefined') return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } return JSON.stringify(obj, toJsonReplacer, pretty); } /** * @ngdoc function * @name angular.fromJson * @module ng * @kind function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. * @returns {Object|Array|string|number} Deserialized JSON string. */ function fromJson(json) { return isString(json) ? JSON.parse(json) : json; } /** * @returns {string} Returns the string representation of the element. */ function startingTag(element) { element = jqLite(element).clone(); try { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. element.empty(); } catch (e) {} var elemHtml = jqLite('
    ').append(element).html(); try { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); } catch (e) { return lowercase(elemHtml); } } ///////////////////////////////////////////////// /** * Tries to decode the URI component without throwing an exception. * * @private * @param str value potential URI component to check. * @returns {boolean} True if `value` can be decoded * with the decodeURIComponent function. */ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch (e) { // Ignore any invalid uri component } } /** * Parses an escaped url query string into key-value pairs. * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; forEach((keyValue || "").split('&'), function(keyValue) { if (keyValue) { key_value = keyValue.replace(/\+/g,'%20').split('='); key = tryDecodeURIComponent(key_value[0]); if (isDefined(key)) { var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; if (!hasOwnProperty.call(obj, key)) { obj[key] = val; } else if (isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; } } } }); return obj; } function toKeyValue(obj) { var parts = []; forEach(obj, function(value, key) { if (isArray(value)) { forEach(value, function(arrayValue) { parts.push(encodeUriQuery(key, true) + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); }); } else { parts.push(encodeUriQuery(key, true) + (value === true ? '' : '=' + encodeUriQuery(value, true))); } }); return parts.length ? parts.join('&') : ''; } /** * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path * segments: * segment = *pchar * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * pct-encoded = "%" HEXDIG HEXDIG * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ function encodeUriSegment(val) { return encodeUriQuery(val, true). replace(/%26/gi, '&'). replace(/%3D/gi, '='). replace(/%2B/gi, '+'); } /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val). replace(/%40/gi, '@'). replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). replace(/%3B/gi, ';'). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; function getNgAttribute(element, ngAttr) { var attr, i, ii = ngAttrPrefixes.length; element = jqLite(element); for (i = 0; i < ii; ++i) { attr = ngAttrPrefixes[i] + ngAttr; if (isString(attr = element.attr(attr))) { return attr; } } return null; } /** * @ngdoc directive * @name ngApp * @module ng * * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be * created in "strict-di" mode. This means that the application will fail to invoke functions which * do not use explicit function annotation (and are thus unsuitable for minification), as described * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in * tracking down the root of these bugs. * * @description * * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `` or `` tags. * * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` * found in the document will be used to define the root element to auto-bootstrap as an * application. To run multiple applications in an HTML document you must manually bootstrap them using * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. * * You can specify an **AngularJS module** to be used as the root module for the application. This * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It * should contain the application code needed or have dependencies on other modules that will * contain the code. See {@link angular.module} for more information. * * In the example below if the `ngApp` directive were not placed on the `html` element then the * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` * would not be resolved to `3`. * * `ngApp` is the easiest, and most common way to bootstrap an application. *
    I can add: {{a}} + {{b}} = {{ a+b }}
    angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { $scope.a = 1; $scope.b = 2; });
    * * Using `ngStrictDi`, you would see something like this: *
    I can add: {{a}} + {{b}} = {{ a+b }}

    This renders because the controller does not fail to instantiate, by using explicit annotation style (see script.js for details)

    Name:
    Hello, {{name}}!

    This renders because the controller does not fail to instantiate, by using explicit annotation style (see script.js for details)

    I can add: {{a}} + {{b}} = {{ a+b }}

    The controller could not be instantiated, due to relying on automatic function annotations (which are disabled in strict mode). As such, the content of this section is not interpolated, and there should be an error in your web console.

    angular.module('ngAppStrictDemo', []) // BadController will fail to instantiate, due to relying on automatic function annotation, // rather than an explicit annotation .controller('BadController', function($scope) { $scope.a = 1; $scope.b = 2; }) // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, // due to using explicit annotations using the array style and $inject property, respectively. .controller('GoodController1', ['$scope', function($scope) { $scope.a = 1; $scope.b = 2; }]) .controller('GoodController2', GoodController2); function GoodController2($scope) { $scope.name = "World"; } GoodController2.$inject = ['$scope']; div[ng-controller] { margin-bottom: 1em; -webkit-border-radius: 4px; border-radius: 4px; border: 1px solid; padding: .5em; } div[ng-controller^=Good] { border-color: #d6e9c6; background-color: #dff0d8; color: #3c763d; } div[ng-controller^=Bad] { border-color: #ebccd1; background-color: #f2dede; color: #a94442; margin-bottom: 0; }
    */ function angularInit(element, bootstrap) { var appElement, module, config = {}; // The element `element` has priority over any other element forEach(ngAttrPrefixes, function(prefix) { var name = prefix + 'app'; if (!appElement && element.hasAttribute && element.hasAttribute(name)) { appElement = element; module = element.getAttribute(name); } }); forEach(ngAttrPrefixes, function(prefix) { var name = prefix + 'app'; var candidate; if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { appElement = candidate; module = candidate.getAttribute(name); } }); if (appElement) { config.strictDi = getNgAttribute(appElement, "strict-di") !== null; bootstrap(appElement, module ? [module] : [], config); } } /** * @ngdoc function * @name angular.bootstrap * @module ng * @description * Use this function to manually start up angular application. * * See: {@link guide/bootstrap Bootstrap} * * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * * ```html * * * *
    * {{greeting}} *
    * * * * * * ``` * * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) * function that will be invoked by the injector as a `config` block. * See: {@link angular.module modules} * @param {Object=} config an object for defining configuration options for the application. The * following keys are supported: * * * `strictDi` - disable automatic function annotation for the application. This is meant to * assist in finding bugs which break minified code. Defaults to `false`. * * @returns {auto.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules, config) { if (!isObject(config)) config = {}; var defaultConfig = { strictDi: false }; config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { var tag = (element[0] === document) ? 'document' : startingTag(element); //Encode angle brackets to prevent input from being sanitized to empty string #8683 throw ngMinErr( 'btstrpd', "App Already Bootstrapped with this Element '{0}'", tag.replace(//,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); if (config.debugInfoEnabled) { // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. modules.push(['$compileProvider', function($compileProvider) { $compileProvider.debugInfoEnabled(true); }]); } modules.unshift('ng'); var injector = createInjector(modules, config.strictDi); injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }] ); return injector; }; var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { config.debugInfoEnabled = true; window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); } if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); angular.resumeBootstrap = function(extraModules) { forEach(extraModules, function(module) { modules.push(module); }); return doBootstrap(); }; if (isFunction(angular.resumeDeferredBootstrap)) { angular.resumeDeferredBootstrap(); } } /** * @ngdoc function * @name angular.reloadWithDebugInfo * @module ng * @description * Use this function to reload the current application with debug information turned on. * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. * * See {@link ng.$compileProvider#debugInfoEnabled} for more. */ function reloadWithDebugInfo() { window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; window.location.reload(); } /** * @name angular.getTestability * @module ng * @description * Get the testability service for the instance of Angular on the given * element. * @param {DOMElement} element DOM element which is the root of angular application. */ function getTestability(rootElement) { var injector = angular.element(rootElement).injector(); if (!injector) { throw ngMinErr('test', 'no injector found for element argument to getTestability'); } return injector.get('$$testability'); } var SNAKE_CASE_REGEXP = /[A-Z]/g; function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } var bindJQueryFired = false; var skipDestroyOnNextJQueryCleanData; function bindJQuery() { var originalCleanData; if (bindJQueryFired) { return; } // bind to jQuery if present; jQuery = window.jQuery; // Use jQuery if it exists with proper functionality, otherwise default to us. // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older // versions. It will not work for sure with jQuery <1.7, though. if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, isolateScope: JQLitePrototype.isolateScope, controller: JQLitePrototype.controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); // All nodes removed from the DOM via various jQuery APIs like .remove() // are passed through jQuery.cleanData. Monkey-patch this method to fire // the $destroy event on all removed nodes. originalCleanData = jQuery.cleanData; jQuery.cleanData = function(elems) { var events; if (!skipDestroyOnNextJQueryCleanData) { for (var i = 0, elem; (elem = elems[i]) != null; i++) { events = jQuery._data(elem, "events"); if (events && events.$destroy) { jQuery(elem).triggerHandler('$destroy'); } } } else { skipDestroyOnNextJQueryCleanData = false; } originalCleanData(elems); }; } else { jqLite = JQLite; } angular.element = jqLite; // Prevent double-proxying. bindJQueryFired = true; } /** * throw error if the argument is falsy. */ function assertArg(arg, name, reason) { if (!arg) { throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } return arg; } function assertArgFn(arg, name, acceptArrayAnnotation) { if (acceptArrayAnnotation && isArray(arg)) { arg = arg[arg.length - 1]; } assertArg(isFunction(arg), name, 'not a function, got ' + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } /** * throw error if the name given is hasOwnProperty * @param {String} name the name to test * @param {String} context the context in which the name is used, such as module or directive */ function assertNotHasOwnProperty(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); } } /** * Return the value accessible from the object by path. Any undefined traversals are ignored * @param {Object} obj starting object * @param {String} path path to traverse * @param {boolean} [bindFnToScope=true] * @returns {Object} value as accessible by path */ //TODO(misko): this function needs to be removed function getter(obj, path, bindFnToScope) { if (!path) return obj; var keys = path.split('.'); var key; var lastInstance = obj; var len = keys.length; for (var i = 0; i < len; i++) { key = keys[i]; if (obj) { obj = (lastInstance = obj)[key]; } } if (!bindFnToScope && isFunction(obj)) { return bind(lastInstance, obj); } return obj; } /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object * @returns {jqLite} jqLite collection containing the nodes */ function getBlockNodes(nodes) { // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original // collection, otherwise update the original collection. var node = nodes[0]; var endNode = nodes[nodes.length - 1]; var blockNodes = [node]; do { node = node.nextSibling; if (!node) break; blockNodes.push(node); } while (node !== endNode); return jqLite(blockNodes); } /** * Creates a new object without a prototype. This object is useful for lookup without having to * guard against prototypically inherited properties via hasOwnProperty. * * Related micro-benchmarks: * - http://jsperf.com/object-create2 * - http://jsperf.com/proto-map-lookup/2 * - http://jsperf.com/for-in-vs-object-keys2 * * @returns {Object} */ function createMap() { return Object.create(null); } var NODE_TYPE_ELEMENT = 1; var NODE_TYPE_TEXT = 3; var NODE_TYPE_COMMENT = 8; var NODE_TYPE_DOCUMENT = 9; var NODE_TYPE_DOCUMENT_FRAGMENT = 11; /** * @ngdoc type * @name angular.Module * @module ng * @description * * Interface for configuring angular {@link angular.module modules}. */ function setupModuleLoader(window) { var $injectorMinErr = minErr('$injector'); var ngMinErr = minErr('ng'); function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } var angular = ensure(window, 'angular', Object); // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap angular.$$minErr = angular.$$minErr || minErr; return ensure(angular, 'module', function() { /** @type {Object.} */ var modules = {}; /** * @ngdoc function * @name angular.module * @module ng * @description * * The `angular.module` is a global place for creating, registering and retrieving Angular * modules. * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * * When passed two or more arguments, a new module is created. If passed only one argument, an * existing module (the name passed as the first argument to `module`) is retrieved. * * * # Module * * A module is a collection of services, directives, controllers, filters, and configuration information. * `angular.module` is used to configure the {@link auto.$injector $injector}. * * ```js * // Create a new module * var myModule = angular.module('myModule', []); * * // register a new service * myModule.value('appName', 'MyCoolApp'); * * // configure existing services inside initialization blocks. * myModule.config(['$locationProvider', function($locationProvider) { * // Configure existing providers * $locationProvider.hashPrefix('!'); * }]); * ``` * * Then you can create an injector and load your modules like this: * * ```js * var injector = angular.injector(['ng', 'myModule']) * ``` * * However it's more likely that you'll just use * {@link ng.directive:ngApp ngApp} or * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. * @param {!Array.=} requires If specified then new module is being created. If * unspecified then the module is being retrieved for further configuration. * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); } }; assertNotHasOwnProperty(name, 'module'); if (requires && modules.hasOwnProperty(name)) { modules[name] = null; } return ensure(modules, name, function() { if (!requires) { throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + "the module name or forgot to load it. If registering a module ensure that you " + "specify the dependencies as the second argument.", name); } /** @type {!Array.>} */ var invokeQueue = []; /** @type {!Array.} */ var configBlocks = []; /** @type {!Array.} */ var runBlocks = []; var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, _configBlocks: configBlocks, _runBlocks: runBlocks, /** * @ngdoc property * @name angular.Module#requires * @module ng * * @description * Holds the list of modules which the injector will load before the current module is * loaded. */ requires: requires, /** * @ngdoc property * @name angular.Module#name * @module ng * * @description * Name of the module. */ name: name, /** * @ngdoc method * @name angular.Module#provider * @module ng * @param {string} name service name * @param {Function} providerType Construction function for creating new instance of the * service. * @description * See {@link auto.$provide#provider $provide.provider()}. */ provider: invokeLater('$provide', 'provider'), /** * @ngdoc method * @name angular.Module#factory * @module ng * @param {string} name service name * @param {Function} providerFunction Function for creating new instance of the service. * @description * See {@link auto.$provide#factory $provide.factory()}. */ factory: invokeLater('$provide', 'factory'), /** * @ngdoc method * @name angular.Module#service * @module ng * @param {string} name service name * @param {Function} constructor A constructor function that will be instantiated. * @description * See {@link auto.$provide#service $provide.service()}. */ service: invokeLater('$provide', 'service'), /** * @ngdoc method * @name angular.Module#value * @module ng * @param {string} name service name * @param {*} object Service instance object. * @description * See {@link auto.$provide#value $provide.value()}. */ value: invokeLater('$provide', 'value'), /** * @ngdoc method * @name angular.Module#constant * @module ng * @param {string} name constant name * @param {*} object Constant value. * @description * Because the constant are fixed, they get applied before other provide methods. * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), /** * @ngdoc method * @name angular.Module#animation * @module ng * @param {string} name animation name * @param {Function} animationFactory Factory function for creating new instance of an * animation. * @description * * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. * * * Defines an animation hook that can be later used with * {@link ngAnimate.$animate $animate} service and directives that use this service. * * ```js * module.animation('.animation-name', function($inject1, $inject2) { * return { * eventName : function(element, done) { * //code to run the animation * //once complete, then run done() * return function cancellationFunction(element) { * //code to cancel the animation * } * } * } * }) * ``` * * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ animation: invokeLater('$animateProvider', 'register'), /** * @ngdoc method * @name angular.Module#filter * @module ng * @param {string} name Filter name. * @param {Function} filterFactory Factory function for creating new instance of filter. * @description * See {@link ng.$filterProvider#register $filterProvider.register()}. */ filter: invokeLater('$filterProvider', 'register'), /** * @ngdoc method * @name angular.Module#controller * @module ng * @param {string|Object} name Controller name, or an object map of controllers where the * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. */ controller: invokeLater('$controllerProvider', 'register'), /** * @ngdoc method * @name angular.Module#directive * @module ng * @param {string|Object} name Directive name, or an object map of directives where the * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ directive: invokeLater('$compileProvider', 'directive'), /** * @ngdoc method * @name angular.Module#config * @module ng * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description * Use this method to register work which needs to be performed on module loading. * For more about how to configure services, see * {@link providers#provider-recipe Provider Recipe}. */ config: config, /** * @ngdoc method * @name angular.Module#run * @module ng * @param {Function} initializationFn Execute this function after injector creation. * Useful for application initialization. * @description * Use this method to register work which should be performed when the injector is done * loading all modules. */ run: function(block) { runBlocks.push(block); return this; } }; if (configFn) { config(configFn); } return moduleInstance; /** * @param {string} provider * @param {string} method * @param {String=} insertMethod * @returns {angular.Module} */ function invokeLater(provider, method, insertMethod, queue) { if (!queue) queue = invokeQueue; return function() { queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } }); }; }); } /* global: toDebugString: true */ function serializeObject(obj) { var seen = []; return JSON.stringify(obj, function(key, val) { val = toJsonReplacer(key, val); if (isObject(val)) { if (seen.indexOf(val) >= 0) return '<>'; seen.push(val); } return val; }); } function toDebugString(obj) { if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); } else if (typeof obj === 'undefined') { return 'undefined'; } else if (typeof obj !== 'string') { return serializeObject(obj); } return obj; } /* global angularModule: true, version: true, $LocaleProvider, $CompileProvider, htmlAnchorDirective, inputDirective, inputDirective, formDirective, scriptDirective, selectDirective, styleDirective, optionDirective, ngBindDirective, ngBindHtmlDirective, ngBindTemplateDirective, ngClassDirective, ngClassEvenDirective, ngClassOddDirective, ngCspDirective, ngCloakDirective, ngControllerDirective, ngFormDirective, ngHideDirective, ngIfDirective, ngIncludeDirective, ngIncludeFillContentDirective, ngInitDirective, ngNonBindableDirective, ngPluralizeDirective, ngRepeatDirective, ngShowDirective, ngStyleDirective, ngSwitchDirective, ngSwitchWhenDirective, ngSwitchDefaultDirective, ngOptionsDirective, ngTranscludeDirective, ngModelDirective, ngListDirective, ngChangeDirective, patternDirective, patternDirective, requiredDirective, requiredDirective, minlengthDirective, minlengthDirective, maxlengthDirective, maxlengthDirective, ngValueDirective, ngModelOptionsDirective, ngAttributeAliasDirectives, ngEventDirectives, $AnchorScrollProvider, $AnimateProvider, $BrowserProvider, $CacheFactoryProvider, $ControllerProvider, $DocumentProvider, $ExceptionHandlerProvider, $FilterProvider, $InterpolateProvider, $IntervalProvider, $HttpProvider, $HttpBackendProvider, $LocationProvider, $LogProvider, $ParseProvider, $RootScopeProvider, $QProvider, $$QProvider, $$SanitizeUriProvider, $SceProvider, $SceDelegateProvider, $SnifferProvider, $TemplateCacheProvider, $TemplateRequestProvider, $$TestabilityProvider, $TimeoutProvider, $$RAFProvider, $$AsyncCallbackProvider, $WindowProvider, $$jqLiteProvider */ /** * @ngdoc object * @name angular.version * @module ng * @description * An object that contains information about the current AngularJS version. This object has the * following properties: * * - `full` – `{string}` – Full version string, such as "0.9.18". * - `major` – `{number}` – Major version number, such as "0". * - `minor` – `{number}` – Minor version number, such as "9". * - `dot` – `{number}` – Dot version number, such as "18". * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { full: '1.3.15', // all of these placeholder strings will be replaced by grunt's major: 1, // package task minor: 3, dot: 15, codeName: 'locality-filtration' }; function publishExternalAPI(angular) { extend(angular, { 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, 'equals': equals, 'element': jqLite, 'forEach': forEach, 'injector': createInjector, 'noop': noop, 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, 'isFunction': isFunction, 'isObject': isObject, 'isNumber': isNumber, 'isElement': isElement, 'isArray': isArray, 'version': version, 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, 'callbacks': {counter: 0}, 'getTestability': getTestability, '$$minErr': minErr, '$$csp': csp, 'reloadWithDebugInfo': reloadWithDebugInfo }); angularModule = setupModuleLoader(window); try { angularModule('ngLocale'); } catch (e) { angularModule('ngLocale', []).provider('$locale', $LocaleProvider); } angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. $provide.provider({ $$sanitizeUri: $$SanitizeUriProvider }); $provide.provider('$compile', $CompileProvider). directive({ a: htmlAnchorDirective, input: inputDirective, textarea: inputDirective, form: formDirective, script: scriptDirective, select: selectDirective, style: styleDirective, option: optionDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, ngClass: ngClassDirective, ngClassEven: ngClassEvenDirective, ngClassOdd: ngClassOddDirective, ngCloak: ngCloakDirective, ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, ngPluralize: ngPluralizeDirective, ngRepeat: ngRepeatDirective, ngShow: ngShowDirective, ngStyle: ngStyleDirective, ngSwitch: ngSwitchDirective, ngSwitchWhen: ngSwitchWhenDirective, ngSwitchDefault: ngSwitchDefaultDirective, ngOptions: ngOptionsDirective, ngTransclude: ngTranscludeDirective, ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, pattern: patternDirective, ngPattern: patternDirective, required: requiredDirective, ngRequired: requiredDirective, minlength: minlengthDirective, ngMinlength: minlengthDirective, maxlength: maxlengthDirective, ngMaxlength: maxlengthDirective, ngValue: ngValueDirective, ngModelOptions: ngModelOptionsDirective }). directive({ ngInclude: ngIncludeFillContentDirective }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, $httpBackend: $HttpBackendProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $templateRequest: $TemplateRequestProvider, $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, $$asyncCallback: $$AsyncCallbackProvider, $$jqLite: $$jqLiteProvider }); } ]); } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Any commits to this file should be reviewed with security in mind. * * Changes to this file can potentially create security vulnerabilities. * * An approval from 2 Core members with history of modifying * * this file is required. * * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* global JQLitePrototype: true, addEventListenerFn: true, removeEventListenerFn: true, BOOLEAN_ATTR: true, ALIASED_ATTR: true, */ ////////////////////////////////// //JQLite ////////////////////////////////// /** * @ngdoc function * @name angular.element * @module ng * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. * * If jQuery is available, `angular.element` is an alias for the * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." * *
    jqLite is a tiny, API-compatible subset of jQuery that allows * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most * commonly needed functionality with the goal of having a very small footprint.
    * * To use jQuery, simply load it before `DOMContentLoaded` event fired. * *
    **Note:** all element references in Angular are always wrapped with jQuery or * jqLite; they are never raw DOM references.
    * * ## Angular's jqLite * jqLite provides only the following jQuery methods: * * - [`addClass()`](http://api.jquery.com/addClass/) * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()` * - [`data()`](http://api.jquery.com/data/) * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name * - [`hasClass()`](http://api.jquery.com/hasClass/) * - [`html()`](http://api.jquery.com/html/) * - [`next()`](http://api.jquery.com/next/) - Does not support selectors * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) * - [`ready()`](http://api.jquery.com/ready/) * - [`remove()`](http://api.jquery.com/remove/) * - [`removeAttr()`](http://api.jquery.com/removeAttr/) * - [`removeClass()`](http://api.jquery.com/removeClass/) * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * * ## jQuery/jqLite Extras * Angular also provides the following additional methods and events to both jQuery and jqLite: * * ### Events * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM * element before it is removed. * * ### Methods * - `controller(name)` - retrieves the controller of the current element or its parent. By default * retrieves controller associated with the `ngController` directive. If `name` is provided as * camelCase directive name, then the controller for this directive will be retrieved (e.g. * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to * be enabled. * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. * @returns {Object} jQuery object. */ JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, jqId = 1, addEventListenerFn = function(element, type, fn) { element.addEventListener(type, fn, false); }, removeEventListenerFn = function(element, type, fn) { element.removeEventListener(type, fn, false); }; /* * !!! This is an undocumented "private" function !!! */ JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; var jqLiteMinErr = minErr('jqLite'); /** * Converts snake_case to camelCase. * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function camelCase(name) { return name. replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { return offset ? letter.toUpperCase() : letter; }). replace(MOZ_HACK_REGEXP, 'Moz$1'); } var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; var TAG_NAME_REGEXP = /<([\w:]+)/; var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; var wrapMap = { 'option': [1, ''], 'thead': [1, '', '
    '], 'col': [2, '', '
    '], 'tr': [2, '', '
    '], 'td': [3, '', '
    '], '_default': [0, "", ""] }; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } function jqLiteAcceptsData(node) { // The window object can accept data but has no nodeType // Otherwise we are only interested in elements (1) and documents (9) var nodeType = node.nodeType; return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; } function jqLiteBuildFragment(html, context) { var tmp, tag, wrap, fragment = context.createDocumentFragment(), nodes = [], i; if (jqLiteIsTextNode(html)) { // Convert non-html into a text node nodes.push(context.createTextNode(html)); } else { // Convert html into DOM nodes tmp = tmp || fragment.appendChild(context.createElement("div")); tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; while (i--) { tmp = tmp.lastChild; } nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; tmp.textContent = ""; } // Remove wrapper from fragment fragment.textContent = ""; fragment.innerHTML = ""; // Clear inner HTML forEach(nodes, function(node) { fragment.appendChild(node); }); return fragment; } function jqLiteParseHTML(html, context) { context = context || document; var parsed; if ((parsed = SINGLE_TAG_REGEXP.exec(html))) { return [context.createElement(parsed[1])]; } if ((parsed = jqLiteBuildFragment(html, context))) { return parsed.childNodes; } return []; } ///////////////////////////////////////////// function JQLite(element) { if (element instanceof JQLite) { return element; } var argIsString; if (isString(element)) { element = trim(element); argIsString = true; } if (!(this instanceof JQLite)) { if (argIsString && element.charAt(0) != '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); } else { jqLiteAddNodes(this, element); } } function jqLiteClone(element) { return element.cloneNode(true); } function jqLiteDealoc(element, onlyDescendants) { if (!onlyDescendants) jqLiteRemoveData(element); if (element.querySelectorAll) { var descendants = element.querySelectorAll('*'); for (var i = 0, l = descendants.length; i < l; i++) { jqLiteRemoveData(descendants[i]); } } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); var expandoStore = jqLiteExpandoStore(element); var events = expandoStore && expandoStore.events; var handle = expandoStore && expandoStore.handle; if (!handle) return; //no listeners registered if (!type) { for (type in events) { if (type !== '$destroy') { removeEventListenerFn(element, type, handle); } delete events[type]; } } else { forEach(type.split(' '), function(type) { if (isDefined(fn)) { var listenerFns = events[type]; arrayRemove(listenerFns || [], fn); if (listenerFns && listenerFns.length > 0) { return; } } removeEventListenerFn(element, type, handle); delete events[type]; }); } } function jqLiteRemoveData(element, name) { var expandoId = element.ng339; var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { delete expandoStore.data[name]; return; } if (expandoStore.handle) { if (expandoStore.events.$destroy) { expandoStore.handle({}, '$destroy'); } jqLiteOff(element); } delete jqCache[expandoId]; element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } } function jqLiteExpandoStore(element, createIfNecessary) { var expandoId = element.ng339, expandoStore = expandoId && jqCache[expandoId]; if (createIfNecessary && !expandoStore) { element.ng339 = expandoId = jqNextId(); expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } return expandoStore; } function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); var massGetter = !key; var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) data[key] = value; } else { if (massGetter) { // data() return data; } else { if (isSimpleGetter) { // data('key') // don't force creation of expandoStore if it doesn't exist yet return data && data[key]; } else { // mass-setter: data({key1: val1, key2: val2}) extend(data, key); } } } } } function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). indexOf(" " + selector + " ") > -1); } function jqLiteRemoveClass(element, cssClasses) { if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { element.setAttribute('class', trim( (" " + (element.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") .replace(" " + trim(cssClass) + " ", " ")) ); }); } } function jqLiteAddClass(element, cssClasses) { if (cssClasses && element.setAttribute) { var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') .replace(/[\n\t]/g, " "); forEach(cssClasses.split(' '), function(cssClass) { cssClass = trim(cssClass); if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { existingClasses += cssClass + ' '; } }); element.setAttribute('class', trim(existingClasses)); } } function jqLiteAddNodes(root, elements) { // THIS CODE IS VERY HOT. Don't make changes without benchmarking. if (elements) { // if a Node (the most common case) if (elements.nodeType) { root[root.length++] = elements; } else { var length = elements.length; // if an Array or NodeList and not a Window if (typeof length === 'number' && elements.window !== elements) { if (length) { for (var i = 0; i < length; i++) { root[root.length++] = elements[i]; } } } else { root[root.length++] = elements; } } } } function jqLiteController(element, name) { return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); } function jqLiteInheritedData(element, name, value) { // if element is the document object work with the html element instead // this makes $(document).scope() possible if (element.nodeType == NODE_TYPE_DOCUMENT) { element = element.documentElement; } var names = isArray(name) ? name : [name]; while (element) { for (var i = 0, ii = names.length; i < ii; i++) { if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } function jqLiteRemove(element, keepData) { if (!keepData) jqLiteDealoc(element); var parent = element.parentNode; if (parent) parent.removeChild(element); } function jqLiteDocumentLoaded(action, win) { win = win || window; if (win.document.readyState === 'complete') { // Force the action to be run async for consistent behaviour // from the action's point of view // i.e. it will definitely not be in a $apply win.setTimeout(action); } else { // No need to unbind this handler as load is only ever called once jqLite(win).on('load', action); } } ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { ready: function(fn) { var fired = false; function trigger() { if (fired) return; fired = true; fn(); } // check if document is already loaded if (document.readyState === 'complete') { setTimeout(trigger); } else { this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 // we can not use jqLite since we are not done loading and jQuery could be loaded later. // jshint -W064 JQLite(window).on('load', trigger); // fallback to window.onload for others // jshint +W064 } }, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); return '[' + value.join(', ') + ']'; }, eq: function(index) { return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); }, length: 0, push: push, sort: [].sort, splice: [].splice }; ////////////////////////////////////////// // Functions iterating getter/setters. // these functions return self on setter and // value on get. ////////////////////////////////////////// var BOOLEAN_ATTR = {}; forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { BOOLEAN_ATTR[lowercase(value)] = value; }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { BOOLEAN_ELEMENTS[value] = true; }); var ALIASED_ATTR = { 'ngMinlength': 'minlength', 'ngMaxlength': 'maxlength', 'ngMin': 'min', 'ngMax': 'max', 'ngPattern': 'pattern' }; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } function getAliasedAttrName(element, name) { var nodeName = element.nodeName; return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, removeData: jqLiteRemoveData }, function(fn, name) { JQLite[name] = fn; }); forEach({ data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, injector: function(element) { return jqLiteInheritedData(element, '$injector'); }, removeAttr: function(element, name) { element.removeAttribute(name); }, hasClass: jqLiteHasClass, css: function(element, name, value) { name = camelCase(name); if (isDefined(value)) { element.style[name] = value; } else { return element.style[name]; } }, attr: function(element, name, value) { var lowercasedName = lowercase(name); if (BOOLEAN_ATTR[lowercasedName]) { if (isDefined(value)) { if (!!value) { element[name] = true; element.setAttribute(name, lowercasedName); } else { element[name] = false; element.removeAttribute(lowercasedName); } } else { return (element[name] || (element.attributes.getNamedItem(name) || noop).specified) ? lowercasedName : undefined; } } else if (isDefined(value)) { element.setAttribute(name, value); } else if (element.getAttribute) { // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code // some elements (e.g. Document) don't have get attribute, so return undefined var ret = element.getAttribute(name, 2); // normalize non-existing attributes to undefined (as jQuery) return ret === null ? undefined : ret; } }, prop: function(element, name, value) { if (isDefined(value)) { element[name] = value; } else { return element[name]; } }, text: (function() { getText.$dv = ''; return getText; function getText(element, value) { if (isUndefined(value)) { var nodeType = element.nodeType; return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { if (element.multiple && nodeName_(element) === 'select') { var result = []; forEach(element.options, function(option) { if (option.selected) { result.push(option.value || option.text); } }); return result.length === 0 ? null : result; } return element.value; } element.value = value; }, html: function(element, value) { if (isUndefined(value)) { return element.innerHTML; } jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty }, function(fn, name) { /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; var nodeCount = this.length; // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. // jqLiteEmpty takes no arguments but is a setter. if (fn !== jqLiteEmpty && (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values for (i = 0; i < nodeCount; i++) { if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); } else { for (key in arg1) { fn(this[i], key, arg1[key]); } } } // return self for chaining return this; } else { // we are a read, so read the first child. // TODO: do we still need this? var value = fn.$dv; // Only if we have $dv do we iterate over all, otherwise it is just the first element. var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; } return value; } } else { // we are a write, so apply to all children for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining return this; } }; }); function createEventHandler(element, events) { var eventHandler = function(event, type) { // jQuery specific api event.isDefaultPrevented = function() { return event.defaultPrevented; }; var eventFns = events[type || event.type]; var eventFnsLength = eventFns ? eventFns.length : 0; if (!eventFnsLength) return; if (isUndefined(event.immediatePropagationStopped)) { var originalStopImmediatePropagation = event.stopImmediatePropagation; event.stopImmediatePropagation = function() { event.immediatePropagationStopped = true; if (event.stopPropagation) { event.stopPropagation(); } if (originalStopImmediatePropagation) { originalStopImmediatePropagation.call(event); } }; } event.isImmediatePropagationStopped = function() { return event.immediatePropagationStopped === true; }; // Copy event handlers in case event handlers array is modified during execution. if ((eventFnsLength > 1)) { eventFns = shallowCopy(eventFns); } for (var i = 0; i < eventFnsLength; i++) { if (!event.isImmediatePropagationStopped()) { eventFns[i].call(element, event); } } }; // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all // events on `element` eventHandler.elem = element; return eventHandler; } ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single // selector. ////////////////////////////////////////// forEach({ removeData: jqLiteRemoveData, on: function jqLiteOn(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); // Do not add event handlers to non-elements because they will not be cleaned up. if (!jqLiteAcceptsData(element)) { return; } var expandoStore = jqLiteExpandoStore(element, true); var events = expandoStore.events; var handle = expandoStore.handle; if (!handle) { handle = expandoStore.handle = createEventHandler(element, events); } // http://jsperf.com/string-indexof-vs-split var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; var i = types.length; while (i--) { type = types[i]; var eventFns = events[type]; if (!eventFns) { events[type] = []; if (type === 'mouseenter' || type === 'mouseleave') { // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if (!related || (related !== target && !target.contains(related))) { handle(event, type); } }); } else { if (type !== '$destroy') { addEventListenerFn(element, type, handle); } } eventFns = events[type]; } eventFns.push(fn); } }, off: jqLiteOff, one: function(element, type, fn) { element = jqLite(element); //add the listener twice so that when it is called //you can remove the original function and still be //able to call element.off(ev, fn) normally element.on(type, function onFn() { element.off(type, fn); element.off(type, onFn); }); element.on(type, fn); }, replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; jqLiteDealoc(element); forEach(new JQLite(replaceNode), function(node) { if (index) { parent.insertBefore(node, index.nextSibling); } else { parent.replaceChild(node, element); } index = node; }); }, children: function(element) { var children = []; forEach(element.childNodes, function(element) { if (element.nodeType === NODE_TYPE_ELEMENT) children.push(element); }); return children; }, contents: function(element) { return element.contentDocument || element.childNodes || []; }, append: function(element, node) { var nodeType = element.nodeType; if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; node = new JQLite(node); for (var i = 0, ii = node.length; i < ii; i++) { var child = node[i]; element.appendChild(child); } }, prepend: function(element, node) { if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; forEach(new JQLite(node), function(child) { element.insertBefore(child, index); }); } }, wrap: function(element, wrapNode) { wrapNode = jqLite(wrapNode).eq(0).clone()[0]; var parent = element.parentNode; if (parent) { parent.replaceChild(wrapNode, element); } wrapNode.appendChild(element); }, remove: jqLiteRemove, detach: function(element) { jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; newElement = new JQLite(newElement); for (var i = 0, ii = newElement.length; i < ii; i++) { var node = newElement[i]; parent.insertBefore(node, index.nextSibling); index = node; } }, addClass: jqLiteAddClass, removeClass: jqLiteRemoveClass, toggleClass: function(element, selector, condition) { if (selector) { forEach(selector.split(' '), function(className) { var classCondition = condition; if (isUndefined(classCondition)) { classCondition = !jqLiteHasClass(element, className); } (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); }); } }, parent: function(element) { var parent = element.parentNode; return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { return element.nextElementSibling; }, find: function(element, selector) { if (element.getElementsByTagName) { return element.getElementsByTagName(selector); } else { return []; } }, clone: jqLiteClone, triggerHandler: function(element, event, extraParameters) { var dummyEvent, eventFnsCopy, handlerArgs; var eventName = event.type || event; var expandoStore = jqLiteExpandoStore(element); var events = expandoStore && expandoStore.events; var eventFns = events && events[eventName]; if (eventFns) { // Create a dummy event to pass to the handlers dummyEvent = { preventDefault: function() { this.defaultPrevented = true; }, isDefaultPrevented: function() { return this.defaultPrevented === true; }, stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, stopPropagation: noop, type: eventName, target: element }; // If a custom event was provided then extend our dummy event with it if (event.type) { dummyEvent = extend(dummyEvent, event); } // Copy event handlers in case event handlers array is modified during execution. eventFnsCopy = shallowCopy(eventFns); handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; forEach(eventFnsCopy, function(fn) { if (!dummyEvent.isImmediatePropagationStopped()) { fn.apply(element, handlerArgs); } }); } } }, function(fn, name) { /** * chaining functions */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; for (var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { // any function which returns a value needs to be wrapped value = jqLite(value); } } else { jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); } } return isDefined(value) ? value : this; }; // bind legacy bind/unbind to on/off JQLite.prototype.bind = JQLite.prototype.on; JQLite.prototype.unbind = JQLite.prototype.off; }); // Provider for private $$jqLite service function $$jqLiteProvider() { this.$get = function $$jqLite() { return extend(JQLite, { hasClass: function(node, classes) { if (node.attr) node = node[0]; return jqLiteHasClass(node, classes); }, addClass: function(node, classes) { if (node.attr) node = node[0]; return jqLiteAddClass(node, classes); }, removeClass: function(node, classes) { if (node.attr) node = node[0]; return jqLiteRemoveClass(node, classes); } }); }; } /** * Computes a hash of an 'obj'. * Hash of a: * string is string * number is number as string * object is either result of calling $$hashKey function on the object or uniquely generated id, * that is also assigned to the $$hashKey property of the object. * * @param obj * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ function hashKey(obj, nextUidFn) { var key = obj && obj.$$hashKey; if (key) { if (typeof key === 'function') { key = obj.$$hashKey(); } return key; } var objType = typeof obj; if (objType == 'function' || (objType == 'object' && obj !== null)) { key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { key = objType + ':' + obj; } return key; } /** * HashMap which can use objects as keys */ function HashMap(array, isolatedUid) { if (isolatedUid) { var uid = 0; this.nextUid = function() { return ++uid; }; } forEach(array, this.put, this); } HashMap.prototype = { /** * Store key value pair * @param key key to store can be any type * @param value value to store can be any type */ put: function(key, value) { this[hashKey(key, this.nextUid)] = value; }, /** * @param key * @returns {Object} the value for the key */ get: function(key) { return this[hashKey(key, this.nextUid)]; }, /** * Remove the key/value pair * @param key */ remove: function(key) { var value = this[key = hashKey(key, this.nextUid)]; delete this[key]; return value; } }; /** * @ngdoc function * @module ng * @name angular.injector * @kind function * * @description * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * * @param {Array.} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which * disallows argument name annotation inference. * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage * ```js * // create an injector * var $injector = angular.injector(['ng']); * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection * $injector.invoke(function($rootScope, $compile, $document) { * $compile($document)($rootScope); * $rootScope.$digest(); * }); * ``` * * Sometimes you want to get access to the injector of a currently running Angular app * from outside Angular. Perhaps, you want to inject and compile some markup after the * application has been bootstrapped. You can do this using the extra `injector()` added * to JQuery/jqLite elements. See {@link angular.element}. * * *This is fairly rare but could be the case if a third party library is injecting the * markup.* * * In the following example a new block of HTML containing a `ng-controller` * directive is added to the end of the document body by JQuery. We then compile and link * it into the current AngularJS scope. * * ```js * var $div = $('
    {{content.label}}
    '); * $(document.body).append($div); * * angular.element(document).injector().invoke(function($compile) { * var scope = angular.element($div).scope(); * $compile($div)(scope); * }); * ``` */ /** * @ngdoc module * @name auto * @description * * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); function anonFn(fn) { // For anonymous functions, showing at the very least the function signature can help in // debugging. var fnText = fn.toString().replace(STRIP_COMMENTS, ''), args = fnText.match(FN_ARGS); if (args) { return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; } return 'fn'; } function annotate(fn, strictDi, name) { var $inject, fnText, argDecl, last; if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { if (strictDi) { if (!isString(name) || !name) { name = fn.name || anonFn(fn); } throw $injectorMinErr('strictdi', '{0} is not using explicit annotation and cannot be invoked in strict mode', name); } fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); } fn.$inject = $inject; } } else if (isArray(fn)) { last = fn.length - 1; assertArgFn(fn[last], 'fn'); $inject = fn.slice(0, last); } else { assertArgFn(fn, 'fn', true); } return $inject; } /////////////////////////////////////// /** * @ngdoc service * @name $injector * * @description * * `$injector` is used to retrieve object instances as defined by * {@link auto.$provide provider}, instantiate types, invoke methods, * and load modules. * * The following always holds true: * * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); * expect($injector.invoke(function($injector) { * return $injector; * })).toBe($injector); * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. * * ```js * // inferred (only works if code not minified/obfuscated) * $injector.invoke(function(serviceA){}); * * // annotated * function explicit(serviceA) {}; * explicit.$inject = ['serviceA']; * $injector.invoke(explicit); * * // inline * $injector.invoke(['serviceA', function(serviceA){}]); * ``` * * ## Inference * * In JavaScript calling `toString()` on a function returns the function definition. The definition * can then be parsed and the function arguments can be extracted. This method of discovering * annotations is disallowed when the injector is in strict mode. * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the * argument names. * * ## `$inject` Annotation * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. */ /** * @ngdoc method * @name $injector#get * * @description * Return an instance of the service. * * @param {string} name The name of the instance to retrieve. * @param {string} caller An optional string to provide the origin of the function call for error messages. * @return {*} The instance. */ /** * @ngdoc method * @name $injector#invoke * * @description * Invoke the method and supply the method arguments from the `$injector`. * * @param {!Function} fn The function to invoke. Function parameters are injected according to the * {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. * @returns {*} the value returned by the invoked `fn` function. */ /** * @ngdoc method * @name $injector#has * * @description * Allows the user to query if the particular service exists. * * @param {string} name Name of the service to query. * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method * @name $injector#instantiate * @description * Create a new instance of JS type. The method takes a constructor function, invokes the new * operator, and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * * @param {Function} Type Annotated constructor function. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. * @returns {Object} new instance of `Type`. */ /** * @ngdoc method * @name $injector#annotate * * @description * Returns an array of service names which the function is requesting for injection. This API is * used by the injector to determine which services need to be injected into the function when the * function is invoked. There are three ways in which the function can be annotated with the needed * dependencies. * * # Argument names * * The simplest form is to extract the dependencies from the arguments of the function. This is done * by converting the function into a string using `toString()` method and extracting the argument * names. * ```js * // Given * function MyController($scope, $route) { * // ... * } * * // Then * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * * You can disallow this method by using strict injection mode. * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. * * # The `$inject` property * * If a function has an `$inject` property and its value is an array of strings, then the strings * represent names of services to be injected into the function. * ```js * // Given * var MyController = function(obfuscatedScope, obfuscatedRoute) { * // ... * } * // Define function dependencies * MyController['$inject'] = ['$scope', '$route']; * * // Then * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * * # The array notation * * It is often desirable to inline Injected functions and that's when setting the `$inject` property * is very inconvenient. In these situations using the array notation to specify the dependencies in * a way that survives minification is a better choice: * * ```js * // We wish to write this (not minification / obfuscation safe) * injector.invoke(function($compile, $rootScope) { * // ... * }); * * // We are forced to write break inlining * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { * // ... * }; * tmpFn.$inject = ['$compile', '$rootScope']; * injector.invoke(tmpFn); * * // To better support inline function the inline annotation is supported * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { * // ... * }]); * * // Therefore * expect(injector.annotate( * ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) * ).toEqual(['$compile', '$rootScope']); * ``` * * @param {Function|Array.} fn Function for which dependent service names need to * be retrieved as described above. * * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. * * @returns {Array.} The names of the services which the function requires. */ /** * @ngdoc service * @name $provide * * @description * * The {@link auto.$provide $provide} service has a number of methods for registering components * with the {@link auto.$injector $injector}. Many of these functions are also exposed on * {@link angular.Module}. * * An Angular **service** is a singleton object created by a **service factory**. These **service * factories** are functions which, in turn, are created by a **service provider**. * The **service providers** are constructor functions. When instantiated they must contain a * property called `$get`, which holds the **service factory** function. * * When you request a service, the {@link auto.$injector $injector} is responsible for finding the * correct **service provider**, instantiating it and then calling its `$get` **service factory** * function to get the instance of the **service**. * * Often services have no configuration options and there is no need to add methods to the service * provider. The provider will be no more than a constructor function with a `$get` property. For * these cases the {@link auto.$provide $provide} service has additional helper methods to register * services without specifying a provider. * * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the * {@link auto.$injector $injector} * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by * providers and services. * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by * services, not providers. * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, * that will be wrapped in a **service provider** object, whose `$get` property will contain the * given factory function. * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` * that will be wrapped in a **service provider** object, whose `$get` property will instantiate * a new object using the given constructor function. * * See the individual methods for more information and examples. */ /** * @ngdoc method * @name $provide#provider * @description * * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions * are constructor functions, whose instances are responsible for "providing" a factory for a * service. * * Service provider names start with the name of the service they provide followed by `Provider`. * For example, the {@link ng.$log $log} service has a provider called * {@link ng.$logProvider $logProvider}. * * Service provider objects can have additional methods which allow configuration of the provider * and its service. Importantly, you can configure what kind of service is created by the `$get` * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a * method {@link ng.$logProvider#debugEnabled debugEnabled} * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the * console or not. * * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. * @param {(Object|function())} provider If the provider is: * * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. * - `Constructor`: a new instance of the provider will be created using * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance * @example * * The following example shows how to create a simple event tracking service and register it using * {@link auto.$provide#provider $provide.provider()}. * * ```js * // Define the eventTracker provider * function EventTrackerProvider() { * var trackingUrl = '/track'; * * // A provider method for configuring where the tracked events should been saved * this.setTrackingUrl = function(url) { * trackingUrl = url; * }; * * // The service factory function * this.$get = ['$http', function($http) { * var trackedEvents = {}; * return { * // Call this to track an event * event: function(event) { * var count = trackedEvents[event] || 0; * count += 1; * trackedEvents[event] = count; * return count; * }, * // Call this to save the tracked events to the trackingUrl * save: function() { * $http.post(trackingUrl, trackedEvents); * } * }; * }]; * } * * describe('eventTracker', function() { * var postSpy; * * beforeEach(module(function($provide) { * // Register the eventTracker provider * $provide.provider('eventTracker', EventTrackerProvider); * })); * * beforeEach(module(function(eventTrackerProvider) { * // Configure eventTracker provider * eventTrackerProvider.setTrackingUrl('/custom-track'); * })); * * it('tracks events', inject(function(eventTracker) { * expect(eventTracker.event('login')).toEqual(1); * expect(eventTracker.event('login')).toEqual(2); * })); * * it('saves to the tracking url', inject(function(eventTracker, $http) { * postSpy = spyOn($http, 'post'); * eventTracker.event('login'); * eventTracker.save(); * expect(postSpy).toHaveBeenCalled(); * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); * })); * }); * ``` */ /** * @ngdoc method * @name $provide#factory * @description * * Register a **service factory**, which will be called to return the service instance. * This is short for registering a service where its provider consists of only a `$get` property, * which is the given service factory function. * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to * configure your service in a provider. * * @param {string} name The name of the instance. * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand * for `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance * * @example * Here is an example of registering a service * ```js * $provide.factory('ping', ['$http', function($http) { * return function ping() { * return $http.send('/ping'); * }; * }]); * ``` * You would then inject and use this service like this: * ```js * someModule.controller('Ctrl', ['ping', function(ping) { * ping(); * }]); * ``` */ /** * @ngdoc method * @name $provide#service * @description * * Register a **service constructor**, which will be invoked with `new` to create the service * instance. * This is short for registering a service where its provider's `$get` property is the service * constructor function that will be used to instantiate the service instance. * * You should use {@link auto.$provide#service $provide.service(class)} if you define your service * as a type/class. * * @param {string} name The name of the instance. * @param {Function} constructor A class (constructor function) that will be instantiated. * @returns {Object} registered provider instance * * @example * Here is an example of registering a service using * {@link auto.$provide#service $provide.service(class)}. * ```js * var Ping = function($http) { * this.$http = $http; * }; * * Ping.$inject = ['$http']; * * Ping.prototype.send = function() { * return this.$http.get('/ping'); * }; * $provide.service('ping', Ping); * ``` * You would then inject and use this service like this: * ```js * someModule.controller('Ctrl', ['ping', function(ping) { * ping.send(); * }]); * ``` */ /** * @ngdoc method * @name $provide#value * @description * * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value * service**. * * Value services are similar to constant services, except that they cannot be injected into a * module configuration function (see {@link angular.Module#config}) but they can be overridden by * an Angular * {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. * @returns {Object} registered provider instance * * @example * Here are some examples of creating value services. * ```js * $provide.value('ADMIN_USER', 'admin'); * * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); * * $provide.value('halfOf', function(value) { * return value / 2; * }); * ``` */ /** * @ngdoc method * @name $provide#constant * @description * * Register a **constant service**, such as a string, a number, an array, an object or a function, * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be * injected into a module configuration function (see {@link angular.Module#config}) and it cannot * be overridden by an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. * @returns {Object} registered instance * * @example * Here a some examples of creating constants: * ```js * $provide.constant('SHARD_HEIGHT', 306); * * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); * * $provide.constant('double', function(value) { * return value * 2; * }); * ``` */ /** * @ngdoc method * @name $provide#decorator * @description * * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator * intercepts the creation of a service, allowing it to override or modify the behaviour of the * service. The object returned by the decorator may be the original service, or a new service * object which replaces or wraps and delegates to the original service. * * @param {string} name The name of the service to decorate. * @param {function()} decorator This function will be invoked when the service needs to be * instantiated and should return the decorated service instance. The function is called using * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. * Local injection arguments: * * * `$delegate` - The original service instance, which can be monkey patched, configured, * decorated or delegated to. * * @example * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting * calls to {@link ng.$log#error $log.warn()}. * ```js * $provide.decorator('$log', ['$delegate', function($delegate) { * $delegate.warn = $delegate.error; * return $delegate; * }]); * ``` */ function createInjector(modulesToLoad, strictDi) { strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], loadedModules = new HashMap([], true), providerCache = { $provide: { provider: supportObject(provider), factory: supportObject(factory), service: supportObject(service), value: supportObject(value), constant: supportObject(constant), decorator: decorator } }, providerInjector = (providerCache.$injector = createInternalInjector(providerCache, function(serviceName, caller) { if (angular.isString(caller)) { path.push(caller); } throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(serviceName, caller) { var provider = providerInjector.get(serviceName + providerSuffix, caller); return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); })); forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); return instanceInjector; //////////////////////////////////// // $provider //////////////////////////////////// function supportObject(delegate) { return function(key, value) { if (isObject(key)) { forEach(key, reverseParams(delegate)); } else { return delegate(key, value); } }; } function provider(name, provider_) { assertNotHasOwnProperty(name, 'service'); if (isFunction(provider_) || isArray(provider_)) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } function enforceReturnValue(name, factory) { return function enforcedReturnValue() { var result = instanceInjector.invoke(factory, this); if (isUndefined(result)) { throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); } return result; }; } function factory(name, factoryFn, enforce) { return provider(name, { $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn }); } function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); } function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; } function decorator(serviceName, decorFn) { var origProvider = providerInjector.get(serviceName + providerSuffix), orig$get = origProvider.$get; origProvider.$get = function() { var origInstance = instanceInjector.invoke(orig$get, origProvider); return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); }; } //////////////////////////////////// // Module Loading //////////////////////////////////// function loadModules(modulesToLoad) { var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); function runInvokeQueue(queue) { var i, ii; for (i = 0, ii = queue.length; i < ii; i++) { var invokeArgs = queue[i], provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } } try { if (isString(module)) { moduleFn = angularModule(module); runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { runBlocks.push(providerInjector.invoke(module)); } else { assertArgFn(module, 'module'); } } catch (e) { if (isArray(module)) { module = module[module.length - 1]; } if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { // Safari & FF's stack traces don't contain error.message content // unlike those of Chrome and IE // So if stack doesn't contain message, we create a new string that contains both. // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. /* jshint -W022 */ e = e.message + '\n' + e.stack; } throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", module, e.stack || e.message || e); } }); return runBlocks; } //////////////////////////////////// // internal Injector //////////////////////////////////// function createInternalInjector(cache, factory) { function getService(serviceName, caller) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { throw $injectorMinErr('cdep', 'Circular dependency found: {0}', serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName, caller); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; } throw err; } finally { path.shift(); } } } function invoke(fn, self, locals, serviceName) { if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = createInjector.$$annotate(fn, strictDi, serviceName), length, i, key; for (i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key, serviceName) ); } if (isArray(fn)) { fn = fn[length]; } // http://jsperf.com/angularjs-invoke-apply-vs-switch // #5388 return fn.apply(self, args); } function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); // Object creation: http://jsperf.com/create-constructor/2 var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null); var returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } return { invoke: invoke, instantiate: instantiate, get: getService, annotate: createInjector.$$annotate, has: function(name) { return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); } }; } } createInjector.$$annotate = annotate; /** * @ngdoc provider * @name $anchorScrollProvider * * @description * Use `$anchorScrollProvider` to disable automatic scrolling whenever * {@link ng.$location#hash $location.hash()} changes. */ function $AnchorScrollProvider() { var autoScrollingEnabled = true; /** * @ngdoc method * @name $anchorScrollProvider#disableAutoScrolling * * @description * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
    * Use this method to disable automatic scrolling. * * If automatic scrolling is disabled, one must explicitly call * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the * current hash. */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; /** * @ngdoc service * @name $anchorScroll * @kind function * @requires $window * @requires $location * @requires $rootScope * * @description * When called, it checks the current value of {@link ng.$location#hash $location.hash()} and * scrolls to the related element, according to the rules specified in the * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). * * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to * match any anchor whenever it changes. This can be disabled by calling * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. * * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a * vertical scroll-offset (either fixed or dynamic). * * @property {(number|function|jqLite)} yOffset * If set, specifies a vertical scroll-offset. This is often useful when there are fixed * positioned elements at the top of the page, such as navbars, headers etc. * * `yOffset` can be specified in various ways: * - **number**: A fixed number of pixels to be used as offset.

    * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return * a number representing the offset (in pixels).

    * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from * the top of the page to the element's bottom will be used as offset.
    * **Note**: The element will be taken into account only as long as its `position` is set to * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust * their height and/or positioning according to the viewport's size. * *
    *
    * In order for `yOffset` to work properly, scrolling should take place on the document's root and * not some child element. *
    * * @example
    Go to bottom You're at the bottom!
    angular.module('anchorScrollExample', []) .controller('ScrollController', ['$scope', '$location', '$anchorScroll', function ($scope, $location, $anchorScroll) { $scope.gotoBottom = function() { // set the location.hash to the id of // the element you wish to scroll to. $location.hash('bottom'); // call $anchorScroll() $anchorScroll(); }; }]); #scrollArea { height: 280px; overflow: auto; } #bottom { display: block; margin-top: 2000px; }
    * *
    * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. * * @example
    Anchor {{x}} of 5
    angular.module('anchorScrollOffsetExample', []) .run(['$anchorScroll', function($anchorScroll) { $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels }]) .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', function ($anchorScroll, $location, $scope) { $scope.gotoAnchor = function(x) { var newHash = 'anchor' + x; if ($location.hash() !== newHash) { // set the $location.hash to `newHash` and // $anchorScroll will automatically scroll to it $location.hash('anchor' + x); } else { // call $anchorScroll() explicitly, // since $location.hash hasn't changed $anchorScroll(); } }; } ]); body { padding-top: 50px; } .anchor { border: 2px dashed DarkOrchid; padding: 10px 10px 200px 10px; } .fixed-header { background-color: rgba(0, 0, 0, 0.2); height: 50px; position: fixed; top: 0; left: 0; right: 0; } .fixed-header > a { display: inline-block; margin: 5px 15px; }
    */ this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { var document = $window.document; // Helper function to get first anchor from a NodeList // (using `Array#some()` instead of `angular#forEach()` since it's more performant // and working in all supported browsers.) function getFirstAnchor(list) { var result = null; Array.prototype.some.call(list, function(element) { if (nodeName_(element) === 'a') { result = element; return true; } }); return result; } function getYOffset() { var offset = scroll.yOffset; if (isFunction(offset)) { offset = offset(); } else if (isElement(offset)) { var elem = offset[0]; var style = $window.getComputedStyle(elem); if (style.position !== 'fixed') { offset = 0; } else { offset = elem.getBoundingClientRect().bottom; } } else if (!isNumber(offset)) { offset = 0; } return offset; } function scrollTo(elem) { if (elem) { elem.scrollIntoView(); var offset = getYOffset(); if (offset) { // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the // top of the viewport. // // IF the number of pixels from the top of `elem` to the end of the page's content is less // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some // way down the page. // // This is often the case for elements near the bottom of the page. // // In such cases we do not need to scroll the whole `offset` up, just the difference between // the top of the element and the offset, which is enough to align the top of `elem` at the // desired position. var elemTop = elem.getBoundingClientRect().top; $window.scrollBy(0, elemTop - offset); } } else { $window.scrollTo(0, 0); } } function scroll() { var hash = $location.hash(), elm; // empty hash, scroll to the top of the page if (!hash) scrollTo(null); // element with given id else if ((elm = document.getElementById(hash))) scrollTo(elm); // first anchor with given name :-D else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); // no element and hash == 'top', scroll to the top of the page else if (hash === 'top') scrollTo(null); } // does not scroll when user clicks on anchor link that is currently on // (no url change, no $location.hash() change), browser native does scroll if (autoScrollingEnabled) { $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, function autoScrollWatchAction(newVal, oldVal) { // skip the initial scroll if $location.hash is empty if (newVal === oldVal && newVal === '') return; jqLiteDocumentLoaded(function() { $rootScope.$evalAsync(scroll); }); }); } return scroll; }]; } var $animateMinErr = minErr('$animate'); /** * @ngdoc provider * @name $animateProvider * * @description * Default implementation of $animate that doesn't perform any animations, instead just * synchronously performs DOM * updates and calls done() callbacks. * * In order to enable animations the ngAnimate module has to be loaded. * * To see the functional implementation check out src/ngAnimate/animate.js */ var $AnimateProvider = ['$provide', function($provide) { this.$$selectors = {}; /** * @ngdoc method * @name $animateProvider#register * * @description * Registers a new injectable animation factory function. The factory function produces the * animation object which contains callback functions for each event that is expected to be * animated. * * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` * must be called once the element animation is complete. If a function is returned then the * animation service will use this function to cancel the animation whenever a cancel event is * triggered. * * * ```js * return { * eventFn : function(element, done) { * //code to run the animation * //once complete, then run done() * return function cancellationFunction() { * //code to cancel the animation * } * } * } * ``` * * @param {string} name The name of the animation. * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { var key = name + '-animation'; if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name); this.$$selectors[name.substr(1)] = key; $provide.factory(key, factory); }; /** * @ngdoc method * @name $animateProvider#classNameFilter * * @description * Sets and/or returns the CSS class regular expression that is checked when performing * an animation. Upon bootstrap the classNameFilter value is not set at all and will * therefore enable $animate to attempt to perform an animation on any element. * When setting the classNameFilter value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. * @param {RegExp=} expression The className expression which will be checked against all animations * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { if (arguments.length === 1) { this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; } return this.$$classNameFilter; }; this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { var currentDefer; function runAnimationPostDigest(fn) { var cancelFn, defer = $$q.defer(); defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { cancelFn && cancelFn(); }; $rootScope.$$postDigest(function ngAnimatePostDigest() { cancelFn = fn(function ngAnimateNotifyComplete() { defer.resolve(); }); }); return defer.promise; } function resolveElementClasses(element, classes) { var toAdd = [], toRemove = []; var hasClasses = createMap(); forEach((element.attr('class') || '').split(/\s+/), function(className) { hasClasses[className] = true; }); forEach(classes, function(status, className) { var hasClass = hasClasses[className]; // If the most recent class manipulation (via $animate) was to remove the class, and the // element currently has the class, the class is scheduled for removal. Otherwise, if // the most recent class manipulation (via $animate) was to add the class, and the // element does not currently have the class, the class is scheduled to be added. if (status === false && hasClass) { toRemove.push(className); } else if (status === true && !hasClass) { toAdd.push(className); } }); return (toAdd.length + toRemove.length) > 0 && [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null]; } function cachedClassManipulation(cache, classes, op) { for (var i=0, ii = classes.length; i < ii; ++i) { var className = classes[i]; cache[className] = op; } } function asyncPromise() { // only serve one instance of a promise in order to save CPU cycles if (!currentDefer) { currentDefer = $$q.defer(); $$asyncCallback(function() { currentDefer.resolve(); currentDefer = null; }); } return currentDefer.promise; } function applyStyles(element, options) { if (angular.isObject(options)) { var styles = extend(options.from || {}, options.to || {}); element.css(styles); } } /** * * @ngdoc service * @name $animate * @description The $animate service provides rudimentary DOM manipulation functions to * insert, remove and move elements within the DOM, as well as adding and removing classes. * This service is the core service used by the ngAnimate $animator service which provides * high-level animation hooks for CSS and JavaScript. * * $animate is available in the AngularJS core, however, the ngAnimate module must be included * to enable full out animation support. Otherwise, $animate will only perform simple DOM * manipulation operations. * * To learn more about enabling animation support, click here to visit the {@link ngAnimate * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service * page}. */ return { animate: function(element, from, to) { applyStyles(element, { from: from, to: to }); return asyncPromise(); }, /** * * @ngdoc method * @name $animate#enter * @kind function * @description Inserts the element into the DOM either after the `after` element or * as the first child within the `parent` element. When the function is called a promise * is returned that will be resolved at a later time. * @param {DOMElement} element the element which will be inserted into the DOM * @param {DOMElement} parent the parent element which will append the element as * a child (if the after element is not present) * @param {DOMElement} after the sibling element which will append the element * after itself * @param {object=} options an optional collection of styles that will be applied to the element. * @return {Promise} the animation callback promise */ enter: function(element, parent, after, options) { applyStyles(element, options); after ? after.after(element) : parent.prepend(element); return asyncPromise(); }, /** * * @ngdoc method * @name $animate#leave * @kind function * @description Removes the element from the DOM. When the function is called a promise * is returned that will be resolved at a later time. * @param {DOMElement} element the element which will be removed from the DOM * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ leave: function(element, options) { applyStyles(element, options); element.remove(); return asyncPromise(); }, /** * * @ngdoc method * @name $animate#move * @kind function * @description Moves the position of the provided element within the DOM to be placed * either after the `after` element or inside of the `parent` element. When the function * is called a promise is returned that will be resolved at a later time. * * @param {DOMElement} element the element which will be moved around within the * DOM * @param {DOMElement} parent the parent element where the element will be * inserted into (if the after element is not present) * @param {DOMElement} after the sibling element where the element will be * positioned next to * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ move: function(element, parent, after, options) { // Do not remove element before insert. Removing will cause data associated with the // element to be dropped. Insert will implicitly do the remove. return this.enter(element, parent, after, options); }, /** * * @ngdoc method * @name $animate#addClass * @kind function * @description Adds the provided className CSS class value to the provided element. * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have the className value * added to it * @param {string} className the CSS class which will be added to the element * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ addClass: function(element, className, options) { return this.setClass(element, className, [], options); }, $$addClassImmediately: function(element, className, options) { element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') : className; forEach(element, function(element) { jqLiteAddClass(element, className); }); applyStyles(element, options); return asyncPromise(); }, /** * * @ngdoc method * @name $animate#removeClass * @kind function * @description Removes the provided className CSS class value from the provided element. * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have the className value * removed from it * @param {string} className the CSS class which will be removed from the element * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ removeClass: function(element, className, options) { return this.setClass(element, [], className, options); }, $$removeClassImmediately: function(element, className, options) { element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') : className; forEach(element, function(element) { jqLiteRemoveClass(element, className); }); applyStyles(element, options); return asyncPromise(); }, /** * * @ngdoc method * @name $animate#setClass * @kind function * @description Adds and/or removes the given CSS classes to and from the element. * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have its CSS classes changed * removed from it * @param {string} add the CSS classes which will be added to the element * @param {string} remove the CSS class which will be removed from the element * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ setClass: function(element, add, remove, options) { var self = this; var STORAGE_KEY = '$$animateClasses'; var createdCache = false; element = jqLite(element); var cache = element.data(STORAGE_KEY); if (!cache) { cache = { classes: {}, options: options }; createdCache = true; } else if (options && cache.options) { cache.options = angular.extend(cache.options || {}, options); } var classes = cache.classes; add = isArray(add) ? add : add.split(' '); remove = isArray(remove) ? remove : remove.split(' '); cachedClassManipulation(classes, add, true); cachedClassManipulation(classes, remove, false); if (createdCache) { cache.promise = runAnimationPostDigest(function(done) { var cache = element.data(STORAGE_KEY); element.removeData(STORAGE_KEY); // in the event that the element is removed before postDigest // is run then the cache will be undefined and there will be // no need anymore to add or remove and of the element classes if (cache) { var classes = resolveElementClasses(element, cache.classes); if (classes) { self.$$setClassImmediately(element, classes[0], classes[1], cache.options); } } done(); }); element.data(STORAGE_KEY, cache); } return cache.promise; }, $$setClassImmediately: function(element, add, remove, options) { add && this.$$addClassImmediately(element, add); remove && this.$$removeClassImmediately(element, remove); applyStyles(element, options); return asyncPromise(); }, enabled: noop, cancel: noop }; }]; }]; function $$AsyncCallbackProvider() { this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { return $$rAF.supported ? function(fn) { return $$rAF(fn); } : function(fn) { return $timeout(fn, 0, false); }; }]; } /* global stripHash: true */ /** * ! This is a private undocumented service ! * * @name $browser * @requires $log * @description * This object has two goals: * * - hide all the global state in the browser caused by the window object * - abstract away all the browser specific features and inconsistencies * * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` * service, which can be used for convenient testing of the application without the interaction with * the real browser apis. */ /** * @param {object} window The global window object. * @param {object} document jQuery wrapped document. * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer) { var self = this, rawDocument = document[0], location = window.location, history = window.history, setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, pendingDeferIds = {}; self.isMock = false; var outstandingRequestCount = 0; var outstandingRequestCallbacks = []; // TODO(vojta): remove this temporary api self.$$completeOutstandingRequest = completeOutstandingRequest; self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; /** * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. */ function completeOutstandingRequest(fn) { try { fn.apply(null, sliceArgs(arguments, 1)); } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { while (outstandingRequestCallbacks.length) { try { outstandingRequestCallbacks.pop()(); } catch (e) { $log.error(e); } } } } } function getHash(url) { var index = url.indexOf('#'); return index === -1 ? '' : url.substr(index + 1); } /** * @private * Note: this method is used only by scenario runner * TODO(vojta): prefix this method with $$ ? * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { // force browser to execute all pollFns - this is needed so that cookies and other pollers fire // at some deterministic time in respect to the test runner's actions. Leaving things up to the // regular poller would result in flaky tests. forEach(pollFns, function(pollFn) { pollFn(); }); if (outstandingRequestCount === 0) { callback(); } else { outstandingRequestCallbacks.push(callback); } }; ////////////////////////////////////////////////////////////// // Poll Watcher API ////////////////////////////////////////////////////////////// var pollFns = [], pollTimeout; /** * @name $browser#addPollFn * * @param {function()} fn Poll function to add * * @description * Adds a function to the list of functions that poller periodically executes, * and starts polling if not started yet. * * @returns {function()} the added function */ self.addPollFn = function(fn) { if (isUndefined(pollTimeout)) startPoller(100, setTimeout); pollFns.push(fn); return fn; }; /** * @param {number} interval How often should browser call poll functions (ms) * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. * * @description * Configures the poller to run in the specified intervals, using the specified * setTimeout fn and kicks it off. */ function startPoller(interval, setTimeout) { (function check() { forEach(pollFns, function(pollFn) { pollFn(); }); pollTimeout = setTimeout(check, interval); })(); } ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// var cachedState, lastHistoryState, lastBrowserUrl = location.href, baseElement = document.find('base'), reloadLocation = null; cacheState(); lastHistoryState = cachedState; /** * @name $browser#url * * @description * GETTER: * Without any argument, this method just returns current value of location.href. * * SETTER: * With at least one argument, this method sets url to new value. * If html5 history api supported, pushState/replaceState is used, otherwise * location.href/location.replace is used. * Returns its own instance to allow chaining * * NOTE: this api is intended for use only by the $location service. Please use the * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) * @param {boolean=} replace Should new url replace current history record? * @param {object=} state object to use with pushState/replaceState */ self.url = function(url, replace, state) { // In modern browsers `history.state` is `null` by default; treating it separately // from `undefined` would cause `$browser.url('/foo')` to change `history.state` // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. if (isUndefined(state)) { state = null; } // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { var sameState = lastHistoryState === state; // Don't change anything if previous and current URLs and states match. This also prevents // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. // See https://github.com/angular/angular.js/commit/ffb2701 if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { return self; } var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; lastHistoryState = state; // Don't use history API if only the hash changed // due to a bug in IE10/IE11 which leads // to not firing a `hashchange` nor `popstate` event // in some cases (see #9143). if ($sniffer.history && (!sameBase || !sameState)) { history[replace ? 'replaceState' : 'pushState'](state, '', url); cacheState(); // Do the assignment again so that those two variables are referentially identical. lastHistoryState = cachedState; } else { if (!sameBase) { reloadLocation = url; } if (replace) { location.replace(url); } else if (!sameBase) { location.href = url; } else { location.hash = getHash(url); } } return self; // getter } else { // - reloadLocation is needed as browsers don't allow to read out // the new location.href if a reload happened. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 return reloadLocation || location.href.replace(/%27/g,"'"); } }; /** * @name $browser#state * * @description * This method is a getter. * * Return history.state or null if history.state is undefined. * * @returns {object} state */ self.state = function() { return cachedState; }; var urlChangeListeners = [], urlChangeInit = false; function cacheStateAndFireUrlChange() { cacheState(); fireUrlChange(); } function getCurrentState() { try { return history.state; } catch (e) { // MSIE can reportedly throw when there is no state (UNCONFIRMED). } } // This variable should be used *only* inside the cacheState function. var lastCachedState = null; function cacheState() { // This should be the only place in $browser where `history.state` is read. cachedState = getCurrentState(); cachedState = isUndefined(cachedState) ? null : cachedState; // Prevent callbacks fo fire twice if both hashchange & popstate were fired. if (equals(cachedState, lastCachedState)) { cachedState = lastCachedState; } lastCachedState = cachedState; } function fireUrlChange() { if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { return; } lastBrowserUrl = self.url(); lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { listener(self.url(), cachedState); }); } /** * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. * * It's only called when the url is changed from outside of angular: * - user types different url into address bar * - user clicks on history (forward/back) button * - user clicks on a link * * It's not called when url is changed by $browser.url() method * * The listener gets called with new url as parameter. * * NOTE: this api is intended for use only by the $location service. Please use the * {@link ng.$location $location service} to monitor url changes in angular apps. * * @param {function(string)} listener Listener function to be called when url changes. * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) // don't fire popstate when user change the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } urlChangeListeners.push(callback); return callback; }; /** * Checks whether the url has changed outside of Angular. * Needs to be exported to be able to check for changes that have been done in sync, * as hashchange/popstate events fire in async. */ self.$$checkUrlChange = fireUrlChange; ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// /** * @name $browser#baseHref * * @description * Returns current * (always relative - without domain) * * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; }; ////////////////////////////////////////////////////////////// // Cookies API ////////////////////////////////////////////////////////////// var lastCookies = {}; var lastCookieString = ''; var cookiePath = self.baseHref(); function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch (e) { return str; } } /** * @name $browser#cookies * * @param {string=} name Cookie name * @param {string=} value Cookie value * * @description * The cookies method provides a 'private' low level access to browser cookies. * It is not meant to be used directly, use the $cookie service instead. * * The return values vary depending on the arguments that the method was called with as follows: * * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify * it * - cookies(name, value) -> set name to value, if value is undefined delete the cookie * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that * way) * * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';path=' + cookiePath).length + 1; // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: // - 300 cookies // - 20 cookies per unique domain // - 4096 bytes per cookie if (cookieLength > 4096) { $log.warn("Cookie '" + name + "' possibly not set or overflowed because it was too large (" + cookieLength + " > 4096 bytes)!"); } } } } else { if (rawDocument.cookie !== lastCookieString) { lastCookieString = rawDocument.cookie; cookieArray = lastCookieString.split("; "); lastCookies = {}; for (i = 0; i < cookieArray.length; i++) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies name = safeDecodeURIComponent(cookie.substring(0, index)); // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. if (lastCookies[name] === undefined) { lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); } } } } return lastCookies; } }; /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] of milliseconds to defer the function execution. * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. * * @description * Executes a fn asynchronously via `setTimeout(fn, delay)`. * * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed * via `$browser.defer.flush()`. * */ self.defer = function(fn, delay) { var timeoutId; outstandingRequestCount++; timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; completeOutstandingRequest(fn); }, delay || 0); pendingDeferIds[timeoutId] = true; return timeoutId; }; /** * @name $browser#defer.cancel * * @description * Cancels a deferred task identified with `deferId`. * * @param {*} deferId Token returned by the `$browser.defer` function. * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully * canceled. */ self.defer.cancel = function(deferId) { if (pendingDeferIds[deferId]) { delete pendingDeferIds[deferId]; clearTimeout(deferId); completeOutstandingRequest(noop); return true; } return false; }; } function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', function($window, $log, $sniffer, $document) { return new Browser($window, $document, $log, $sniffer); }]; } /** * @ngdoc service * @name $cacheFactory * * @description * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to * them. * * ```js * * var cache = $cacheFactory('cacheId'); * expect($cacheFactory.get('cacheId')).toBe(cache); * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); * * cache.put("key", "value"); * cache.put("another key", "another value"); * * // We've specified no options on creation * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); * * ``` * * * @param {string} cacheId Name or id of the newly created cache. * @param {object=} options Options object that specifies the cache behavior. Properties: * * - `{number=}` `capacity` — turns the cache into LRU cache. * * @returns {object} Newly created cache object with the following set of methods: * * - `{object}` `info()` — Returns id, size, and options of cache. * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns * it. * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. * - `{void}` `removeAll()` — Removes all cached values. * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * * @example

    Cached Values

    :

    Cache Info

    :
    angular.module('cacheExampleApp', []). controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { if ($scope.cache.get(key) === undefined) { $scope.keys.push(key); } $scope.cache.put(key, value === undefined ? null : value); }; }]); p { margin: 10px 0 3px; }
    */ function $CacheFactoryProvider() { this.$get = function() { var caches = {}; function cacheFactory(cacheId, options) { if (cacheId in caches) { throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); } var size = 0, stats = extend({}, options, {id: cacheId}), data = {}, capacity = (options && options.capacity) || Number.MAX_VALUE, lruHash = {}, freshEnd = null, staleEnd = null; /** * @ngdoc type * @name $cacheFactory.Cache * * @description * A cache object used to store and retrieve data, primarily used by * {@link $http $http} and the {@link ng.directive:script script} directive to cache * templates and other data. * * ```js * angular.module('superCache') * .factory('superCache', ['$cacheFactory', function($cacheFactory) { * return $cacheFactory('super-cache'); * }]); * ``` * * Example test: * * ```js * it('should behave like a cache', inject(function(superCache) { * superCache.put('key', 'value'); * superCache.put('another key', 'another value'); * * expect(superCache.info()).toEqual({ * id: 'super-cache', * size: 2 * }); * * superCache.remove('another key'); * expect(superCache.get('another key')).toBeUndefined(); * * superCache.removeAll(); * expect(superCache.info()).toEqual({ * id: 'super-cache', * size: 0 * }); * })); * ``` */ return caches[cacheId] = { /** * @ngdoc method * @name $cacheFactory.Cache#put * @kind function * * @description * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be * retrieved later, and incrementing the size of the cache if the key was not already * present in the cache. If behaving like an LRU cache, it will also remove stale * entries from the set. * * It will not insert undefined values into the cache. * * @param {string} key the key under which the cached data is stored. * @param {*} value the value to store alongside the key. If it is undefined, the key * will not be stored. * @returns {*} the value stored. */ put: function(key, value) { if (capacity < Number.MAX_VALUE) { var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); refresh(lruEntry); } if (isUndefined(value)) return; if (!(key in data)) size++; data[key] = value; if (size > capacity) { this.remove(staleEnd.key); } return value; }, /** * @ngdoc method * @name $cacheFactory.Cache#get * @kind function * * @description * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. * * @param {string} key the key of the data to be retrieved * @returns {*} the value stored. */ get: function(key) { if (capacity < Number.MAX_VALUE) { var lruEntry = lruHash[key]; if (!lruEntry) return; refresh(lruEntry); } return data[key]; }, /** * @ngdoc method * @name $cacheFactory.Cache#remove * @kind function * * @description * Removes an entry from the {@link $cacheFactory.Cache Cache} object. * * @param {string} key the key of the entry to be removed */ remove: function(key) { if (capacity < Number.MAX_VALUE) { var lruEntry = lruHash[key]; if (!lruEntry) return; if (lruEntry == freshEnd) freshEnd = lruEntry.p; if (lruEntry == staleEnd) staleEnd = lruEntry.n; link(lruEntry.n,lruEntry.p); delete lruHash[key]; } delete data[key]; size--; }, /** * @ngdoc method * @name $cacheFactory.Cache#removeAll * @kind function * * @description * Clears the cache object of any entries. */ removeAll: function() { data = {}; size = 0; lruHash = {}; freshEnd = staleEnd = null; }, /** * @ngdoc method * @name $cacheFactory.Cache#destroy * @kind function * * @description * Destroys the {@link $cacheFactory.Cache Cache} object entirely, * removing it from the {@link $cacheFactory $cacheFactory} set. */ destroy: function() { data = null; stats = null; lruHash = null; delete caches[cacheId]; }, /** * @ngdoc method * @name $cacheFactory.Cache#info * @kind function * * @description * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. * * @returns {object} an object with the following properties: *
      *
    • **id**: the id of the cache instance
    • *
    • **size**: the number of entries kept in the cache instance
    • *
    • **...**: any additional properties from the options object when creating the * cache.
    • *
    */ info: function() { return extend({}, stats, {size: size}); } }; /** * makes the `entry` the freshEnd of the LRU linked list */ function refresh(entry) { if (entry != freshEnd) { if (!staleEnd) { staleEnd = entry; } else if (staleEnd == entry) { staleEnd = entry.n; } link(entry.n, entry.p); link(entry, freshEnd); freshEnd = entry; freshEnd.n = null; } } /** * bidirectionally links two entries of the LRU linked list */ function link(nextEntry, prevEntry) { if (nextEntry != prevEntry) { if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } } } /** * @ngdoc method * @name $cacheFactory#info * * @description * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ cacheFactory.info = function() { var info = {}; forEach(caches, function(cache, cacheId) { info[cacheId] = cache.info(); }); return info; }; /** * @ngdoc method * @name $cacheFactory#get * * @description * Get access to a cache object by the `cacheId` used when it was created. * * @param {string} cacheId Name or id of a cache to access. * @returns {object} Cache object identified by the cacheId or undefined if no such cache. */ cacheFactory.get = function(cacheId) { return caches[cacheId]; }; return cacheFactory; }; } /** * @ngdoc service * @name $templateCache * * @description * The first time a template is used, it is loaded in the template cache for quick retrieval. You * can load templates directly into the cache in a `script` tag, or by consuming the * `$templateCache` service directly. * * Adding via the `script` tag: * * ```html * * ``` * * **Note:** the `script` tag containing the template does not need to be included in the `head` of * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, * element with ng-app attribute), otherwise the template will be ignored. * * Adding via the `$templateCache` service: * * ```js * var myApp = angular.module('myApp', []); * myApp.run(function($templateCache) { * $templateCache.put('templateId.html', 'This is the content of the template'); * }); * ``` * * To retrieve the template later, simply use it in your HTML: * ```html *
    * ``` * * or get it via Javascript: * ```js * $templateCache.get('templateId.html') * ``` * * See {@link ng.$cacheFactory $cacheFactory}. * */ function $TemplateCacheProvider() { this.$get = ['$cacheFactory', function($cacheFactory) { return $cacheFactory('templates'); }]; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Any commits to this file should be reviewed with security in mind. * * Changes to this file can potentially create security vulnerabilities. * * An approval from 2 Core members with history of modifying * * this file is required. * * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! * * DOM-related variables: * * - "node" - DOM Node * - "element" - DOM Element or Node * - "$node" or "$element" - jqLite-wrapped node or element * * * Compiler related stuff: * * - "linkFn" - linking fn of a single directive * - "nodeLinkFn" - function that aggregates all linking fns for a particular node * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) */ /** * @ngdoc service * @name $compile * @kind function * * @description * Compiles an HTML string or DOM into a template and produces a template function, which * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. * * The compilation is a process of walking the DOM tree and matching DOM elements to * {@link ng.$compileProvider#directive directives}. * *
    * **Note:** This document is an in-depth reference of all directive options. * For a gentle introduction to directives with examples of common use cases, * see the {@link guide/directive directive guide}. *
    * * ## Comprehensive Directive API * * There are many different options for a directive. * * The difference resides in the return value of the factory function. * You can either return a "Directive Definition Object" (see below) that defines the directive properties, * or just the `postLink` function (all other properties will have the default values). * *
    * **Best Practice:** It's recommended to use the "directive definition object" form. *
    * * Here's an example directive declared with a Directive Definition Object: * * ```js * var myModule = angular.module(...); * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { * priority: 0, * template: '
    ', // or // function(tElement, tAttrs) { ... }, * // or * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, * transclude: false, * restrict: 'A', * templateNamespace: 'html', * scope: false, * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, * controllerAs: 'stringAlias', * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], * compile: function compile(tElement, tAttrs, transclude) { * return { * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, * post: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or * // link: { * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, * // post: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or * // link: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); * ``` * *
    * **Note:** Any unspecified options will use the default value. You can see the default values below. *
    * * Therefore the above can be simplified as: * * ```js * var myModule = angular.module(...); * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { * link: function postLink(scope, iElement, iAttrs) { ... } * }; * return directiveDefinitionObject; * // or * // return function postLink(scope, iElement, iAttrs) { ... } * }); * ``` * * * * ### Directive Definition Object * * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * * #### `multiElement` * When this property is set to true, the HTML compiler will collect DOM nodes between * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them * together as the directive elements. It is recommended that this feature be used on directives * which are not strictly behavioural (such as {@link ngClick}), and which * do not manipulate or replace child nodes (such as {@link ngInclude}). * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it * is necessary to specify the order in which the directives are applied. The `priority` is used * to sort the directives before their `compile` functions get called. Priority is defined as a * number. Directives with greater numerical `priority` are compiled first. Pre-link functions * are also run in priority order, but post-link functions are run in reverse order. The order * of directives with the same priority is undefined. The default priority is `0`. * * #### `terminal` * If set to true then the current `priority` will be the last set of directives * which will execute (any directives at the current priority will still execute * as the order of execution on same `priority` is undefined). Note that expressions * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the * same element request a new scope, only one new scope is created. The new scope rule does not * apply for the root of the template since the root of the template always gets a new scope. * * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from * normal scope in that it does not prototypically inherit from the parent scope. This is useful * when creating reusable components, which should not accidentally read or modify data in the * parent scope. * * The 'isolate' scope takes an object hash which defines a set of local scope properties * derived from the parent scope. These local properties are useful for aliasing values for * templates. Locals definition is a hash of local scope property to its source: * * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is * always a string since DOM attributes are strings. If no `attr` name is specified then the * attribute name is assumed to be the same as the local name. * Given `` and widget definition * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the * `localName` property on the widget scope. The `name` is read from the parent scope (not * component scope). * * * `=` or `=attr` - set up bi-directional binding between a local scope property and the * parent scope property of name defined via the value of the `attr` attribute. If no `attr` * name is specified then the attribute name is assumed to be the same as the local name. * Given `` and widget definition of * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). * * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. * If no `attr` name is specified then the attribute name is assumed to be the same as the * local name. Given `` and widget definition of * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to * a function wrapper for the `count = count + value` expression. Often it's desirable to * pass data from the isolated scope via an expression to the parent scope, this can be * done by passing a map of local variable names and values into the expression wrapper fn. * For example, if the expression is `increment(amount)` then we can specify the amount value * by calling the `localFn` as `localFn({amount: 22})`. * * * #### `bindToController` * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. When the controller * is instantiated, the initial values of the isolate scope bindings are already available. * * #### `controller` * Controller constructor function. The controller is instantiated before the * pre-linking phase and it is shared with other directives (see * `require` attribute). This allows the directives to communicate with each other and augment * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: * * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: * `function([scope], cloneLinkingFn, futureParentElement)`. * * `scope`: optional argument to override the scope. * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. * * `futureParentElement`: * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) * and when the `cloneLinkinFn` is passed, * as those elements need to created and cloned in a special way when they are defined outside their * usual containers (e.g. like ``). * * See also the `directive.templateNamespace` property. * * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the * injected argument will be an array in corresponding order. If no such directive can be * found, or if the directive does not have a controller, then an error is raised (unless no link function * is specified, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass * `null` to the `link` fn if not found. * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass * `null` to the `link` fn if not found. * * * #### `controllerAs` * Controller alias at the directive scope. An alias for the controller so it * can be referenced at the directive template. The directive needs to define a scope for this * configuration to be used. Useful in the case when directive is used as component. * * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive * declaration style. If omitted, the defaults (elements and attributes) are used. * * * `E` - Element name (default): `` * * `A` - Attribute (default): `
    ` * * `C` - Class: `
    ` * * `M` - Comment: `` * * * #### `templateNamespace` * String representing the document type used by the markup in the template. * AngularJS needs this information as those elements need to be created and cloned * in a special way when they are defined outside their usual containers like `` and ``. * * * `html` - All root nodes in the template are HTML. Root nodes may also be * top-level elements such as `` or ``. * * `svg` - The root nodes in the template are SVG elements (excluding ``). * * `math` - The root nodes in the template are MathML elements (excluding ``). * * If no `templateNamespace` is specified, then the namespace is considered to be `html`. * * #### `template` * HTML markup that may: * * Replace the contents of the directive's element (default). * * Replace the directive's element itself (if `replace` is true - DEPRECATED). * * Wrap the contents of the directive's element (if `transclude` is true). * * Value may be: * * * A string. For example `
    {{delete_str}}
    `. * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` * function api below) and returns a string value. * * * #### `templateUrl` * This is similar to `template` but the template is loaded from the specified URL, asynchronously. * * Because template loading is asynchronous the compiler will suspend compilation of directives on that element * for later when the template has been resolved. In the meantime it will continue to compile and link * sibling and parent elements as though this element had not contained any directives. * * The compiler does not suspend the entire compilation to wait for templates to be loaded because this * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the * case when only one deeply nested directive has `templateUrl`. * * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) * specify what the template should replace. Defaults to `false`. * * * `true` - the template will replace the directive's element. * * `false` - the template will replace the contents of the directive's element. * * The replacement process migrates all of the attributes / classes from the old element to the new * one. See the {@link guide/directive#template-expanding-directive * Directives Guide} for an example. * * There are very few scenarios where element replacement is required for the application function, * the main one being reusable custom components that are used within SVG contexts * (because SVG doesn't work with custom elements in the DOM tree). * * #### `transclude` * Extract the contents of the element where the directive appears and make it available to the directive. * The contents are compiled and provided to the directive as a **transclusion function**. See the * {@link $compile#transclusion Transclusion} section below. * * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the * directive's element or the entire element: * * * `true` - transclude the content (i.e. the child nodes) of the directive's element. * * `'element'` - transclude the whole of the directive's element including any directives on this * element that defined at a lower priority than this directive. When used, the `template` * property is ignored. * * * #### `compile` * * ```js * function compile(tElement, tAttrs, transclude) { ... } * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do * template transformation, it is not used often. The compile function takes the following arguments: * * * `tElement` - template element - The element where the directive has been declared. It is * safe to do template transformation on the element and child elements only. * * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared * between all directive compile functions. * * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` * *
    * **Note:** The template instance and the link instance may be different objects if the template has * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration * should be done in a linking function rather than in a compile function. *
    *
    * **Note:** The compile function cannot handle directives that recursively use themselves in their * own templates or compile functions. Compiling these directives results in an infinite loop and a * stack overflow errors. * * This can be avoided by manually using $compile in the postLink function to imperatively compile * a directive's template instead of relying on automatic template compilation via `template` or * `templateUrl` declaration or manual compilation inside the compile function. *
    * *
    * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it * e.g. does not know about the right outer scope. Please use the transclude function that is passed * to the link function instead. *
    * A compile function can have a return value which can be either a function or an object. * * * returning a (post-link) function - is equivalent to registering the linking function via the * `link` property of the config object when the compile function is empty. * * * returning an object with function(s) registered via `pre` and `post` properties - allows you to * control when a linking function should be called during the linking phase. See info about * pre-linking and post-linking functions below. * * * #### `link` * This property is used only if the `compile` property is not defined. * * ```js * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... } * ``` * * The link function is responsible for registering DOM listeners as well as updating the DOM. It is * executed after the template has been cloned. This is where most of the directive logic will be * put. * * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the * directive for registering {@link ng.$rootScope.Scope#$watch watches}. * * * `iElement` - instance element - The element where the directive is to be used. It is safe to * manipulate the children of the element only in `postLink` function since the children have * already been linked. * * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared * between all directive linking functions. * * * `controller` - a controller instance - A controller instance if at least one directive on the * element defines a controller. The controller is shared among all the directives, which allows * the directives to use the controllers as a communication channel. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. * This is the same as the `$transclude` * parameter of directive controllers, see there for details. * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * * Executed before the child elements are linked. Not safe to do DOM transformation since the * compiler linking function will fail to locate the correct elements for linking. * * #### Post-linking function * * Executed after the child elements are linked. * * Note that child elements that contain `templateUrl` directives will not have been compiled * and linked since they are waiting for their template to load asynchronously and their own * compilation and linking has been suspended until that occurs. * * It is safe to do DOM transformation in the post-linking function on elements that are not waiting * for their async templates to be resolved. * * * ### Transclusion * * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and * copying them to another part of the DOM, while maintaining their connection to the original AngularJS * scope from where they were taken. * * Transclusion is used (often with {@link ngTransclude}) to insert the * original contents of a directive's element into a specified place in the template of the directive. * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded * content has access to the properties on the scope from which it was taken, even if the directive * has isolated scope. * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. * * This makes it possible for the widget to have private state for its template, while the transcluded * content has access to its originating scope. * *
    * **Note:** When testing an element transclude directive you must not place the directive at the root of the * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives * Testing Transclusion Directives}. *
    * * #### Transclusion Functions * * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion * function** to the directive's `link` function and `controller`. This transclusion function is a special * **linking function** that will return the compiled contents linked to a new transclusion scope. * *
    * If you are just using {@link ngTransclude} then you don't need to worry about this function, since * ngTransclude will deal with it for us. *
    * * If you want to manually control the insertion and removal of the transcluded content in your directive * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery * object that contains the compiled DOM, which is linked to the correct transclusion scope. * * When you call a transclusion function you can pass in a **clone attach function**. This function accepts * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded * content and the `scope` is the newly created transclusion scope, to which the clone is bound. * *
    * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. *
    * * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone * attach function**: * * ```js * var transcludedContent, transclusionScope; * * $transclude(function(clone, scope) { * element.append(clone); * transcludedContent = clone; * transclusionScope = scope; * }); * ``` * * Later, if you want to remove the transcluded content from your DOM then you should also destroy the * associated transclusion scope: * * ```js * transcludedContent.remove(); * transclusionScope.$destroy(); * ``` * *
    * **Best Practice**: if you intend to add and remove transcluded content manually in your directive * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it), * then you are also responsible for calling `$destroy` on the transclusion scope. *
    * * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} * automatically destroy their transluded clones as necessary so you do not need to worry about this if * you are simply using {@link ngTransclude} to inject the transclusion into your directive. * * * #### Transclusion Scopes * * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it * was taken. * * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look * like this: * * ```html *
    *
    *
    *
    *
    *
    * ``` * * The `$parent` scope hierarchy will look like this: * * ``` * - $rootScope * - isolate * - transclusion * ``` * * but the scopes will inherit prototypically from different scopes to their `$parent`. * * ``` * - $rootScope * - transclusion * - isolate * ``` * * * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * * accessing *Normalized attribute names:* * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. * the attributes object allows for normalized access to * the attributes. * * * *Directive inter-communication:* All directives share the same instance of the attributes * object which allows the directives to use the attributes object as inter directive * communication. * * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object * allowing other directives to read the interpolated value. * * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also * the only way to easily get the actual value because during the linking phase the interpolation * hasn't been evaluated yet and so the value is at this time set to `undefined`. * * ```js * function linkingFn(scope, elm, attrs, ctrl) { * // get the attribute value * console.log(attrs.ngModel); * * // change the attribute * attrs.$set('ngModel', 'new value'); * * // observe changes to interpolated attribute * attrs.$observe('ngModel', function(value) { * console.log('ngModel has changed value to ' + value); * }); * } * ``` * * ## Example * *
    * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. *
    *


    it('should auto compile', function() { var textarea = $('textarea'); var output = $('div[compile]'); // The initial state reads 'Hello Angular'. expect(output.getText()).toBe('Hello Angular'); textarea.clear(); textarea.sendKeys('{{name}}!'); expect(output.getText()).toBe('Angular!'); });
    * * * @param {string|DOMElement} element Element or HTML string to compile into a template function. * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. * *
    * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it * e.g. will not use the right outer scope. Please pass the transclude function as a * `parentBoundTranscludeFn` to the link function instead. *
    * * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the * `template` and call the `cloneAttachFn` function allowing the caller to attach the * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is * called as:
    `cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. * * * `options` - An optional object hash with linking options. If `options` is provided, then the following * keys may be used to control linking behavior: * * * `parentBoundTranscludeFn` - the transclude function made available to * directives; if given, it will be passed through to the link functions of * directives found in `element` during compilation. * * `transcludeControllers` - an object hash with keys that map controller names * to controller instances; if given, it will make the controllers * available to directives. * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add * the cloned elements; only needed for transcludes that are allowed to contain non html * elements (e.g. SVG elements). See also the directive.controller property. * * Calling the linking function returns the element of the template. It is either the original * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * * After linking the view is not updated until after a call to $digest which typically is done by * Angular automatically. * * If you need access to the bound view, there are two ways to do it: * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. * ```js * var element = $compile('

    {{total}}

    ')(scope); * ``` * * - if on the other hand, you need the element to be cloned, the view reference from the original * example would not point to the clone, but rather to the original template that was cloned. In * this case, you can access the clone via the cloneAttachFn: * ```js * var templateElement = angular.element('

    {{total}}

    '), * scope = ....; * * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { * //attach the clone to DOM document at the right place * }); * * //now we have reference to the cloned DOM via `clonedElement` * ``` * * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. */ var $compileMinErr = minErr('$compile'); /** * @ngdoc provider * @name $compileProvider * * @description */ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/, ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; function parseIsolateBindings(scope, directiveName) { var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; var bindings = {}; forEach(scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP); if (!match) { throw $compileMinErr('iscp', "Invalid isolate scope definition for directive '{0}'." + " Definition: {... {1}: '{2}' ...}", directiveName, scopeName, definition); } bindings[scopeName] = { mode: match[1][0], collection: match[2] === '*', optional: match[3] === '?', attrName: match[4] || scopeName }; }); return bindings; } /** * @ngdoc method * @name $compileProvider#directive * @kind function * * @description * Register a new directive with the compiler. * * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which * will match as ng-bind), or an object map of directives where the keys are the * names and the values are the factories. * @param {Function|Array} directiveFactory An injectable directive factory function. See * {@link guide/directive} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', function($injector, $exceptionHandler) { var directives = []; forEach(hasDirectives[name], function(directiveFactory, index) { try { var directive = $injector.invoke(directiveFactory); if (isFunction(directive)) { directive = { compile: valueFn(directive) }; } else if (!directive.compile && directive.link) { directive.compile = valueFn(directive.link); } directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); directive.restrict = directive.restrict || 'EA'; if (isObject(directive.scope)) { directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name); } directives.push(directive); } catch (e) { $exceptionHandler(e); } }); return directives; }]); } hasDirectives[name].push(directiveFactory); } else { forEach(name, reverseParams(registerDirective)); } return this; }; /** * @ngdoc method * @name $compileProvider#aHrefSanitizationWhitelist * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * * @param {RegExp=} regexp New regexp to whitelist urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ this.aHrefSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); return this; } else { return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); } }; /** * @ngdoc method * @name $compileProvider#imgSrcSanitizationWhitelist * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during img[src] sanitization. * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * * Any url about to be assigned to img[src] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * * @param {RegExp=} regexp New regexp to whitelist urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ this.imgSrcSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); return this; } else { return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); } }; /** * @ngdoc method * @name $compileProvider#debugInfoEnabled * * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the * current debugInfoEnabled state * @returns {*} current value if used as getter or itself (chaining) if used as setter * * @kind function * * @description * Call this method to enable/disable various debug runtime information in the compiler such as adding * binding information and a reference to the current scope on to DOM elements. * If enabled, the compiler will add the following to DOM elements that have been bound to the scope * * `ng-binding` CSS class * * `$binding` data property containing an array of the binding expressions * * You may want to disable this in production for a significant performance boost. See * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. * * The default value is true. */ var debugInfoEnabled = true; this.debugInfoEnabled = function(enabled) { if (isDefined(enabled)) { debugInfoEnabled = enabled; return this; } return debugInfoEnabled; }; this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { var Attributes = function(element, attributesToCopy) { if (attributesToCopy) { var keys = Object.keys(attributesToCopy); var i, l, key; for (i = 0, l = keys.length; i < l; i++) { key = keys[i]; this[key] = attributesToCopy[key]; } } else { this.$attr = {}; } this.$$element = element; }; Attributes.prototype = { /** * @ngdoc method * @name $compile.directive.Attributes#$normalize * @kind function * * @description * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or * `data-`) to its normalized, camelCase form. * * Also there is special case for Moz prefix starting with upper case letter. * * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} * * @param {string} name Name to normalize */ $normalize: directiveNormalize, /** * @ngdoc method * @name $compile.directive.Attributes#$addClass * @kind function * * @description * Adds the CSS class value specified by the classVal parameter to the element. If animations * are enabled then an animation will be triggered for the class addition. * * @param {string} classVal The className value that will be added to the element */ $addClass: function(classVal) { if (classVal && classVal.length > 0) { $animate.addClass(this.$$element, classVal); } }, /** * @ngdoc method * @name $compile.directive.Attributes#$removeClass * @kind function * * @description * Removes the CSS class value specified by the classVal parameter from the element. If * animations are enabled then an animation will be triggered for the class removal. * * @param {string} classVal The className value that will be removed from the element */ $removeClass: function(classVal) { if (classVal && classVal.length > 0) { $animate.removeClass(this.$$element, classVal); } }, /** * @ngdoc method * @name $compile.directive.Attributes#$updateClass * @kind function * * @description * Adds and removes the appropriate CSS class values to the element based on the difference * between the new and old CSS class values (specified as newClasses and oldClasses). * * @param {string} newClasses The current CSS className value * @param {string} oldClasses The former CSS className value */ $updateClass: function(newClasses, oldClasses) { var toAdd = tokenDifference(newClasses, oldClasses); if (toAdd && toAdd.length) { $animate.addClass(this.$$element, toAdd); } var toRemove = tokenDifference(oldClasses, newClasses); if (toRemove && toRemove.length) { $animate.removeClass(this.$$element, toRemove); } }, /** * Set a normalized attribute on the element in a way such that all directives * can share the attribute. This function properly handles boolean attributes. * @param {string} key Normalized key. (ie ngAttribute) * @param {string|boolean} value The value to set. If `null` attribute will be deleted. * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. * Defaults to true. * @param {string=} attrName Optional none normalized name. Defaults to key. */ $set: function(key, value, writeAttr, attrName) { // TODO: decide whether or not to throw an error if "class" //is set through this function since it may cause $updateClass to //become unstable. var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), aliasedKey = getAliasedAttrName(node, key), observer = key, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; } else if (aliasedKey) { this[aliasedKey] = value; observer = aliasedKey; } this[key] = value; // translate normalized key to actual key if (attrName) { this.$attr[key] = attrName; } else { attrName = this.$attr[key]; if (!attrName) { this.$attr[key] = attrName = snake_case(key, '-'); } } nodeName = nodeName_(this.$$element); if ((nodeName === 'a' && key === 'href') || (nodeName === 'img' && key === 'src')) { // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); } else if (nodeName === 'img' && key === 'srcset') { // sanitize img[srcset] values var result = ""; // first check if there are spaces because it's not the same pattern var trimmedSrcset = trim(value); // ( 999x ,| 999w ,| ,|, ) var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; // split srcset into tuple of uri and descriptor except for the last item var rawUris = trimmedSrcset.split(pattern); // for each tuples var nbrUrisWith2parts = Math.floor(rawUris.length / 2); for (var i = 0; i < nbrUrisWith2parts; i++) { var innerIdx = i * 2; // sanitize the uri result += $$sanitizeUri(trim(rawUris[innerIdx]), true); // add the descriptor result += (" " + trim(rawUris[innerIdx + 1])); } // split the last item into uri and descriptor var lastTuple = trim(rawUris[i * 2]).split(/\s/); // sanitize the last uri result += $$sanitizeUri(trim(lastTuple[0]), true); // and add the last descriptor if any if (lastTuple.length === 2) { result += (" " + trim(lastTuple[1])); } this[key] = value = result; } if (writeAttr !== false) { if (value === null || value === undefined) { this.$$element.removeAttr(attrName); } else { this.$$element.attr(attrName, value); } } // fire observers var $$observers = this.$$observers; $$observers && forEach($$observers[observer], function(fn) { try { fn(value); } catch (e) { $exceptionHandler(e); } }); }, /** * @ngdoc method * @name $compile.directive.Attributes#$observe * @kind function * * @description * Observes an interpolated attribute. * * The observer function will be invoked once during the next `$digest` following * compilation. The observer is then invoked whenever the interpolated value * changes. * * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); $rootScope.$evalAsync(function() { if (!listeners.$$inter && attrs.hasOwnProperty(key)) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } }); return function() { arrayRemove(listeners, fn); }; } }; function safeAddClass($element, className) { try { $element.addClass(className); } catch (e) { // ignore, since it means that we are trying to set class on // SVG element, where class name is read-only. } } var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { var bindings = $element.data('$binding') || []; if (isArray(binding)) { bindings = bindings.concat(binding); } else { bindings.push(binding); } $element.data('$binding', bindings); } : noop; compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { safeAddClass($element, 'ng-binding'); } : noop; compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; $element.data(dataName, scope); } : noop; compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); } : noop; return compile; //================================ function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { if (!($compileNodes instanceof jqLite)) { // jquery always rewraps, whereas we need to preserve the original selector so that we can // modify it. $compileNodes = jqLite($compileNodes); } // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in forEach($compileNodes, function(node, index) { if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { $compileNodes[index] = jqLite(node).wrap('').parent()[0]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; return function publicLinkFn(scope, cloneConnectFn, options) { assertArg(scope, 'scope'); options = options || {}; var parentBoundTranscludeFn = options.parentBoundTranscludeFn, transcludeControllers = options.transcludeControllers, futureParentElement = options.futureParentElement; // When `parentBoundTranscludeFn` is passed, it is a // `controllersBoundTransclude` function (it was previously passed // as `transclude` to directive.link) so we must unwrap it to get // its `boundTranscludeFn` if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) { parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude; } if (!namespace) { namespace = detectNamespaceForChildElements(futureParentElement); } var $linkNode; if (namespace !== 'html') { // When using a directive with replace:true and templateUrl the $compileNodes // (or a child element inside of them) // might change, so we need to recreate the namespace adapted compileNodes // for call to the link function. // Note: This will already clone the nodes... $linkNode = jqLite( wrapTemplate(namespace, jqLite('
    ').append($compileNodes).html()) ); } else if (cloneConnectFn) { // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. $linkNode = JQLitePrototype.clone.call($compileNodes); } else { $linkNode = $compileNodes; } if (transcludeControllers) { for (var controllerName in transcludeControllers) { $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } compile.$$addScopeInfo($linkNode, scope); if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; } function detectNamespaceForChildElements(parentElement) { // TODO: Make this detect MathML as well... var node = parentElement && parentElement[0]; if (!node) { return 'html'; } else { return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; } } /** * Compile function matches each node in nodeList against the directives. Once all directives * for a particular node are collected their compile functions are executed. The compile * functions return values - the linking functions - are combined into a composite linking * function, which is the a linking function for the node. * * @param {NodeList} nodeList an array of nodes or NodeList to compile * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the * scope argument is auto-generated to the new child of the transcluded parent scope. * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then * the rootElement must be set the jqLite collection of the compile root. This is * needed so that the jqLite collection items can be replaced with widgets. * @param {number=} maxPriority Max directive priority. * @returns {Function} A composite linking function of all of the matched directives or null. */ function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); // we must always refer to nodeList[i] since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext) : null; if (nodeLinkFn && nodeLinkFn.scope) { compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !(childNodes = nodeList[i].childNodes) || !childNodes.length) ? null : compileNodes(childNodes, nodeLinkFn ? ( (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) && nodeLinkFn.transclude) : transcludeFn); if (nodeLinkFn || childLinkFn) { linkFns.push(i, nodeLinkFn, childLinkFn); linkFnFound = true; nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; } //use the previous context only for the first element in the virtual group previousCompileContext = null; } // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; var stableNodeList; if (nodeLinkFnFound) { // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our // offsets don't get screwed up var nodeListLength = nodeList.length; stableNodeList = new Array(nodeListLength); // create a sparse array by only copying the elements which have a linkFn for (i = 0; i < linkFns.length; i+=3) { idx = linkFns[i]; stableNodeList[idx] = nodeList[idx]; } } else { stableNodeList = nodeList; } for (i = 0, ii = linkFns.length; i < ii;) { node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } if (nodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn( scope, nodeLinkFn.transclude, parentBoundTranscludeFn, nodeLinkFn.elementTranscludeOnThisElement); } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; } else if (!parentBoundTranscludeFn && transcludeFn) { childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); } else { childBoundTranscludeFn = null; } nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); } else if (childLinkFn) { childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; } return transcludeFn(transcludedScope, cloneFn, { parentBoundTranscludeFn: previousBoundTranscludeFn, transcludeControllers: controllers, futureParentElement: futureParentElement }); }; return boundTranscludeFn; } /** * Looks for directives on the given node and adds them to the directive collection which is * sorted. * * @param node Node to search. * @param directives An array to which the directives are added to. This array is sorted before * the function returns. * @param attrs The shared attrs object which is used to populate the normalized attributes. * @param {number=} maxPriority Max directive priority. */ function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { var nodeType = node.nodeType, attrsMap = attrs.$attr, match, className; switch (nodeType) { case NODE_TYPE_ELEMENT: /* Element */ // use the node name: addDirective(directives, directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; name = attr.name; value = trim(attr.value); // support ngAttr attribute binding ngAttrName = directiveNormalize(name); if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { name = name.replace(PREFIX_REGEXP, '') .substr(8).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); }); } var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); if (directiveIsMultiElement(directiveNName)) { if (ngAttrName === directiveNName + 'Start') { attrStartName = name; attrEndName = name.substr(0, name.length - 5) + 'end'; name = name.substr(0, name.length - 6); } } nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; if (isNgAttr || !attrs.hasOwnProperty(nName)) { attrs[nName] = value; if (getBooleanAttrName(node, nName)) { attrs[nName] = true; // presence means true } } addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName); } // use class as directive className = node.className; if (isObject(className)) { // Maybe SVGAnimatedString className = className.animVal; } if (isString(className) && className !== '') { while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); } className = className.substr(match.index + match[0].length); } } break; case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; case NODE_TYPE_COMMENT: /* Comment */ try { match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { nName = directiveNormalize(match[1]); if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[2]); } } } catch (e) { // turns out that under some circumstances IE9 throws errors when one attempts to read // comment's node value. // Just ignore it and continue. (Can't seem to reproduce in test case.) } break; } directives.sort(byPriority); return directives; } /** * Given a node with an directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart * @param attrEnd * @returns {*} */ function groupScan(node, attrStart, attrEnd) { var nodes = []; var depth = 0; if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { do { if (!node) { throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); } if (node.nodeType == NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } nodes.push(node); node = node.nextSibling; } while (depth > 0); } else { nodes.push(node); } return jqLite(nodes); } /** * Wrapper for linking function which converts normal linking function into a grouped * linking function. * @param linkFn * @param attrStart * @param attrEnd * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { return function(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; } /** * Once the directives have been collected, their compile functions are executed. This method * is responsible for inlining directive templates as well as terminating the application * of the directives if the terminal directive has been reached. * * @param {Array} directives Array of collected directives to execute their compile function. * this needs to be pre-sorted by priority order. * @param {Node} compileNode The raw DOM node to apply the compile functions to * @param {Object} templateAttrs The shared attribute function * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the * scope argument is auto-generated to the new * child of the transcluded parent scope. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this * argument has the root jqLite array so that we can replace nodes * on it. * @param {Object=} originalReplaceDirective An optional directive that will be ignored when * compiling the transclusion. * @param {Array.} preLinkFns * @param {Array.} postLinkFns * @param {Object} previousCompileContext Context used for previous compilation of the current * node * @returns {Function} linkFn */ function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) { previousCompileContext = previousCompileContext || {}; var terminalPriority = -Number.MAX_VALUE, newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, controllers, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, hasTranscludeDirective = false, hasTemplate = false, hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, directiveName, $template, replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, linkFn, directiveValue; // executes all directives on the current element for (var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; var attrStart = directive.$$start; var attrEnd = directive.$$end; // collect multiblock sections if (attrStart) { $compileNode = groupScan(compileNode, attrStart, attrEnd); } $template = undefined; if (terminalPriority > directive.priority) { break; // prevent further processing of directives } if (directiveValue = directive.scope) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { if (isObject(directiveValue)) { // This directive is trying to add an isolated scope. // Check that there is no scope of any kind already assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, directive, $compileNode); newIsolateScopeDirective = directive; } else { // This directive is trying to add a child scope. // Check that there is no isolated scope already assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode); } } newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; if (!directive.templateUrl && directive.controller) { directiveValue = directive.controller; controllerDirectives = controllerDirectives || {}; assertNoDuplicate("'" + directiveName + "' controller", controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } if (directiveValue = directive.transclude) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. // This option should only be used by directives that know how to safely handle element transclusion, // where the transcluded nodes are added or replaced after linking. if (!directive.$$tlb) { assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); nonTlbTranscludeDirective = directive; } if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = $compileNode; $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers // - newIsolateScopeDirective or templateDirective - combining templates with // element transclusion doesn't make sense. // // We need only nonTlbTranscludeDirective so that we prevent putting transclusion // on the same element more than once. nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { $template = jqLite(jqLiteClone(compileNode)).contents(); $compileNode.empty(); // clear contents childTranscludeFn = compile($template, transcludeFn); } } if (directive.template) { hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; directiveValue = (isFunction(directive.template)) ? directive.template($compileNode, templateAttrs) : directive.template; directiveValue = denormalizeTemplate(directiveValue); if (directive.replace) { replaceDirective = directive; if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, ''); } replaceWith(jqCollection, $compileNode, compileNode); var newTemplateAttrs = {$attr: {}}; // combine directives from the original node and from the template: // - take the array of directives for this element // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) // - collect directives from the template and sort them by priority // - combine directives as: processed + template + unprocessed var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); if (newIsolateScopeDirective) { markDirectivesAsIsolate(templateDirectives); } directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); ii = directives.length; } else { $compileNode.html(directiveValue); } } if (directive.templateUrl) { hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; if (directive.replace) { replaceDirective = directive; } nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, nonTlbTranscludeDirective: nonTlbTranscludeDirective }); ii = directives.length; } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); if (isFunction(linkFn)) { addLinkFns(null, linkFn, attrStart, attrEnd); } else if (linkFn) { addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); } } if (directive.terminal) { nodeLinkFn.terminal = true; terminalPriority = Math.max(terminalPriority, directive.priority); } } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; nodeLinkFn.templateOnThisElement = hasTemplate; nodeLinkFn.transclude = childTranscludeFn; previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present return nodeLinkFn; //////////////////// function addLinkFns(pre, post, attrStart, attrEnd) { if (pre) { if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; pre.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { pre = cloneAndAnnotateFn(pre, {isolateScope: true}); } preLinkFns.push(pre); } if (post) { if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; post.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { post = cloneAndAnnotateFn(post, {isolateScope: true}); } postLinkFns.push(post); } } function getControllers(directiveName, require, $element, elementControllers) { var value, retrievalMethod = 'data', optional = false; var $searchElement = $element; var match; if (isString(require)) { match = require.match(REQUIRE_PREFIX_REGEXP); require = require.substring(match[0].length); if (match[3]) { if (match[1]) match[3] = null; else match[1] = match[3]; } if (match[1] === '^') { retrievalMethod = 'inheritedData'; } else if (match[1] === '^^') { retrievalMethod = 'inheritedData'; $searchElement = $element.parent(); } if (match[2] === '?') { optional = true; } value = null; if (elementControllers && retrievalMethod === 'data') { if (value = elementControllers[require]) { value = value.instance; } } value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } return value || null; } else if (isArray(require)) { value = []; forEach(require, function(require) { value.push(getControllers(directiveName, require, $element, elementControllers)); }); } return value; } function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, attrs; if (compileNode === linkNode) { attrs = templateAttrs; $element = templateAttrs.$$element; } else { $element = jqLite(linkNode); attrs = new Attributes($element, templateAttrs); } if (newIsolateScopeDirective) { isolateScope = scope.$new(true); } if (boundTranscludeFn) { // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` transcludeFn = controllersBoundTransclude; transcludeFn.$$boundTransclude = boundTranscludeFn; } if (controllerDirectives) { // TODO: merge `controllers` and `elementControllers` into single object. controllers = {}; elementControllers = {}; forEach(controllerDirectives, function(directive) { var locals = { $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, $element: $element, $attrs: attrs, $transclude: transcludeFn }, controllerInstance; controller = directive.controller; if (controller == '@') { controller = attrs[directive.name]; } controllerInstance = $controller(controller, locals, true, directive.controllerAs); // For directives with element transclusion the element is a comment, // but jQuery .data doesn't support attaching data to comment nodes as it's hard to // clean up (http://bugs.jquery.com/ticket/8335). // Instead, we save the controllers for the element in a local hash and attach to .data // later, once we have the actual element. elementControllers[directive.name] = controllerInstance; if (!hasElementTranscludeDirective) { $element.data('$' + directive.name + 'Controller', controllerInstance.instance); } controllers[directive.name] = controllerInstance; }); } if (newIsolateScopeDirective) { compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || templateDirective === newIsolateScopeDirective.$$originalDirective))); compile.$$addScopeClass($element, true); var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name]; var isolateBindingContext = isolateScope; if (isolateScopeController && isolateScopeController.identifier && newIsolateScopeDirective.bindToController === true) { isolateBindingContext = isolateScopeController.instance; } forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, mode = definition.mode, // @, =, or & lastValue, parentGet, parentSet, compare; switch (mode) { case '@': attrs.$observe(attrName, function(value) { isolateBindingContext[scopeName] = value; }); attrs.$$observers[attrName].$$scope = scope; if (attrs[attrName]) { // If the attribute has been provided then we trigger an interpolation to ensure // the value is there for use in the link fn isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); } break; case '=': if (optional && !attrs[attrName]) { return; } parentGet = $parse(attrs[attrName]); if (parentGet.literal) { compare = equals; } else { compare = function(a, b) { return a === b || (a !== a && b !== b); }; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = isolateBindingContext[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", attrs[attrName], newIsolateScopeDirective.name); }; lastValue = isolateBindingContext[scopeName] = parentGet(scope); var parentValueWatch = function parentValueWatch(parentValue) { if (!compare(parentValue, isolateBindingContext[scopeName])) { // we are out of sync and need to copy if (!compare(parentValue, lastValue)) { // parent changed and it has precedence isolateBindingContext[scopeName] = parentValue; } else { // if the parent can be assigned then do so parentSet(scope, parentValue = isolateBindingContext[scopeName]); } } return lastValue = parentValue; }; parentValueWatch.$stateful = true; var unwatch; if (definition.collection) { unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); } isolateScope.$on('$destroy', unwatch); break; case '&': parentGet = $parse(attrs[attrName]); isolateBindingContext[scopeName] = function(locals) { return parentGet(scope, locals); }; break; } }); } if (controllers) { forEach(controllers, function(controller) { controller(); }); controllers = null; } // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { linkFn = preLinkFns[i]; invokeLinkFn(linkFn, linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn ); } // RECURSION // We only pass the isolate scope, if the isolate directive has a template, // otherwise the child elements do not belong to the isolate directive. var scopeToChild = scope; if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); // POSTLINKING for (i = postLinkFns.length - 1; i >= 0; i--) { linkFn = postLinkFns[i]; invokeLinkFn(linkFn, linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn ); } // This is the function that is injected as `$transclude`. // Note: all arguments are optional! function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { var transcludeControllers; // No scope passed in: if (!isScope(scope)) { futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } if (!futureParentElement) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } function markDirectivesAsIsolate(directives) { // mark all directives as needing isolate scope. for (var j = 0, jj = directives.length; j < jj; j++) { directives[j] = inherit(directives[j], {$$isolateScope: true}); } } /** * looks up the directive and decorates it with exception handling and proper parameters. We * call this the boundDirective. * * @param {string} name name of the directive to look up. * @param {string} location The directive must be found in specific format. * String containing any of theses characters: * * * `E`: element name * * `A': attribute * * `C`: class * * `M`: comment * @returns {boolean} true if directive was added. */ function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) { if (name === ignoreDirective) return null; var match = null; if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { try { directive = directives[i]; if ((maxPriority === undefined || maxPriority > directive.priority) && directive.restrict.indexOf(location) != -1) { if (startAttrName) { directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); } tDirectives.push(directive); match = directive; } } catch (e) { $exceptionHandler(e); } } } return match; } /** * looks up the directive and returns true if it is a multi-element directive, * and therefore requires DOM nodes between -start and -end markers to be grouped * together. * * @param {string} name name of the directive to look up. * @returns true if directive was registered as multi-element. */ function directiveIsMultiElement(name) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; if (directive.multiElement) { return true; } } } return false; } /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. * The desired effect is to have both of the attributes present. * * @param {object} dst destination attributes (original DOM) * @param {object} src source attributes (from the directive template) */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, dstAttr = dst.$attr, $element = dst.$$element; // reapply the old attributes to the new element forEach(dst, function(value, key) { if (key.charAt(0) != '$') { if (src[key] && src[key] !== value) { value += (key === 'style' ? ';' : ' ') + src[key]; } dst.$set(key, value, true, srcAttr[key]); } }); // copy the new attributes on the old attrs object forEach(src, function(value, key) { if (key == 'class') { safeAddClass($element, value); dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; } else if (key == 'style') { $element.attr('style', $element.attr('style') + ';' + value); dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; // `dst` will never contain hasOwnProperty as DOM parser won't let it. // You will get an "InvalidCharacterError: DOM Exception 5" error if you // have an attribute like "has-own-property" or "data-has-own-property", etc. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { dst[key] = value; dstAttr[key] = srcAttr[key]; } }); } function compileTemplateUrl(directives, $compileNode, tAttrs, $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { var linkQueue = [], afterTemplateNodeLinkFn, afterTemplateChildLinkFn, beforeTemplateCompileNode = $compileNode[0], origAsyncDirective = directives.shift(), derivedSyncDirective = inherit(origAsyncDirective, { templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) : origAsyncDirective.templateUrl, templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); $templateRequest($sce.getTrustedResourceUrl(templateUrl)) .then(function(content) { var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); if (origAsyncDirective.replace) { if (jqLiteIsTextNode(content)) { $template = []; } else { $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", origAsyncDirective.name, templateUrl); } tempTemplateAttrs = {$attr: {}}; replaceWith($rootElement, $compileNode, compileNode); var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); if (isObject(origAsyncDirective.scope)) { markDirectivesAsIsolate(templateDirectives); } directives = templateDirectives.concat(directives); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); } else { compileNode = beforeTemplateCompileNode; $compileNode.html(content); } directives.unshift(derivedSyncDirective); afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { if (node == compileNode) { $rootElement[i] = $compileNode[0]; } }); afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); while (linkQueue.length) { var scope = linkQueue.shift(), beforeTemplateLinkNode = linkQueue.shift(), linkRootElement = linkQueue.shift(), boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; if (scope.$$destroyed) continue; if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; if (!(previousCompileContext.hasElementTranscludeDirective && origAsyncDirective.replace)) { // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); } replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); // Copy in CSS classes from original node safeAddClass(jqLite(linkNode), oldClasses); } if (afterTemplateNodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } else { childBoundTranscludeFn = boundTranscludeFn; } afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, childBoundTranscludeFn); } linkQueue = null; }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { var childBoundTranscludeFn = boundTranscludeFn; if (scope.$$destroyed) return; if (linkQueue) { linkQueue.push(scope, node, rootElement, childBoundTranscludeFn); } else { if (afterTemplateNodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } /** * Sorting function for bound directives. */ function byPriority(a, b) { var diff = b.priority - a.priority; if (diff !== 0) return diff; if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; return a.index - b.index; } function assertNoDuplicate(what, previousDirective, directive, element) { if (previousDirective) { throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', previousDirective.name, directive.name, what, startingTag(element)); } } function addTextInterpolateDirective(directives, text) { var interpolateFn = $interpolate(text, true); if (interpolateFn) { directives.push({ priority: 0, compile: function textInterpolateCompileFn(templateNode) { var templateNodeParent = templateNode.parent(), hasCompileParent = !!templateNodeParent.length; // When transcluding a template that has bindings in the root // we don't have a parent and thus need to add the class during linking fn. if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); return function textInterpolateLinkFn(scope, node) { var parent = node.parent(); if (!hasCompileParent) compile.$$addBindingClass(parent); compile.$$addBindingInfo(parent, interpolateFn.expressions); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; }); }; } }); } } function wrapTemplate(type, template) { type = lowercase(type || 'html'); switch (type) { case 'svg': case 'math': var wrapper = document.createElement('div'); wrapper.innerHTML = '<' + type + '>' + template + ''; return wrapper.childNodes[0].childNodes; default: return template; } } function getTrustedContext(node, attrNormalizedName) { if (attrNormalizedName == "srcdoc") { return $sce.HTML; } var tag = nodeName_(node); // maction[xlink:href] can source SVG. It's not limited to . if (attrNormalizedName == "xlinkHref" || (tag == "form" && attrNormalizedName == "action") || (tag != "img" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { return $sce.RESOURCE_URL; } } function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { var trustedContext = getTrustedContext(node, name); allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; if (name === "multiple" && nodeName_(node) === "select") { throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}", startingTag(node)); } directives.push({ priority: 100, compile: function() { return { pre: function attrInterpolatePreLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = {})); if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { throw $compileMinErr('nodomevents', "Interpolations for HTML DOM event attributes are disallowed. Please use the " + "ng- versions (such as ng-click instead of onclick) instead."); } // If the attribute has changed since last $interpolate()ed var newValue = attr[name]; if (newValue !== value) { // we need to interpolate again since the attribute value has been updated // (e.g. by another directive's compile function) // ensure unset/empty values make interpolateFn falsy interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); value = newValue; } // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; // initialize attr object so that it's ready in case we need the value for isolate // scope initialization, otherwise the value would not be available from isolate // directive's linking fn during linking phase attr[name] = interpolateFn(scope); ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { //special case for class attribute addition + removal //so that class changes can tap into the animation //hooks provided by the $animate service. Be sure to //skip animations when the first digest occurs (when //both the new and the old values are the same) since //the CSS classes are the non-interpolated values if (name === 'class' && newValue != oldValue) { attr.$updateClass(newValue, oldValue); } else { attr.$set(name, newValue); } }); } }; } }); } /** * This is a special jqLite.replaceWith, which can replace items which * have no parents, provided that the containing jqLite collection is provided. * * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes * in the root of the tree. * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep * the shell, but replace its DOM node reference. * @param {Node} newNode The new DOM node. */ function replaceWith($rootElement, elementsToRemove, newNode) { var firstElementToRemove = elementsToRemove[0], removeCount = elementsToRemove.length, parent = firstElementToRemove.parentNode, i, ii; if ($rootElement) { for (i = 0, ii = $rootElement.length; i < ii; i++) { if ($rootElement[i] == firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; j < jj; j++, j2++) { if (j2 < jj) { $rootElement[j] = $rootElement[j2]; } else { delete $rootElement[j]; } } $rootElement.length -= removeCount - 1; // If the replaced element is also the jQuery .context then replace it // .context is a deprecated jQuery api, so we should set it only when jQuery set it // http://api.jquery.com/context/ if ($rootElement.context === firstElementToRemove) { $rootElement.context = newNode; } break; } } } if (parent) { parent.replaceChild(newNode, firstElementToRemove); } // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); // Copy over user data (that includes Angular's $scope etc.). Don't copy private // data here because there's no public interface in jQuery to do that and copying over // event listeners (which is the main use of private data) wouldn't work anyway. jqLite(newNode).data(jqLite(firstElementToRemove).data()); // Remove data of the replaced element. We cannot just call .remove() // on the element it since that would deallocate scope that is needed // for the new node. Instead, remove the data "manually". if (!jQuery) { delete jqLite.cache[firstElementToRemove[jqLite.expando]]; } else { // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after // the replaced element. The cleanData version monkey-patched by Angular would cause // the scope to be trashed and we do need the very same scope to work with the new // element. However, we cannot just cache the non-patched version and use it here as // that would break if another library patches the method after Angular does (one // example is jQuery UI). Instead, set a flag indicating scope destroying should be // skipped this one time. skipDestroyOnNextJQueryCleanData = true; jQuery.cleanData([firstElementToRemove]); } for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { var element = elementsToRemove[k]; jqLite(element).remove(); // must do this way to clean up expando fragment.appendChild(element); delete elementsToRemove[k]; } elementsToRemove[0] = newNode; elementsToRemove.length = 1; } function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { try { linkFn(scope, $element, attrs, controllers, transcludeFn); } catch (e) { $exceptionHandler(e, startingTag($element)); } } }]; } var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { return camelCase(name.replace(PREFIX_REGEXP, '')); } /** * @ngdoc type * @name $compile.directive.Attributes * * @description * A shared object between directive compile / linking functions which contains normalized DOM * element attributes. The values reflect current binding state `{{ }}`. The normalization is * needed since all of these are treated as equivalent in Angular: * * ``` * * ``` */ /** * @ngdoc property * @name $compile.directive.Attributes#$attr * * @description * A map of DOM element attribute names to the normalized name. This is * needed to do reverse lookup from normalized name back to actual name. */ /** * @ngdoc method * @name $compile.directive.Attributes#$set * @kind function * * @description * Set DOM element attribute value. * * * @param {string} name Normalized element attribute name of the property to modify. The name is * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} * property to the original name. * @param {string} value Value to set the attribute to. The value can be an interpolated string. */ /** * Closure compiler type information */ function nodesetLinkingFn( /* angular.Scope */ scope, /* NodeList */ nodeList, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn ) {} function directiveLinkingFn( /* nodesetLinkingFn */ nodesetLinkingFn, /* angular.Scope */ scope, /* Node */ node, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn ) {} function tokenDifference(str1, str2) { var values = '', tokens1 = str1.split(/\s+/), tokens2 = str2.split(/\s+/); outer: for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; for (var j = 0; j < tokens2.length; j++) { if (token == tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } return values; } function removeComments(jqNodes) { jqNodes = jqLite(jqNodes); var i = jqNodes.length; if (i <= 1) { return jqNodes; } while (i--) { var node = jqNodes[i]; if (node.nodeType === NODE_TYPE_COMMENT) { splice.call(jqNodes, i, 1); } } return jqNodes; } var $controllerMinErr = minErr('$controller'); /** * @ngdoc provider * @name $controllerProvider * @description * The {@link ng.$controller $controller service} is used by Angular to create new * controllers. * * This provider allows controller registration via the * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { var controllers = {}, globals = false, CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; /** * @ngdoc method * @name $controllerProvider#register * @param {string|Object} name Controller name, or an object map of controllers where the keys are * the names and the values are the constructors. * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI * annotations in the array notation). */ this.register = function(name, constructor) { assertNotHasOwnProperty(name, 'controller'); if (isObject(name)) { extend(controllers, name); } else { controllers[name] = constructor; } }; /** * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` */ this.allowGlobals = function() { globals = true; }; this.$get = ['$injector', '$window', function($injector, $window) { /** * @ngdoc service * @name $controller * @requires $injector * * @param {Function|string} constructor If called with a function then it's considered to be the * controller constructor function. Otherwise it's considered to be a string which is used * to retrieve the controller constructor using the following steps: * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global * `window` object (not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this * to work correctly. * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. * * @description * `$controller` service is responsible for instantiating controllers. * * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ return function(expression, locals, later, ident) { // PRIVATE API: // param `later` --- indicates that the controller's constructor is invoked at a later time. // If true, $controller will allocate the object with the correct // prototype chain, but will not invoke the controller until a returned // callback is invoked. // param `ident` --- An optional label which overrides the label parsed from the controller // expression, if any. var instance, match, constructor, identifier; later = later === true; if (ident && isString(ident)) { identifier = ident; } if (isString(expression)) { match = expression.match(CNTRL_REG); if (!match) { throw $controllerMinErr('ctrlfmt', "Badly formed controller string '{0}'. " + "Must match `__name__ as __id__` or `__name__`.", expression); } constructor = match[1], identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] : getter(locals.$scope, constructor, true) || (globals ? getter($window, constructor, true) : undefined); assertArgFn(expression, constructor, true); } if (later) { // Instantiate controller later: // This machinery is used to create an instance of the object before calling the // controller's constructor itself. // // This allows properties to be added to the controller before the constructor is // invoked. Primarily, this is used for isolate scope bindings in $compile. // // This feature is not intended for use by applications, and is thus not documented // publicly. // Object creation: http://jsperf.com/create-constructor/2 var controllerPrototype = (isArray(expression) ? expression[expression.length - 1] : expression).prototype; instance = Object.create(controllerPrototype || null); if (identifier) { addIdentifier(locals, identifier, instance, constructor || expression.name); } return extend(function() { $injector.invoke(expression, instance, locals, constructor); return instance; }, { instance: instance, identifier: identifier }); } instance = $injector.instantiate(expression, locals, constructor); if (identifier) { addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; function addIdentifier(locals, identifier, instance, name) { if (!(locals && isObject(locals.$scope))) { throw minErr('$controller')('noscp', "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", name, identifier); } locals.$scope[identifier] = instance; } }]; } /** * @ngdoc service * @name $document * @requires $window * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example

    $document title:

    window.document title:

    angular.module('documentExample', []) .controller('ExampleController', ['$scope', '$document', function($scope, $document) { $scope.title = $document[0].title; $scope.windowTitle = angular.element(window.document)[0].title; }]);
    */ function $DocumentProvider() { this.$get = ['$window', function(window) { return jqLite(window.document); }]; } /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log * * @description * Any uncaught exception in angular expressions is delegated to this service. * The default implementation simply delegates to `$log.error` which logs it into * the browser console. * * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. * * ## Example: * * ```js * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { * return function(exception, cause) { * exception.message += ' (caused by "' + cause + '")'; * throw exception; * }; * }); * ``` * * This example will override the normal action of `$exceptionHandler`, to make angular * exceptions fail hard when they happen, instead of just logging to the console. * *
    * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} * (unless executed during a digest). * * If you wish, you can manually delegate exceptions, e.g. * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. * @param {string=} cause optional information about the context in which * the error was thrown. * */ function $ExceptionHandlerProvider() { this.$get = ['$log', function($log) { return function(exception, cause) { $log.error.apply($log, arguments); }; }]; } var APPLICATION_JSON = 'application/json'; var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; var JSON_START = /^\[|^\{(?!\{)/; var JSON_ENDS = { '[': /]$/, '{': /}$/ }; var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; function defaultHttpResponseTransform(data, headers) { if (isString(data)) { // Strip json vulnerability protection prefix and trim whitespace var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); if (tempData) { var contentType = headers('Content-Type'); if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { data = fromJson(tempData); } } } return data; } function isJsonLike(str) { var jsonStart = str.match(JSON_START); return jsonStart && JSON_ENDS[jsonStart[0]].test(str); } /** * Parse headers into key value object * * @param {string} headers Raw headers as a string * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { var parsed = createMap(), key, val, i; if (!headers) return parsed; forEach(headers.split('\n'), function(line) { i = line.indexOf(':'); key = lowercase(trim(line.substr(0, i))); val = trim(line.substr(i + 1)); if (key) { parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); return parsed; } /** * Returns a function that provides access to parsed headers. * * Headers are lazy parsed when first requested. * @see parseHeaders * * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * * - if called with single an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { var headersObj = isObject(headers) ? headers : undefined; return function(name) { if (!headersObj) headersObj = parseHeaders(headers); if (name) { var value = headersObj[lowercase(name)]; if (value === void 0) { value = null; } return value; } return headersObj; }; } /** * Chain all given functions * * This function is used for both request and response transforming * * @param {*} data Data to transform. * @param {function(string=)} headers HTTP headers getter fn. * @param {number} status HTTP status code of the response. * @param {(Function|Array.)} fns Function or an array of functions. * @returns {*} Transformed data. */ function transformData(data, headers, status, fns) { if (isFunction(fns)) return fns(data, headers, status); forEach(fns, function(fn) { data = fn(data, headers, status); }); return data; } function isSuccess(status) { return 200 <= status && status < 300; } /** * @ngdoc provider * @name $httpProvider * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. * */ function $HttpProvider() { /** * @ngdoc property * @name $httpProvider#defaults * @description * * Object containing default values for all {@link ng.$http $http} requests. * * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} * that will provide the cache for all requests who set their `cache` property to `true`. * If you set the `default.cache = false` then only requests that specify their own custom * cache object will be cached. See {@link $http#caching $http Caching} for more information. * * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. * Defaults value is `'XSRF-TOKEN'`. * * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. * * - **`defaults.headers`** - {Object} - Default headers for all $http requests. * Refer to {@link ng.$http#setting-http-headers $http} for documentation on * setting default headers. * - **`defaults.headers.common`** * - **`defaults.headers.post`** * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * **/ var defaults = this.defaults = { // transform incoming response data transformResponse: [defaultHttpResponseTransform], // transform outgoing request data transformRequest: [function(d) { return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; }], // default headers headers: { common: { 'Accept': 'application/json, text/plain, */*' }, post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) }, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN' }; var useApplyAsync = false; /** * @ngdoc method * @name $httpProvider#useApplyAsync * @description * * Configure $http service to combine processing of multiple http responses received at around * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in * significant performance improvement for bigger applications that make many HTTP requests * concurrently (common during application bootstrap). * * Defaults to false. If no value is specifed, returns the current configured value. * * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window * to load and share the same digest cycle. * * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. * otherwise, returns the current configured value. **/ this.useApplyAsync = function(value) { if (isDefined(value)) { useApplyAsync = !!value; return this; } return useApplyAsync; }; /** * @ngdoc property * @name $httpProvider#interceptors * @description * * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} * pre-processing of request or postprocessing of responses. * * These service factories are ordered by request, i.e. they are applied in the same order as the * array, on request, but reverse order, on response. * * {@link ng.$http#interceptors Interceptors detailed info} **/ var interceptorFactories = this.interceptors = []; this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { var defaultCache = $cacheFactory('$http'); /** * Interceptors stored in reverse order. Inner interceptors before outer interceptors. * The reversal is needed so that we can build up the interception chain around the * server request. */ var reversedInterceptors = []; forEach(interceptorFactories, function(interceptorFactory) { reversedInterceptors.unshift(isString(interceptorFactory) ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); /** * @ngdoc service * @kind function * @name $http * @requires ng.$httpBackend * @requires $cacheFactory * @requires $rootScope * @requires $q * @requires $injector * * @description * The `$http` service is a core Angular service that facilitates communication with the remote * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). * * For unit testing applications that use `$http` service, see * {@link ngMock.$httpBackend $httpBackend mock}. * * For a higher level of abstraction, please check out the {@link ngResource.$resource * $resource} service. * * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage * it is important to familiarize yourself with these APIs and the guarantees they provide. * * * ## General usage * The `$http` service is a function which takes a single argument — a configuration object — * that is used to generate an HTTP request and returns a {@link ng.$q promise} * with two $http specific methods: `success` and `error`. * * ```js * // Simple GET request example : * $http.get('/someUrl'). * success(function(data, status, headers, config) { * // this callback will be called asynchronously * // when the response is available * }). * error(function(data, status, headers, config) { * // called asynchronously if an error occurs * // or server returns response with an error status. * }); * ``` * * ```js * // Simple POST request example (passing data) : * $http.post('/someUrl', {msg:'hello word!'}). * success(function(data, status, headers, config) { * // this callback will be called asynchronously * // when the response is available * }). * error(function(data, status, headers, config) { * // called asynchronously if an error occurs * // or server returns response with an error status. * }); * ``` * * * Since the returned value of calling the $http function is a `promise`, you can also use * the `then` method to register callbacks, and these callbacks will receive a single argument – * an object representing the response. See the API signature and type info below for more * details. * * A response status code between 200 and 299 is considered a success status and * will result in the success callback being called. Note that if the response is a redirect, * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * * ## Writing Unit Tests that use $http * When unit testing (using {@link ngMock ngMock}), it is necessary to call * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending * request using trained responses. * * ``` * $httpBackend.expectGET(...); * $http.get(...); * $httpBackend.flush(); * ``` * * ## Shortcut methods * * Shortcut methods are also available. All shortcut methods require passing in the URL, and * request data must be passed in for POST/PUT requests. * * ```js * $http.get('/someUrl').success(successCallback); * $http.post('/someUrl', data).success(successCallback); * ``` * * Complete list of shortcut methods: * * - {@link ng.$http#get $http.get} * - {@link ng.$http#head $http.head} * - {@link ng.$http#post $http.post} * - {@link ng.$http#put $http.put} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} * - {@link ng.$http#patch $http.patch} * * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): * - `Accept: application/json, text/plain, * / *` * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) * - `Content-Type: application/json` * * To add or overwrite these defaults, simply add or remove a property from these configuration * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object * with the lowercased HTTP method name as the key, e.g. * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. * * The defaults can also be set at runtime via the `$http.defaults` object in the same * fashion. For example: * * ``` * module.run(function($http) { * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' * }); * ``` * * In addition, you can supply a `headers` property in the config object passed when * calling `$http(config)`, which overrides the defaults without changing them globally. * * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, * Use the `headers` property, setting the desired header to `undefined`. For example: * * ```js * var req = { * method: 'POST', * url: 'http://example.com', * headers: { * 'Content-Type': undefined * }, * data: { test: 'test' }, * } * * $http(req).success(function(){...}).error(function(){...}); * ``` * * ## Transforming Requests and Responses * * Both requests and responses can be transformed using transformation functions: `transformRequest` * and `transformResponse`. These properties can be a single function that returns * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, * which allows you to `push` or `unshift` a new transformation function into the transformation chain. * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and * `defaults.transformResponse` properties. If a request does not provide its own transformations * then these will be applied. * * You can augment or replace the default transformations by modifying these properties by adding to or * replacing the array. * * Angular provides the following default transformations: * * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): * * - If XSRF prefix is detected, strip it (see Security Considerations section below). * - If JSON response is detected, deserialize it using a JSON parser. * * * ### Overriding the Default Transformations Per Request * * If you wish override the request/response transformations only for a single request then provide * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * * Note that if you provide these properties on the config object the default transformations will be * overwritten. If you wish to augment the default transformations then you must include them in your * local transformation array. * * The following code demonstrates adding a new response transformation to be run after the default response * transformations have been run. * * ```js * function appendTransform(defaults, transform) { * * // We can't guarantee that the default transformation is an array * defaults = angular.isArray(defaults) ? defaults : [defaults]; * * // Append the new transformation to the defaults * return defaults.concat(transform); * } * * $http({ * url: '...', * method: 'GET', * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { * return doTransform(value); * }) * }); * ``` * * * ## Caching * * To enable caching, set the request configuration `cache` property to `true` (to use default * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). * When the cache is enabled, `$http` stores the response from the server in the specified * cache. The next time the same request is made, the response is served from the cache without * sending a request to the server. * * Note that even if the response is served from cache, delivery of the data is asynchronous in * the same way that real requests are. * * If there are multiple GET requests for the same URL that should be cached using the same * cache, but the cache is not populated yet, only one request to the server will be made and * the remaining requests will be fulfilled using the response from the first request. * * You can change the default cache to a new object (built with * {@link ng.$cacheFactory `$cacheFactory`}) by updating the * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set * their `cache` property to `true` will now use this cache object. * * If you set the default cache to `false` then only requests that specify their own custom * cache object will be cached. * * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. * * For purposes of global error handling, authentication, or any kind of synchronous or * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be * able to intercept requests before they are handed to the server and * responses before they are handed over to the application code that * initiated these requests. The interceptors leverage the {@link ng.$q * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. * * The interceptors are service factories that are registered with the `$httpProvider` by * adding them to the `$httpProvider.interceptors` array. The factory is called and * injected with dependencies (if specified) and returns the interceptor. * * There are two kinds of interceptors (and two kinds of rejection interceptors): * * * `request`: interceptors get called with a http `config` object. The function is free to * modify the `config` object or create a new one. The function needs to return the `config` * object directly, or a promise containing the `config` or a new `config` object. * * `requestError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * * `response`: interceptors get called with http `response` object. The function is free to * modify the `response` object or create a new one. The function needs to return the `response` * object directly, or as a promise containing the `response` or a new `response` object. * * `responseError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * * * ```js * // register the interceptor as a service * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { * return { * // optional method * 'request': function(config) { * // do something on success * return config; * }, * * // optional method * 'requestError': function(rejection) { * // do something on error * if (canRecover(rejection)) { * return responseOrNewPromise * } * return $q.reject(rejection); * }, * * * * // optional method * 'response': function(response) { * // do something on success * return response; * }, * * // optional method * 'responseError': function(rejection) { * // do something on error * if (canRecover(rejection)) { * return responseOrNewPromise * } * return $q.reject(rejection); * } * }; * }); * * $httpProvider.interceptors.push('myHttpInterceptor'); * * * // alternatively, register the interceptor via an anonymous factory * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { * return { * 'request': function(config) { * // same as above * }, * * 'response': function(response) { * // same as above * } * }; * }); * ``` * * ## Security Considerations * * When designing web applications, consider security threats from: * * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) * * Both server and the client must cooperate in order to eliminate these threats. Angular comes * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * * ### JSON Vulnerability Protection * * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * allows third party website to turn your JSON resource URL into * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To * counter this your server can prefix all JSON requests with following string `")]}',\n"`. * Angular will automatically strip the prefix before processing it as JSON. * * For example if your server needs to return: * ```js * ['one','two'] * ``` * * which is vulnerable to attack, your server can return: * ```js * )]}', * ['one','two'] * ``` * * Angular will strip the prefix, before processing the JSON. * * * ### Cross Site Request Forgery (XSRF) Protection * * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which * an unauthorized site can gain your user's private data. Angular provides a mechanism * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only * JavaScript that runs on your domain could read the cookie, your server can be assured that * the XHR came from JavaScript running on your domain. The header will not be set for * cross-domain requests. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, * or the per-request config object. * * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. * - **params** – `{Object.}` – Map of strings or objects which will be turned * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be * JSONified. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the * header will not be sent. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – * `{function(data, headersGetter)|Array.}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request * Overriding the Default Transformations} * - **transformResponse** – * `{function(data, headersGetter, status)|Array.}` – * transform function or an array of such functions. The transform function takes the http * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request * Overriding the Default Transformations} * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the * GET request, otherwise if a cache instance built with * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for * caching. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) * for more information. * - **responseType** - `{string}` - see * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). * * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the * standard `then` method and two http specific methods: `success` and `error`. The `then` * method takes two arguments a success and an error callback which will be called with a * response object. The `success` and `error` methods take a single argument - a function that * will be called when the request succeeds or fails respectively. The arguments passed into * these functions are destructured representation of the response object passed into the * `then` method. The response object has these properties: * * - **data** – `{string|Object}` – The response body transformed with the transform * functions. * - **status** – `{number}` – HTTP status code of the response. * - **headers** – `{function([headerName])}` – Header getter function. * - **config** – `{Object}` – The configuration object that was used to generate the request. * - **statusText** – `{string}` – HTTP status text of the response. * * @property {Array.} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. * * * @example

    http status code: {{status}}
    http response data: {{data}}
    angular.module('httpExample', []) .controller('FetchController', ['$scope', '$http', '$templateCache', function($scope, $http, $templateCache) { $scope.method = 'GET'; $scope.url = 'http-hello.html'; $scope.fetch = function() { $scope.code = null; $scope.response = null; $http({method: $scope.method, url: $scope.url, cache: $templateCache}). success(function(data, status) { $scope.status = status; $scope.data = data; }). error(function(data, status) { $scope.data = data || "Request failed"; $scope.status = status; }); }; $scope.updateModel = function(method, url) { $scope.method = method; $scope.url = url; }; }]); Hello, $http! var status = element(by.binding('status')); var data = element(by.binding('data')); var fetchBtn = element(by.id('fetchbtn')); var sampleGetBtn = element(by.id('samplegetbtn')); var sampleJsonpBtn = element(by.id('samplejsonpbtn')); var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); it('should make an xhr GET request', function() { sampleGetBtn.click(); fetchBtn.click(); expect(status.getText()).toMatch('200'); expect(data.getText()).toMatch(/Hello, \$http!/); }); // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 // it('should make a JSONP request to angularjs.org', function() { // sampleJsonpBtn.click(); // fetchBtn.click(); // expect(status.getText()).toMatch('200'); // expect(data.getText()).toMatch(/Super Hero!/); // }); it('should make JSONP request to invalid URL and invoke the error handler', function() { invalidJsonpBtn.click(); fetchBtn.click(); expect(status.getText()).toMatch('0'); expect(data.getText()).toMatch('Request failed'); });
    */ function $http(requestConfig) { if (!angular.isObject(requestConfig)) { throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse }, requestConfig); config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); var serverRequest = function(config) { var headers = config.headers; var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); // strip content-type if data is undefined if (isUndefined(reqData)) { forEach(headers, function(value, header) { if (lowercase(header) === 'content-type') { delete headers[header]; } }); } if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { config.withCredentials = defaults.withCredentials; } // send request return sendReq(config, reqData).then(transformResponse, transformResponse); }; var chain = [serverRequest, undefined]; var promise = $q.when(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { if (interceptor.request || interceptor.requestError) { chain.unshift(interceptor.request, interceptor.requestError); } if (interceptor.response || interceptor.responseError) { chain.push(interceptor.response, interceptor.responseError); } }); while (chain.length) { var thenFn = chain.shift(); var rejectFn = chain.shift(); promise = promise.then(thenFn, rejectFn); } promise.success = function(fn) { promise.then(function(response) { fn(response.data, response.status, response.headers, config); }); return promise; }; promise.error = function(fn) { promise.then(null, function(response) { fn(response.data, response.status, response.headers, config); }); return promise; }; return promise; function transformResponse(response) { // make a copy since the response must be cacheable var resp = extend({}, response); if (!response.data) { resp.data = response.data; } else { resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); } return (isSuccess(response.status)) ? resp : $q.reject(resp); } function executeHeaderFns(headers) { var headerContent, processedHeaders = {}; forEach(headers, function(headerFn, header) { if (isFunction(headerFn)) { headerContent = headerFn(); if (headerContent != null) { processedHeaders[header] = headerContent; } } else { processedHeaders[header] = headerFn; } }); return processedHeaders; } function mergeHeaders(config) { var defHeaders = defaults.headers, reqHeaders = extend({}, config.headers), defHeaderName, lowercaseDefHeaderName, reqHeaderName; defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); // using for-in instead of forEach to avoid unecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { lowercaseDefHeaderName = lowercase(defHeaderName); for (reqHeaderName in reqHeaders) { if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { continue defaultHeadersIteration; } } reqHeaders[defHeaderName] = defHeaders[defHeaderName]; } // execute if header value is a function for merged headers return executeHeaderFns(reqHeaders); } } $http.pendingRequests = []; /** * @ngdoc method * @name $http#get * * @description * Shortcut method to perform `GET` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method * @name $http#delete * * @description * Shortcut method to perform `DELETE` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method * @name $http#head * * @description * Shortcut method to perform `HEAD` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method * @name $http#jsonp * * @description * Shortcut method to perform `JSONP` request. * * @param {string} url Relative or absolute URL specifying the destination of the request. * The name of the callback should be the string `JSON_CALLBACK`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ createShortMethods('get', 'delete', 'head', 'jsonp'); /** * @ngdoc method * @name $http#post * * @description * Shortcut method to perform `POST` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method * @name $http#put * * @description * Shortcut method to perform `PUT` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method * @name $http#patch * * @description * Shortcut method to perform `PATCH` request. * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property * @name $http#defaults * * @description * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of * default headers, withCredentials as well as request and response transformations. * * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. */ $http.defaults = defaults; return $http; function createShortMethods(names) { forEach(arguments, function(name) { $http[name] = function(url, config) { return $http(extend(config || {}, { method: name, url: url })); }; }); } function createShortMethodsWithData(name) { forEach(arguments, function(name) { $http[name] = function(url, data, config) { return $http(extend(config || {}, { method: name, url: url, data: data })); }; }); } /** * Makes the request. * * !!! ACCESSES CLOSURE VARS: * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests */ function sendReq(config, reqData) { var deferred = $q.defer(), promise = deferred.promise, cache, cachedResp, reqHeaders = config.headers, url = buildUrl(config.url, config.params); $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache : isObject(defaults.cache) ? defaults.cache : defaultCache; } if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); } else { // serving from cache if (isArray(cachedResp)) { resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); } else { resolvePromise(cachedResp, 200, {}, 'OK'); } } } else { // put the promise for the non-transformed response into cache as a placeholder cache.put(url, promise); } } // if we won't have the response in cache, set the xsrf headers and // send the request to the backend if (isUndefined(cachedResp)) { var xsrfValue = urlIsSameOrigin(config.url) ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; } $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, config.withCredentials, config.responseType); } return promise; /** * Callback registered to $httpBackend(): * - caches the response if desired * - resolves the raw $http promise * - calls $apply */ function done(status, response, headersString, statusText) { if (cache) { if (isSuccess(status)) { cache.put(url, [status, response, parseHeaders(headersString), statusText]); } else { // remove promise from the cache cache.remove(url); } } function resolveHttpPromise() { resolvePromise(response, status, headersString, statusText); } if (useApplyAsync) { $rootScope.$applyAsync(resolveHttpPromise); } else { resolveHttpPromise(); if (!$rootScope.$$phase) $rootScope.$apply(); } } /** * Resolves the raw $http promise. */ function resolvePromise(response, status, headers, statusText) { // normalize internal statuses to 0 status = Math.max(status, 0); (isSuccess(status) ? deferred.resolve : deferred.reject)({ data: response, status: status, headers: headersGetter(headers), config: config, statusText: statusText }); } function resolvePromiseWithResult(result) { resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText); } function removePendingReq() { var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } function buildUrl(url, params) { if (!params) return url; var parts = []; forEachSorted(params, function(value, key) { if (value === null || isUndefined(value)) return; if (!isArray(value)) value = [value]; forEach(value, function(v) { if (isObject(v)) { if (isDate(v)) { v = v.toISOString(); } else { v = toJson(v); } } parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(v)); }); }); if (parts.length > 0) { url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); } return url; } }]; } function createXhr() { return new window.XMLHttpRequest(); } /** * @ngdoc service * @name $httpBackend * @requires $window * @requires $document * * @description * HTTP backend used by the {@link ng.$http service} that delegates to * XMLHttpRequest object or JSONP and deals with browser incompatibilities. * * You should never need to use this service directly, instead use the higher-level abstractions: * {@link ng.$http $http} or {@link ngResource.$resource $resource}. * * During testing this implementation is swapped with {@link ngMock.$httpBackend mock * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); }]; } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); if (lowercase(method) == 'jsonp') { var callbackId = '_' + (callbacks.counter++).toString(36); callbacks[callbackId] = function(data) { callbacks[callbackId].data = data; callbacks[callbackId].called = true; }; var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), callbackId, function(status, text) { completeRequest(callback, status, callbacks[callbackId].data, "", text); callbacks[callbackId] = noop; }); } else { var xhr = createXhr(); xhr.open(method, url, true); forEach(headers, function(value, key) { if (isDefined(value)) { xhr.setRequestHeader(key, value); } }); xhr.onload = function requestLoaded() { var statusText = xhr.statusText || ''; // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) var response = ('response' in xhr) ? xhr.response : xhr.responseText; // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) var status = xhr.status === 1223 ? 204 : xhr.status; // fix status code when it is 0 (0 status is undocumented). // Occurs when accessing file resources or on Android 4.1 stock browser // while retrieving files from application cache. if (status === 0) { status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; } completeRequest(callback, status, response, xhr.getAllResponseHeaders(), statusText); }; var requestError = function() { // The response is always empty // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error completeRequest(callback, -1, null, null, ''); }; xhr.onerror = requestError; xhr.onabort = requestError; if (withCredentials) { xhr.withCredentials = true; } if (responseType) { try { xhr.responseType = responseType; } catch (e) { // WebKit added support for the json responseType value on 09/03/2013 // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are // known to throw when setting the value "json" as the response type. Other older // browsers implementing the responseType // // The json response type can be ignored if not supported, because JSON payloads are // parsed on the client-side regardless. if (responseType !== 'json') { throw e; } } } xhr.send(post || null); } if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { jsonpDone && jsonpDone(); xhr && xhr.abort(); } function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution if (timeoutId !== undefined) { $browserDefer.cancel(timeoutId); } jsonpDone = xhr = null; callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; function jsonpReq(url, callbackId, done) { // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document var script = rawDocument.createElement('script'), callback = null; script.type = "text/javascript"; script.src = url; script.async = true; callback = function(event) { removeEventListenerFn(script, "load", callback); removeEventListenerFn(script, "error", callback); rawDocument.body.removeChild(script); script = null; var status = -1; var text = "unknown"; if (event) { if (event.type === "load" && !callbacks[callbackId].called) { event = { type: "error" }; } text = event.type; status = event.type === "error" ? 404 : 200; } if (done) { done(status, text); } }; addEventListenerFn(script, "load", callback); addEventListenerFn(script, "error", callback); rawDocument.body.appendChild(script); return callback; } } var $interpolateMinErr = minErr('$interpolate'); /** * @ngdoc provider * @name $interpolateProvider * * @description * * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. * * @example
    //demo.label//
    it('should interpolate binding with custom symbols', function() { expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); });
    */ function $InterpolateProvider() { var startSymbol = '{{'; var endSymbol = '}}'; /** * @ngdoc method * @name $interpolateProvider#startSymbol * @description * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. * * @param {string=} value new value to set the starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ this.startSymbol = function(value) { if (value) { startSymbol = value; return this; } else { return startSymbol; } }; /** * @ngdoc method * @name $interpolateProvider#endSymbol * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * * @param {string=} value new value to set the ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ this.endSymbol = function(value) { if (value) { endSymbol = value; return this; } else { return endSymbol; } }; this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, endSymbolLength = endSymbol.length, escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); function escape(ch) { return '\\\\\\' + ch; } /** * @ngdoc service * @name $interpolate * @kind function * * @requires $parse * @requires $sce * * @description * * Compiles a string with markup into an interpolation function. This service is used by the * HTML {@link ng.$compile $compile} service for data binding. See * {@link ng.$interpolateProvider $interpolateProvider} for configuring the * interpolation markup. * * * ```js * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name | uppercase}}!'); * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); * ``` * * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is * `true`, the interpolation function will return `undefined` unless all embedded expressions * evaluate to a value other than `undefined`. * * ```js * var $interpolate = ...; // injected * var context = {greeting: 'Hello', name: undefined }; * * // default "forgiving" mode * var exp = $interpolate('{{greeting}} {{name}}!'); * expect(exp(context)).toEqual('Hello !'); * * // "allOrNothing" mode * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); * expect(exp(context)).toBeUndefined(); * context.name = 'Angular'; * expect(exp(context)).toEqual('Hello Angular!'); * ``` * * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. * * ####Escaped Interpolation * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). * It will be rendered as a regular start/end marker, and will not be interpreted as an expression * or binding. * * This enables web-servers to prevent script injection attacks and defacing attacks, to some * degree, while also enabling code examples to work without relying on the * {@link ng.directive:ngNonBindable ngNonBindable} directive. * * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all * interpolation start/end markers with their escaped counterparts.** * * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered * output when the $interpolate service processes the text. So, for HTML elements interpolated * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, * this is typically useful only when user-data is used in rendering a template from the server, or * when otherwise untrusted data is used by a directive. * * * *
    *

    {{apptitle}}: \{\{ username = "defaced value"; \}\} *

    *

    {{username}} attempts to inject code which will deface the * application, but fails to accomplish their task, because the server has correctly * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) * characters.

    *

    Instead, the result of the attempted script injection is visible, and can be removed * from the database by an administrator.

    *
    *
    *
    * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have * embedded expression in order to return an interpolation function. Strings with no * embedded expression will return null for the interpolation function. * @param {string=} trustedContext when provided, the returned function passes the interpolated * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * * - `context`: evaluation context for all expressions embedded in the interpolated text */ function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { allOrNothing = !!allOrNothing; var startIndex, endIndex, index = 0, expressions = [], parseFns = [], textLength = text.length, exp, concat = [], expressionPositions = []; while (index < textLength) { if (((startIndex = text.indexOf(startSymbol, index)) != -1) && ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { if (index !== startIndex) { concat.push(unescapeText(text.substring(index, startIndex))); } exp = text.substring(startIndex + startSymbolLength, endIndex); expressions.push(exp); parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; expressionPositions.push(concat.length); concat.push(''); } else { // we did not find an interpolation, so we have to add the remainder to the separators array if (index !== textLength) { concat.push(unescapeText(text.substring(index))); } break; } } // Concatenating expressions makes it hard to reason about whether some combination of // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a // single expression be used for iframe[src], object[src], etc., we ensure that the value // that's used is assigned or constructed by some JS code somewhere that is more testable or // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. if (trustedContext && concat.length > 1) { throw $interpolateMinErr('noconcat', "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + "interpolations that concatenate multiple expressions when a trusted value is " + "required. See http://docs.angularjs.org/api/ng.$sce", text); } if (!mustHaveExpression || expressions.length) { var compute = function(values) { for (var i = 0, ii = expressions.length; i < ii; i++) { if (allOrNothing && isUndefined(values[i])) return; concat[expressionPositions[i]] = values[i]; } return concat.join(''); }; var getValue = function(value) { return trustedContext ? $sce.getTrusted(trustedContext, value) : $sce.valueOf(value); }; var stringify = function(value) { if (value == null) { // null || undefined return ''; } switch (typeof value) { case 'string': break; case 'number': value = '' + value; break; default: value = toJson(value); } return value; }; return extend(function interpolationFn(context) { var i = 0; var ii = expressions.length; var values = new Array(ii); try { for (; i < ii; i++) { values[i] = parseFns[i](context); } return compute(values); } catch (err) { var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); $exceptionHandler(newErr); } }, { // all of these properties are undocumented for now exp: text, //just for compatibility with regular watchers created via $watch expressions: expressions, $$watchDelegate: function(scope, listener, objectEquality) { var lastValue; return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { var currValue = compute(values); if (isFunction(listener)) { listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); } lastValue = currValue; }, objectEquality); } }); } function unescapeText(text) { return text.replace(escapedStartRegexp, startSymbol). replace(escapedEndRegexp, endSymbol); } function parseStringifyInterceptor(value) { try { value = getValue(value); return allOrNothing && !isDefined(value) ? value : stringify(value); } catch (err) { var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); $exceptionHandler(newErr); } } } /** * @ngdoc method * @name $interpolate#startSymbol * @description * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. * * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change * the symbol. * * @returns {string} start symbol. */ $interpolate.startSymbol = function() { return startSymbol; }; /** * @ngdoc method * @name $interpolate#endSymbol * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change * the symbol. * * @returns {string} end symbol. */ $interpolate.endSymbol = function() { return endSymbol; }; return $interpolate; }]; } function $IntervalProvider() { this.$get = ['$rootScope', '$window', '$q', '$$q', function($rootScope, $window, $q, $$q) { var intervals = {}; /** * @ngdoc service * @name $interval * * @description * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` * milliseconds. * * The return value of registering an interval function is a promise. This promise will be * notified upon each tick of the interval, and will be resolved after `count` iterations, or * run indefinitely if `count` is not defined. The value of the notification will be the * number of iterations that have run. * To cancel an interval, call `$interval.cancel(promise)`. * * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to * move forward by `millis` milliseconds and trigger any functions scheduled to run in that * time. * *
    * **Note**: Intervals created by this service must be explicitly destroyed when you are finished * with them. In particular they are not automatically destroyed when a controller's scope or a * directive's element are destroyed. * You should take this into consideration and make sure to always cancel the interval at the * appropriate moment. See the example below for more details on how and when to do this. *
    * * @param {function()} fn A function that should be called repeatedly. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {promise} A promise which will be notified on each iteration. * * @example * * * * *
    *
    * Date format:
    * Current time is: *
    * Blood 1 : {{blood_1}} * Blood 2 : {{blood_2}} * * * *
    *
    * *
    *
    */ function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, clearInterval = $window.clearInterval, iteration = 0, skipApply = (isDefined(invokeApply) && !invokeApply), deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise; count = isDefined(count) ? count : 0; promise.then(null, null, fn); promise.$$intervalId = setInterval(function tick() { deferred.notify(iteration++); if (count > 0 && iteration >= count) { deferred.resolve(iteration); clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; } if (!skipApply) $rootScope.$apply(); }, delay); intervals[promise.$$intervalId] = deferred; return promise; } /** * @ngdoc method * @name $interval#cancel * * @description * Cancels a task associated with the `promise`. * * @param {promise} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { intervals[promise.$$intervalId].reject('canceled'); $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } return false; }; return interval; }]; } /** * @ngdoc service * @name $locale * * @description * $locale service provides localization rules for various Angular components. As of right now the * only public api is: * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ function $LocaleProvider() { this.$get = function() { return { id: 'en-us', NUMBER_FORMATS: { DECIMAL_SEP: '.', GROUP_SEP: ',', PATTERNS: [ { // Decimal Pattern minInt: 1, minFrac: 0, maxFrac: 3, posPre: '', posSuf: '', negPre: '-', negSuf: '', gSize: 3, lgSize: 3 },{ //Currency Pattern minInt: 1, minFrac: 2, maxFrac: 2, posPre: '\u00A4', posSuf: '', negPre: '(\u00A4', negSuf: ')', gSize: 3, lgSize: 3 } ], CURRENCY_SYM: '$' }, DATETIME_FORMATS: { MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' .split(','), SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), AMPMS: ['AM','PM'], medium: 'MMM d, y h:mm:ss a', 'short': 'M/d/yy h:mm a', fullDate: 'EEEE, MMMM d, y', longDate: 'MMMM d, y', mediumDate: 'MMM d, y', shortDate: 'M/d/yy', mediumTime: 'h:mm:ss a', shortTime: 'h:mm a', ERANAMES: [ "Before Christ", "Anno Domini" ], ERAS: [ "BC", "AD" ] }, pluralCat: function(num) { if (num === 1) { return 'one'; } return 'other'; } }; }; } var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); /** * Encode path using encodeUriSegment, ignoring forward slashes * * @param {string} path Path to encode * @returns {string} */ function encodePath(path) { var segments = path.split('/'), i = segments.length; while (i--) { segments[i] = encodeUriSegment(segments[i]); } return segments.join('/'); } function parseAbsoluteUrl(absoluteUrl, locationObj) { var parsedUrl = urlResolve(absoluteUrl); locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } function parseAppUrl(relativeUrl, locationObj) { var prefixed = (relativeUrl.charAt(0) !== '/'); if (prefixed) { relativeUrl = '/' + relativeUrl; } var match = urlResolve(relativeUrl); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { locationObj.$$path = '/' + locationObj.$$path; } } /** * * @param {string} begin * @param {string} whole * @returns {string} returns text from whole after begin or undefined if it does not begin with * expected string. */ function beginsWith(begin, whole) { if (whole.indexOf(begin) === 0) { return whole.substr(begin.length); } } function stripHash(url) { var index = url.indexOf('#'); return index == -1 ? url : url.substr(0, index); } function trimEmptyHash(url) { return url.replace(/(#.+)|#$/, '$1'); } function stripFile(url) { return url.substr(0, stripHash(url).lastIndexOf('/') + 1); } /* return the server only (scheme://host:port) */ function serverBase(url) { return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); } /** * LocationHtml5Url represents an url * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} basePrefix url path prefix */ function LocationHtml5Url(appBase, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this); /** * Parse given html5 (regular) url string into properties * @param {string} url HTML5 url * @private */ this.$$parse = function(url) { var pathUrl = beginsWith(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } parseAppUrl(pathUrl, this); if (!this.$$path) { this.$$path = '/'; } this.$$compose(); }; /** * Compose url and update `absUrl` property * @private */ this.$$compose = function() { var search = toKeyValue(this.$$search), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; this.$$parseLinkUrl = function(url, relHref) { if (relHref && relHref[0] === '#') { // special case for links to hash fragments: // keep the old url and only replace the hash fragment this.hash(relHref.slice(1)); return true; } var appUrl, prevAppUrl; var rewrittenUrl; if ((appUrl = beginsWith(appBase, url)) !== undefined) { prevAppUrl = appUrl; if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { this.$$parse(rewrittenUrl); } return !!rewrittenUrl; }; } /** * LocationHashbangUrl represents url * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * * @constructor * @param {string} appBase application base URL * @param {string} hashPrefix hashbang prefix */ function LocationHashbangUrl(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this); /** * Parse given hashbang url into properties * @param {string} url Hashbang url * @private */ this.$$parse = function(url) { var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); var withoutHashUrl; if (withoutBaseUrl.charAt(0) === '#') { // The rest of the url starts with a hash so we have // got either a hashbang path or a plain hash fragment withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { // There was no hashbang prefix so we just have a hash fragment withoutHashUrl = withoutBaseUrl; } } else { // There was no hashbang path nor hash fragment: // If we are in HTML5 mode we use what is left as the path; // Otherwise we ignore what is left withoutHashUrl = this.$$html5 ? withoutBaseUrl : ''; } parseAppUrl(withoutHashUrl, this); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); this.$$compose(); /* * In Windows, on an anchor node on documents loaded from * the filesystem, the browser will return a pathname * prefixed with the drive name ('/C:/path') when a * pathname without a drive is set: * * a.setAttribute('href', '/foo') * * a.pathname === '/C:/foo' //true * * Inside of Angular, we're always using pathnames that * do not include drive names for routing. */ function removeWindowsDriveName(path, url, base) { /* Matches paths for file protocol on windows, such as /C:/foo/bar, and captures only /foo/bar. */ var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; var firstPathSegmentMatch; //Get the relative path from the input URL. if (url.indexOf(base) === 0) { url = url.replace(base, ''); } // The input URL intentionally contains a first path segment that ends with a colon. if (windowsFilePathExp.exec(url)) { return path; } firstPathSegmentMatch = windowsFilePathExp.exec(path); return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; } }; /** * Compose hashbang url and update `absUrl` property * @private */ this.$$compose = function() { var search = toKeyValue(this.$$search), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; this.$$parseLinkUrl = function(url, relHref) { if (stripHash(appBase) == stripHash(url)) { this.$$parse(url); return true; } return false; }; } /** * LocationHashbangUrl represents url * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * * @constructor * @param {string} appBase application base URL * @param {string} hashPrefix hashbang prefix */ function LocationHashbangInHtml5Url(appBase, hashPrefix) { this.$$html5 = true; LocationHashbangUrl.apply(this, arguments); var appBaseNoFile = stripFile(appBase); this.$$parseLinkUrl = function(url, relHref) { if (relHref && relHref[0] === '#') { // special case for links to hash fragments: // keep the old url and only replace the hash fragment this.hash(relHref.slice(1)); return true; } var rewrittenUrl; var appUrl; if (appBase == stripHash(url)) { rewrittenUrl = url; } else if ((appUrl = beginsWith(appBaseNoFile, url))) { rewrittenUrl = appBase + hashPrefix + appUrl; } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { this.$$parse(rewrittenUrl); } return !!rewrittenUrl; }; this.$$compose = function() { var search = toKeyValue(this.$$search), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' this.$$absUrl = appBase + hashPrefix + this.$$url; }; } var locationPrototype = { /** * Are we in html5 mode? * @private */ $$html5: false, /** * Has any change been replacing? * @private */ $$replace: false, /** * @ngdoc method * @name $location#absUrl * * @description * This method is getter only. * * Return full url representation with all segments encoded according to rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var absUrl = $location.absUrl(); * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" * ``` * * @return {string} full url */ absUrl: locationGetter('$$absUrl'), /** * @ngdoc method * @name $location#url * * @description * This method is getter / setter. * * Return url (e.g. `/path?a=b#hash`) when called without any parameter. * * Change path, search and hash, when called with parameter and return `$location`. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var url = $location.url(); * // => "/some/path?foo=bar&baz=xoxo" * ``` * * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { if (isUndefined(url)) return this.$$url; var match = PATH_MATCH.exec(url); if (match[1] || url === '') this.path(decodeURIComponent(match[1])); if (match[2] || match[1] || url === '') this.search(match[3] || ''); this.hash(match[5] || ''); return this; }, /** * @ngdoc method * @name $location#protocol * * @description * This method is getter only. * * Return protocol of current url. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * * @return {string} protocol of current url */ protocol: locationGetter('$$protocol'), /** * @ngdoc method * @name $location#host * * @description * This method is getter only. * * Return host of current url. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var host = $location.host(); * // => "example.com" * ``` * * @return {string} host of current url. */ host: locationGetter('$$host'), /** * @ngdoc method * @name $location#port * * @description * This method is getter only. * * Return port of current url. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` * * @return {Number} port */ port: locationGetter('$$port'), /** * @ngdoc method * @name $location#path * * @description * This method is getter / setter. * * Return path of current url when called without any parameter. * * Change path when called with parameter and return `$location`. * * Note: Path should always begin with forward slash (/), this method will add the forward slash * if it is missing. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var path = $location.path(); * // => "/some/path" * ``` * * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { path = path !== null ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), /** * @ngdoc method * @name $location#search * * @description * This method is getter / setter. * * Return search part (as object) of current url when called without any parameter. * * Change search part when called with parameter and return `$location`. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * * // set foo to 'yipee' * $location.search('foo', 'yipee'); * // $location.search() => {foo: 'yipee', baz: 'xoxo'} * ``` * * @param {string|Object.|Object.>} search New search params - string or * hash object. * * When called with a single argument the method acts as a setter, setting the `search` component * of `$location` to the specified value. * * If the argument is a hash object containing an array of values, these values will be encoded * as duplicate search parameters in the url. * * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. * * If `paramValue` is an array, it will override the property of the `search` component of * `$location` specified via the first argument. * * If `paramValue` is `null`, the property specified via the first argument will be deleted. * * If `paramValue` is `true`, the property specified via the first argument will be added with no * value nor trailing equal sign. * * @return {Object} If called with no arguments returns the parsed `search` object. If called with * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { switch (arguments.length) { case 0: return this.$$search; case 1: if (isString(search) || isNumber(search)) { search = search.toString(); this.$$search = parseKeyValue(search); } else if (isObject(search)) { search = copy(search, {}); // remove object undefined or null properties forEach(search, function(value, key) { if (value == null) delete search[key]; }); this.$$search = search; } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); } break; default: if (isUndefined(paramValue) || paramValue === null) { delete this.$$search[search]; } else { this.$$search[search] = paramValue; } } this.$$compose(); return this; }, /** * @ngdoc method * @name $location#hash * * @description * This method is getter / setter. * * Return hash fragment when called without any parameter. * * Change hash fragment when called with parameter and return `$location`. * * * ```js * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` * * @param {(string|number)=} hash New hash fragment * @return {string} hash */ hash: locationGetterSetter('$$hash', function(hash) { return hash !== null ? hash.toString() : ''; }), /** * @ngdoc method * @name $location#replace * * @description * If called, all changes to $location during current `$digest` will be replacing current history * record, instead of adding new one. */ replace: function() { this.$$replace = true; return this; } }; forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { Location.prototype = Object.create(locationPrototype); /** * @ngdoc method * @name $location#state * * @description * This method is getter / setter. * * Return the history state object when called without any parameter. * * Change the history state object when called with one parameter and return `$location`. * The state object is later passed to `pushState` or `replaceState`. * * NOTE: This method is supported only in HTML5 mode and only in browsers supporting * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support * older browsers (like IE9 or Android < 4.0), don't use this method. * * @param {object=} state State object for pushState or replaceState * @return {object} state */ Location.prototype.state = function(state) { if (!arguments.length) return this.$$state; if (Location !== LocationHtml5Url || !this.$$html5) { throw $locationMinErr('nostate', 'History API state support is available only ' + 'in HTML5 mode and only in browsers supporting HTML5 History API'); } // The user might modify `stateObject` after invoking `$location.state(stateObject)` // but we're changing the $$state reference to $browser.state() during the $digest // so the modification window is narrow. this.$$state = isUndefined(state) ? null : state; return this; }; }); function locationGetter(property) { return function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { return function(value) { if (isUndefined(value)) return this[property]; this[property] = preprocess(value); this.$$compose(); return this; }; } /** * @ngdoc service * @name $location * * @requires $rootElement * * @description * The $location service parses the URL in the browser address bar (based on the * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL * available to your application. Changes to the URL in the address bar are reflected into * $location service and changes to $location are reflected into the browser address bar. * * **The $location service:** * * - Exposes the current URL in the browser address bar, so you can * - Watch and observe the URL. * - Change the URL. * - Synchronizes the URL with the browser when the user * - Changes the address bar. * - Clicks the back or forward button (or clicks a History link). * - Clicks on a link. * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * * For more information see {@link guide/$location Developer Guide: Using $location} */ /** * @ngdoc provider * @name $locationProvider * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { var hashPrefix = '', html5Mode = { enabled: false, requireBase: true, rewriteLinks: true }; /** * @ngdoc method * @name $locationProvider#hashPrefix * @description * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.hashPrefix = function(prefix) { if (isDefined(prefix)) { hashPrefix = prefix; return this; } else { return hashPrefix; } }; /** * @ngdoc method * @name $locationProvider#html5Mode * @description * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported * properties: * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not * support `pushState`. * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies * whether or not a tag is required to be present. If `enabled` and `requireBase` are * true, and a base tag is not present, an error will be thrown when `$location` is injected. * See the {@link guide/$location $location guide for more information} * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, * enables/disables url rewriting for relative links. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { if (isBoolean(mode)) { html5Mode.enabled = mode; return this; } else if (isObject(mode)) { if (isBoolean(mode.enabled)) { html5Mode.enabled = mode.enabled; } if (isBoolean(mode.requireBase)) { html5Mode.requireBase = mode.requireBase; } if (isBoolean(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } return this; } else { return html5Mode; } }; /** * @ngdoc event * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description * Broadcasted before a URL will change. * * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. * * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. * @param {string=} newState New history state object * @param {string=} oldState History state object that was before it was changed. */ /** * @ngdoc event * @name $location#$locationChangeSuccess * @eventType broadcast on root scope * @description * Broadcasted after a URL was changed. * * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. * @param {string=} newState New history state object * @param {string=} oldState History state object that was before it was changed. */ this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', function($rootScope, $browser, $sniffer, $rootElement, $window) { var $location, LocationMode, baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' initialUrl = $browser.url(), appBase; if (html5Mode.enabled) { if (!baseHref && html5Mode.requireBase) { throw $locationMinErr('nobase', "$location in HTML5 mode requires a tag to be present!"); } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } $location = new LocationMode(appBase, '#' + hashPrefix); $location.$$parseLinkUrl(initialUrl, initialUrl); $location.$$state = $browser.state(); var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; function setBrowserUrlWithFallback(url, replace, state) { var oldUrl = $location.url(); var oldState = $location.$$state; try { $browser.url(url, replace, state); // Make sure $location.state() returns referentially identical (not just deeply equal) // state object; this makes possible quick checking if the state changed in the digest // loop. Checking deep equality would be too expensive. $location.$$state = $browser.state(); } catch (e) { // Restore old values if pushState fails $location.url(oldUrl); $location.$$state = oldState; throw e; } } $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } var absHref = elm.prop('href'); // get the actual href attribute - see // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during // an animation. absHref = urlResolve(absHref.animVal).href; } // Ignore when url is started with javascript: or mailto: if (IGNORE_URI_REGEXP.test(absHref)) return; if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { if ($location.$$parseLinkUrl(absHref, relHref)) { // We do a preventDefault for all urls that are part of the angular application, // in html5mode and also without, so that we are able to abort navigation without // getting double entries in the location history. event.preventDefault(); // update location manually if ($location.absUrl() != $browser.url()) { $rootScope.$apply(); // hack to work around FF6 bug 684208 when scenario runner clicks on links $window.angular['ff-684208-preventDefault'] = true; } } } }); // rewrite hashbang url <> html5 url if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { $browser.url($location.absUrl(), true); } var initializing = true; // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; $location.$$parse(newUrl); $location.$$state = newState; defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, newState, oldState).defaultPrevented; // if the location was changed by a `$locationChangeStart` handler then stop // processing this location change if ($location.absUrl() !== newUrl) return; if (defaultPrevented) { $location.$$parse(oldUrl); $location.$$state = oldState; setBrowserUrlWithFallback(oldUrl, false, oldState); } else { initializing = false; afterLocationChange(oldUrl, oldState); } }); if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser $rootScope.$watch(function $locationWatch() { var oldUrl = trimEmptyHash($browser.url()); var newUrl = trimEmptyHash($location.absUrl()); var oldState = $browser.state(); var currentReplace = $location.$$replace; var urlOrStateChanged = oldUrl !== newUrl || ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); if (initializing || urlOrStateChanged) { initializing = false; $rootScope.$evalAsync(function() { var newUrl = $location.absUrl(); var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, $location.$$state, oldState).defaultPrevented; // if the location was changed by a `$locationChangeStart` handler then stop // processing this location change if ($location.absUrl() !== newUrl) return; if (defaultPrevented) { $location.$$parse(oldUrl); $location.$$state = oldState; } else { if (urlOrStateChanged) { setBrowserUrlWithFallback(newUrl, currentReplace, oldState === $location.$$state ? null : $location.$$state); } afterLocationChange(oldUrl, oldState); } }); } $location.$$replace = false; // we don't need to return anything because $evalAsync will make the digest loop dirty when // there is a change }); return $location; function afterLocationChange(oldUrl, oldState) { $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, $location.$$state, oldState); } }]; } /** * @ngdoc service * @name $log * @requires $window * * @description * Simple service for logging. Default implementation safely writes the message * into the browser's console (if present). * * The main purpose of this service is to simplify debugging and troubleshooting. * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example angular.module('logExample', []) .controller('LogController', ['$scope', '$log', function($scope, $log) { $scope.$log = $log; $scope.message = 'Hello World!'; }]);

    Reload this page with open console, enter text and hit the log button...

    Message:
    */ /** * @ngdoc provider * @name $logProvider * @description * Use the `$logProvider` to configure how the application logs messages */ function $LogProvider() { var debug = true, self = this; /** * @ngdoc method * @name $logProvider#debugEnabled * @description * @param {boolean=} flag enable or disable debug level messages * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.debugEnabled = function(flag) { if (isDefined(flag)) { debug = flag; return this; } else { return debug; } }; this.$get = ['$window', function($window) { return { /** * @ngdoc method * @name $log#log * * @description * Write a log message */ log: consoleLog('log'), /** * @ngdoc method * @name $log#info * * @description * Write an information message */ info: consoleLog('info'), /** * @ngdoc method * @name $log#warn * * @description * Write a warning message */ warn: consoleLog('warn'), /** * @ngdoc method * @name $log#error * * @description * Write an error message */ error: consoleLog('error'), /** * @ngdoc method * @name $log#debug * * @description * Write a debug message */ debug: (function() { var fn = consoleLog('debug'); return function() { if (debug) { fn.apply(self, arguments); } }; }()) }; function formatError(arg) { if (arg instanceof Error) { if (arg.stack) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; } else if (arg.sourceURL) { arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; } } return arg; } function consoleLog(type) { var console = $window.console || {}, logFn = console[type] || console.log || noop, hasApply = false; // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. // The reason behind this is that console.log has type "object" in IE8... try { hasApply = !!logFn.apply; } catch (e) {} if (hasApply) { return function() { var args = []; forEach(arguments, function(arg) { args.push(formatError(arg)); }); return logFn.apply(console, args); }; } // we are IE which either doesn't have window.console => this is noop and we do nothing, // or we are IE where console.log doesn't have apply so we log at least first 2 args return function(arg1, arg2) { logFn(arg1, arg2 == null ? '' : arg2); }; } }]; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Any commits to this file should be reviewed with security in mind. * * Changes to this file can potentially create security vulnerabilities. * * An approval from 2 Core members with history of modifying * * this file is required. * * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ var $parseMinErr = minErr('$parse'); // Sandboxing Angular Expressions // ------------------------------ // Angular expressions are generally considered safe because these expressions only have direct // access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by // obtaining a reference to native JS functions such as the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // // This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits // against the expression language, but not to prevent exploits that were enabled by exposing // sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good // practice and therefore we are not even trying to protect against interaction with an object // explicitly exposed in this way. // // In general, it is not possible to access a Window object from an angular expression unless a // window or some DOM object that has a reference to window is published onto a Scope. // Similarly we prevent invocations of function known to be dangerous, as well as assignments to // native objects. // // See https://docs.angularjs.org/guide/security function ensureSafeMemberName(name, fullExpression) { if (name === "__defineGetter__" || name === "__defineSetter__" || name === "__lookupGetter__" || name === "__lookupSetter__" || name === "__proto__") { throw $parseMinErr('isecfld', 'Attempting to access a disallowed field in Angular expressions! ' + 'Expression: {0}', fullExpression); } return name; } function ensureSafeObject(obj, fullExpression) { // nifty check if obj is Function that is fast and works across iframes and other contexts if (obj) { if (obj.constructor === obj) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isWindow(obj) obj.window === obj) { throw $parseMinErr('isecwindow', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isElement(obj) obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { throw $parseMinErr('isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// block Object so that we can't get hold of dangerous Object.* methods obj === Object) { throw $parseMinErr('isecobj', 'Referencing Object in Angular expressions is disallowed! Expression: {0}', fullExpression); } } return obj; } var CALL = Function.prototype.call; var APPLY = Function.prototype.apply; var BIND = Function.prototype.bind; function ensureSafeFunction(obj, fullExpression) { if (obj) { if (obj.constructor === obj) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (obj === CALL || obj === APPLY || obj === BIND) { throw $parseMinErr('isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', fullExpression); } } } //Keyword constants var CONSTANTS = createMap(); forEach({ 'null': function() { return null; }, 'true': function() { return true; }, 'false': function() { return false; }, 'undefined': function() {} }, function(constantGetter, name) { constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; CONSTANTS[name] = constantGetter; }); //Not quite a constant, but can be lex/parsed the same CONSTANTS['this'] = function(self) { return self; }; CONSTANTS['this'].sharedGetter = true; //Operators - will be wrapped by binaryFn/unaryFn/assignment/filter var OPERATORS = extend(createMap(), { '+':function(self, locals, a, b) { a=a(self, locals); b=b(self, locals); if (isDefined(a)) { if (isDefined(b)) { return a + b; } return a; } return isDefined(b) ? b : undefined;}, '-':function(self, locals, a, b) { a=a(self, locals); b=b(self, locals); return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0); }, '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);}, '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);}, '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);}, '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);}, '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);}, '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);}, '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);}, '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);}, '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);}, '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);}, '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);}, '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);}, '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);}, '!':function(self, locals, a) {return !a(self, locals);}, //Tokenized as operators but parsed as assignment/filters '=':true, '|':true }); var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; ///////////////////////////////////////// /** * @constructor */ var Lexer = function(options) { this.options = options; }; Lexer.prototype = { constructor: Lexer, lex: function(text) { this.text = text; this.index = 0; this.tokens = []; while (this.index < this.text.length) { var ch = this.text.charAt(this.index); if (ch === '"' || ch === "'") { this.readString(ch); } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); } else if (this.isIdent(ch)) { this.readIdent(); } else if (this.is(ch, '(){}[].,;:?')) { this.tokens.push({index: this.index, text: ch}); this.index++; } else if (this.isWhitespace(ch)) { this.index++; } else { var ch2 = ch + this.peek(); var ch3 = ch2 + this.peek(2); var op1 = OPERATORS[ch]; var op2 = OPERATORS[ch2]; var op3 = OPERATORS[ch3]; if (op1 || op2 || op3) { var token = op3 ? ch3 : (op2 ? ch2 : ch); this.tokens.push({index: this.index, text: token, operator: true}); this.index += token.length; } else { this.throwError('Unexpected next character ', this.index, this.index + 1); } } } return this.tokens; }, is: function(ch, chars) { return chars.indexOf(ch) !== -1; }, peek: function(i) { var num = i || 1; return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; }, isNumber: function(ch) { return ('0' <= ch && ch <= '9') && typeof ch === "string"; }, isWhitespace: function(ch) { // IE treats non-breaking space as \u00A0 return (ch === ' ' || ch === '\r' || ch === '\t' || ch === '\n' || ch === '\v' || ch === '\u00A0'); }, isIdent: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' === ch || ch === '$'); }, isExpOperator: function(ch) { return (ch === '-' || ch === '+' || this.isNumber(ch)); }, throwError: function(error, start, end) { end = end || this.index; var colStr = (isDefined(start) ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' : ' ' + end); throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', error, colStr, this.text); }, readNumber: function() { var number = ''; var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); if (ch == '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); if (ch == 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && number.charAt(number.length - 1) == 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && number.charAt(number.length - 1) == 'e') { this.throwError('Invalid exponent'); } else { break; } } this.index++; } this.tokens.push({ index: start, text: number, constant: true, value: Number(number) }); }, readIdent: function() { var start = this.index; while (this.index < this.text.length) { var ch = this.text.charAt(this.index); if (!(this.isIdent(ch) || this.isNumber(ch))) { break; } this.index++; } this.tokens.push({ index: start, text: this.text.slice(start, this.index), identifier: true }); }, readString: function(quote) { var start = this.index; this.index++; var string = ''; var rawString = quote; var escape = false; while (this.index < this.text.length) { var ch = this.text.charAt(this.index); rawString += ch; if (escape) { if (ch === 'u') { var hex = this.text.substring(this.index + 1, this.index + 5); if (!hex.match(/[\da-f]{4}/i)) this.throwError('Invalid unicode escape [\\u' + hex + ']'); this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; string = string + (rep || ch); } escape = false; } else if (ch === '\\') { escape = true; } else if (ch === quote) { this.index++; this.tokens.push({ index: start, text: rawString, constant: true, value: string }); return; } else { string += ch; } this.index++; } this.throwError('Unterminated quote', start); } }; function isConstant(exp) { return exp.constant; } /** * @constructor */ var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; }; Parser.ZERO = extend(function() { return 0; }, { sharedGetter: true, constant: true }); Parser.prototype = { constructor: Parser, parse: function(text) { this.text = text; this.tokens = this.lexer.lex(text); var value = this.statements(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); } value.literal = !!value.literal; value.constant = !!value.constant; return value; }, primary: function() { var primary; if (this.expect('(')) { primary = this.filterChain(); this.consume(')'); } else if (this.expect('[')) { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); } else if (this.peek().identifier && this.peek().text in CONSTANTS) { primary = CONSTANTS[this.consume().text]; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { primary = this.constant(); } else { this.throwError('not a primary expression', this.peek()); } var next, context; while ((next = this.expect('(', '[', '.'))) { if (next.text === '(') { primary = this.functionCall(primary, context); context = null; } else if (next.text === '[') { context = primary; primary = this.objectIndex(primary); } else if (next.text === '.') { context = primary; primary = this.fieldAccess(primary); } else { this.throwError('IMPOSSIBLE'); } } return primary; }, throwError: function(msg, token) { throw $parseMinErr('syntax', 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); }, peekToken: function() { if (this.tokens.length === 0) throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); return this.tokens[0]; }, peek: function(e1, e2, e3, e4) { return this.peekAhead(0, e1, e2, e3, e4); }, peekAhead: function(i, e1, e2, e3, e4) { if (this.tokens.length > i) { var token = this.tokens[i]; var t = token.text; if (t === e1 || t === e2 || t === e3 || t === e4 || (!e1 && !e2 && !e3 && !e4)) { return token; } } return false; }, expect: function(e1, e2, e3, e4) { var token = this.peek(e1, e2, e3, e4); if (token) { this.tokens.shift(); return token; } return false; }, consume: function(e1) { if (this.tokens.length === 0) { throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); } var token = this.expect(e1); if (!token) { this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); } return token; }, unaryFn: function(op, right) { var fn = OPERATORS[op]; return extend(function $parseUnaryFn(self, locals) { return fn(self, locals, right); }, { constant:right.constant, inputs: [right] }); }, binaryFn: function(left, op, right, isBranching) { var fn = OPERATORS[op]; return extend(function $parseBinaryFn(self, locals) { return fn(self, locals, left, right); }, { constant: left.constant && right.constant, inputs: !isBranching && [left, right] }); }, identifier: function() { var id = this.consume().text; //Continue reading each `.identifier` unless it is a method invocation while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) { id += this.consume().text + this.consume().text; } return getterFn(id, this.options, this.text); }, constant: function() { var value = this.consume().value; return extend(function $parseConstant() { return value; }, { constant: true, literal: true }); }, statements: function() { var statements = []; while (true) { if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) statements.push(this.filterChain()); if (!this.expect(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] : function $parseStatements(self, locals) { var value; for (var i = 0, ii = statements.length; i < ii; i++) { value = statements[i](self, locals); } return value; }; } } }, filterChain: function() { var left = this.expression(); var token; while ((token = this.expect('|'))) { left = this.filter(left); } return left; }, filter: function(inputFn) { var fn = this.$filter(this.consume().text); var argsFn; var args; if (this.peek(':')) { argsFn = []; args = []; // we can safely reuse the array while (this.expect(':')) { argsFn.push(this.expression()); } } var inputs = [inputFn].concat(argsFn || []); return extend(function $parseFilter(self, locals) { var input = inputFn(self, locals); if (args) { args[0] = input; var i = argsFn.length; while (i--) { args[i + 1] = argsFn[i](self, locals); } return fn.apply(undefined, args); } return fn(input); }, { constant: !fn.$stateful && inputs.every(isConstant), inputs: !fn.$stateful && inputs }); }, expression: function() { return this.assignment(); }, assignment: function() { var left = this.ternary(); var right; var token; if ((token = this.expect('='))) { if (!left.assign) { this.throwError('implies assignment but [' + this.text.substring(0, token.index) + '] can not be assigned to', token); } right = this.ternary(); return extend(function $parseAssignment(scope, locals) { return left.assign(scope, right(scope, locals), locals); }, { inputs: [left, right] }); } return left; }, ternary: function() { var left = this.logicalOR(); var middle; var token; if ((token = this.expect('?'))) { middle = this.assignment(); if (this.consume(':')) { var right = this.assignment(); return extend(function $parseTernary(self, locals) { return left(self, locals) ? middle(self, locals) : right(self, locals); }, { constant: left.constant && middle.constant && right.constant }); } } return left; }, logicalOR: function() { var left = this.logicalAND(); var token; while ((token = this.expect('||'))) { left = this.binaryFn(left, token.text, this.logicalAND(), true); } return left; }, logicalAND: function() { var left = this.equality(); var token; while ((token = this.expect('&&'))) { left = this.binaryFn(left, token.text, this.equality(), true); } return left; }, equality: function() { var left = this.relational(); var token; while ((token = this.expect('==','!=','===','!=='))) { left = this.binaryFn(left, token.text, this.relational()); } return left; }, relational: function() { var left = this.additive(); var token; while ((token = this.expect('<', '>', '<=', '>='))) { left = this.binaryFn(left, token.text, this.additive()); } return left; }, additive: function() { var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { left = this.binaryFn(left, token.text, this.multiplicative()); } return left; }, multiplicative: function() { var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { left = this.binaryFn(left, token.text, this.unary()); } return left; }, unary: function() { var token; if (this.expect('+')) { return this.primary(); } else if ((token = this.expect('-'))) { return this.binaryFn(Parser.ZERO, token.text, this.unary()); } else if ((token = this.expect('!'))) { return this.unaryFn(token.text, this.unary()); } else { return this.primary(); } }, fieldAccess: function(object) { var getter = this.identifier(); return extend(function $parseFieldAccess(scope, locals, self) { var o = self || object(scope, locals); return (o == null) ? undefined : getter(o); }, { assign: function(scope, value, locals) { var o = object(scope, locals); if (!o) object.assign(scope, o = {}, locals); return getter.assign(o, value); } }); }, objectIndex: function(obj) { var expression = this.text; var indexFn = this.expression(); this.consume(']'); return extend(function $parseObjectIndex(self, locals) { var o = obj(self, locals), i = indexFn(self, locals), v; ensureSafeMemberName(i, expression); if (!o) return undefined; v = ensureSafeObject(o[i], expression); return v; }, { assign: function(self, value, locals) { var key = ensureSafeMemberName(indexFn(self, locals), expression); // prevent overwriting of Function.constructor which would break ensureSafeObject check var o = ensureSafeObject(obj(self, locals), expression); if (!o) obj.assign(self, o = {}, locals); return o[key] = value; } }); }, functionCall: function(fnGetter, contextGetter) { var argsFn = []; if (this.peekToken().text !== ')') { do { argsFn.push(this.expression()); } while (this.expect(',')); } this.consume(')'); var expressionText = this.text; // we can safely reuse the array across invocations var args = argsFn.length ? [] : null; return function $parseFunctionCall(scope, locals) { var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; var fn = fnGetter(scope, locals, context) || noop; if (args) { var i = argsFn.length; while (i--) { args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); } } ensureSafeObject(context, expressionText); ensureSafeFunction(fn, expressionText); // IE doesn't have apply for some native functions var v = fn.apply ? fn.apply(context, args) : fn(args[0], args[1], args[2], args[3], args[4]); if (args) { // Free-up the memory (arguments of the last function call). args.length = 0; } return ensureSafeObject(v, expressionText); }; }, // This is used with json array declaration arrayDeclaration: function() { var elementFns = []; if (this.peekToken().text !== ']') { do { if (this.peek(']')) { // Support trailing commas per ES5.1. break; } elementFns.push(this.expression()); } while (this.expect(',')); } this.consume(']'); return extend(function $parseArrayLiteral(self, locals) { var array = []; for (var i = 0, ii = elementFns.length; i < ii; i++) { array.push(elementFns[i](self, locals)); } return array; }, { literal: true, constant: elementFns.every(isConstant), inputs: elementFns }); }, object: function() { var keys = [], valueFns = []; if (this.peekToken().text !== '}') { do { if (this.peek('}')) { // Support trailing commas per ES5.1. break; } var token = this.consume(); if (token.constant) { keys.push(token.value); } else if (token.identifier) { keys.push(token.text); } else { this.throwError("invalid key", token); } this.consume(':'); valueFns.push(this.expression()); } while (this.expect(',')); } this.consume('}'); return extend(function $parseObjectLiteral(self, locals) { var object = {}; for (var i = 0, ii = valueFns.length; i < ii; i++) { object[keys[i]] = valueFns[i](self, locals); } return object; }, { literal: true, constant: valueFns.every(isConstant), inputs: valueFns }); } }; ////////////////////////////////////////////////// // Parser helper functions ////////////////////////////////////////////////// function setter(obj, locals, path, setValue, fullExp) { ensureSafeObject(obj, fullExp); ensureSafeObject(locals, fullExp); var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); var propertyObj = (i === 0 && locals && locals[key]) || obj[key]; if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = ensureSafeObject(propertyObj, fullExp); } key = ensureSafeMemberName(element.shift(), fullExp); ensureSafeObject(obj[key], fullExp); obj[key] = setValue; return setValue; } var getterFnCacheDefault = createMap(); var getterFnCacheExpensive = createMap(); function isPossiblyDangerousMemberName(name) { return name == 'constructor'; } /** * Implementation of the "Black Hole" variant from: * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) { ensureSafeMemberName(key0, fullExp); ensureSafeMemberName(key1, fullExp); ensureSafeMemberName(key2, fullExp); ensureSafeMemberName(key3, fullExp); ensureSafeMemberName(key4, fullExp); var eso = function(o) { return ensureSafeObject(o, fullExp); }; var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity; var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity; var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity; var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity; var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity; return function cspSafeGetter(scope, locals) { var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; if (pathVal == null) return pathVal; pathVal = eso0(pathVal[key0]); if (!key1) return pathVal; if (pathVal == null) return undefined; pathVal = eso1(pathVal[key1]); if (!key2) return pathVal; if (pathVal == null) return undefined; pathVal = eso2(pathVal[key2]); if (!key3) return pathVal; if (pathVal == null) return undefined; pathVal = eso3(pathVal[key3]); if (!key4) return pathVal; if (pathVal == null) return undefined; pathVal = eso4(pathVal[key4]); return pathVal; }; } function getterFnWithEnsureSafeObject(fn, fullExpression) { return function(s, l) { return fn(s, l, ensureSafeObject, fullExpression); }; } function getterFn(path, options, fullExp) { var expensiveChecks = options.expensiveChecks; var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); var fn = getterFnCache[path]; if (fn) return fn; var pathKeys = path.split('.'), pathKeysLength = pathKeys.length; // http://jsperf.com/angularjs-parse-getter/6 if (options.csp) { if (pathKeysLength < 6) { fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks); } else { fn = function cspSafeGetter(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp, expensiveChecks)(scope, locals); locals = undefined; // clear after first iteration scope = val; } while (i < pathKeysLength); return val; }; } } else { var code = ''; if (expensiveChecks) { code += 's = eso(s, fe);\nl = eso(l, fe);\n'; } var needsEnsureSafeObject = expensiveChecks; forEach(pathKeys, function(key, index) { ensureSafeMemberName(key, fullExp); var lookupJs = (index // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, and if so read it first : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key; if (expensiveChecks || isPossiblyDangerousMemberName(key)) { lookupJs = 'eso(' + lookupJs + ', fe)'; needsEnsureSafeObject = true; } code += 'if(s == null) return undefined;\n' + 's=' + lookupJs + ';\n'; }); code += 'return s;'; /* jshint -W054 */ var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); if (needsEnsureSafeObject) { evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp); } fn = evaledFnGetter; } fn.sharedGetter = true; fn.assign = function(self, value, locals) { return setter(self, locals, path, value, path); }; getterFnCache[path] = fn; return fn; } var objectValueOf = Object.prototype.valueOf; function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } /////////////////////////////////// /** * @ngdoc service * @name $parse * @kind function * * @description * * Converts Angular {@link guide/expression expression} into a function. * * ```js * var getter = $parse('user.name'); * var setter = getter.assign; * var context = {user:{name:'angular'}}; * var locals = {user:{name:'local'}}; * * expect(getter(context)).toEqual('angular'); * setter(context, 'newValue'); * expect(context.user.name).toEqual('newValue'); * expect(getter(context, locals)).toEqual('local'); * ``` * * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. * * The returned function also has the following properties: * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript * literal. * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript * constant literals. * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be * set to a function to change its value on the given context. * */ /** * @ngdoc provider * @name $parseProvider * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { var $parseOptions = { csp: $sniffer.csp, expensiveChecks: false }, $parseOptionsExpensive = { csp: $sniffer.csp, expensiveChecks: true }; function wrapSharedExpression(exp) { var wrapped = exp; if (exp.sharedGetter) { wrapped = function $parseWrapper(self, locals) { return exp(self, locals); }; wrapped.literal = exp.literal; wrapped.constant = exp.constant; wrapped.assign = exp.assign; } return wrapped; } return function $parse(exp, interceptorFn, expensiveChecks) { var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': cacheKey = exp = exp.trim(); var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { oneTime = true; exp = exp.substring(2); } var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); parsedExpression = parser.parse(exp); if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { //oneTime is not part of the exp passed to the Parser so we may have to //wrap the parsedExpression before adding a $$watchDelegate parsedExpression = wrapSharedExpression(parsedExpression); parsedExpression.$$watchDelegate = parsedExpression.literal ? oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); case 'function': return addInterceptor(exp, interceptorFn); default: return addInterceptor(noop, interceptorFn); } }; function collectExpressionInputs(inputs, list) { for (var i = 0, ii = inputs.length; i < ii; i++) { var input = inputs[i]; if (!input.constant) { if (input.inputs) { collectExpressionInputs(input.inputs, list); } else if (list.indexOf(input) === -1) { // TODO(perf) can we do better? list.push(input); } } } return list; } function expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined return newValue === oldValueOfValue; } if (typeof newValue === 'object') { // attempt to convert the value to a primitive type // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can // be cheaply dirty-checked newValue = getValueOf(newValue); if (typeof newValue === 'object') { // objects/arrays are not supported - deep-watching them would be too expensive return false; } // fall-through to the primitive equality check } //Primitive or NaN return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { var inputExpressions = parsedExpression.$$inputs || (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); var lastResult; if (inputExpressions.length === 1) { var oldInputValue = expressionInputDirtyCheck; // init to something unique so that equals check fails inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { lastResult = parsedExpression(scope); oldInputValue = newInputValue && getValueOf(newInputValue); } return lastResult; }, listener, objectEquality); } var oldInputValueOfValues = []; for (var i = 0, ii = inputExpressions.length; i < ii; i++) { oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails } return scope.$watch(function expressionInputsWatch(scope) { var changed = false; for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } } if (changed) { lastResult = parsedExpression(scope); } return lastResult; }, listener, objectEquality); } function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch, lastValue; return unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); }, function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { listener.apply(this, arguments); } if (isDefined(value)) { scope.$$postDigest(function() { if (isDefined(lastValue)) { unwatch(); } }); } }, objectEquality); } function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch, lastValue; return unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); }, function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { listener.call(this, value, old, scope); } if (isAllDefined(value)) { scope.$$postDigest(function() { if (isAllDefined(lastValue)) unwatch(); }); } }, objectEquality); function isAllDefined(value) { var allDefined = true; forEach(value, function(val) { if (!isDefined(val)) allDefined = false; }); return allDefined; } } function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch; return unwatch = scope.$watch(function constantWatch(scope) { return parsedExpression(scope); }, function constantListener(value, old, scope) { if (isFunction(listener)) { listener.apply(this, arguments); } unwatch(); }, objectEquality); } function addInterceptor(parsedExpression, interceptorFn) { if (!interceptorFn) return parsedExpression; var watchDelegate = parsedExpression.$$watchDelegate; var regularWatch = watchDelegate !== oneTimeLiteralWatchDelegate && watchDelegate !== oneTimeWatchDelegate; var fn = regularWatch ? function regularInterceptedExpression(scope, locals) { var value = parsedExpression(scope, locals); return interceptorFn(value, scope, locals); } : function oneTimeInterceptedExpression(scope, locals) { var value = parsedExpression(scope, locals); var result = interceptorFn(value, scope, locals); // we only return the interceptor's result if the // initial value is defined (for bind-once) return isDefined(value) ? result : value; }; // Propagate $$watchDelegates other then inputsWatchDelegate if (parsedExpression.$$watchDelegate && parsedExpression.$$watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = parsedExpression.$$watchDelegate; } else if (!interceptorFn.$stateful) { // If there is an interceptor, but no watchDelegate then treat the interceptor like // we treat filters - it is assumed to be a pure function unless flagged with $stateful fn.$$watchDelegate = inputsWatchDelegate; fn.inputs = [parsedExpression]; } return fn; } }]; } /** * @ngdoc service * @name $q * @requires $rootScope * * @description * A service that helps you run functions asynchronously, and use their return values (or exceptions) * when they are done processing. * * This is an implementation of promises/deferred objects inspired by * [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred * implementations, and the other which resembles ES6 promises to some degree. * * # $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are * available yet. * * It can be used like so: * * ```js * // for the purpose of this example let's assume that variables `$q` and `okToGreet` * // are available in the current lexical scope (they could have been injected or passed in). * * function asyncGreet(name) { * // perform some asynchronous operation, resolve or reject the promise when appropriate. * return $q(function(resolve, reject) { * setTimeout(function() { * if (okToGreet(name)) { * resolve('Hello, ' + name + '!'); * } else { * reject('Greeting ' + name + ' is not allowed.'); * } * }, 1000); * }); * } * * var promise = asyncGreet('Robin Hood'); * promise.then(function(greeting) { * alert('Success: ' + greeting); * }, function(reason) { * alert('Failed: ' + reason); * }); * ``` * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * * However, the more traditional CommonJS-style usage is still available, and documented below. * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an * interface for interacting with an object that represents the result of an action that is * performed asynchronously, and may or may not be finished at any given point in time. * * From the perspective of dealing with error handling, deferred and promise APIs are to * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * * ```js * // for the purpose of this example let's assume that variables `$q` and `okToGreet` * // are available in the current lexical scope (they could have been injected or passed in). * * function asyncGreet(name) { * var deferred = $q.defer(); * * setTimeout(function() { * deferred.notify('About to greet ' + name + '.'); * * if (okToGreet(name)) { * deferred.resolve('Hello, ' + name + '!'); * } else { * deferred.reject('Greeting ' + name + ' is not allowed.'); * } * }, 1000); * * return deferred.promise; * } * * var promise = asyncGreet('Robin Hood'); * promise.then(function(greeting) { * alert('Success: ' + greeting); * }, function(reason) { * alert('Failed: ' + reason); * }, function(update) { * alert('Got notification: ' + update); * }); * ``` * * At first it might not be obvious why this extra complexity is worth the trouble. The payoff * comes in the way of guarantees that promise and deferred APIs make, see * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. * * Additionally the promise api allows for composition that is very hard to do with the * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. * * The purpose of the deferred object is to expose the associated Promise instance as well as APIs * that can be used for signaling the successful or unsuccessful completion, as well as the status * of the task. * * **Methods** * * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection * constructed via `$q.reject`, the promise will be rejected instead. * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to * resolving it with a rejection constructed via `$q.reject`. * - `notify(value)` - provides updates on the status of the promise's execution. This may be called * multiple times before the promise is either resolved or rejected. * * **Properties** * * - promise – `{Promise}` – promise object associated with this deferred. * * * # The Promise API * * A new promise instance is created when a deferred instance is created and can be retrieved by * calling `deferred.promise`. * * The purpose of the promise object is to allow for interested parties to get access to the result * of the deferred task when it completes. * * **Methods** * * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously * as soon as the result is available. The callbacks are called with a single argument: the result * or rejection reason. Additionally, the notify callback may be called zero or more times to * provide a progress indication, before the promise is resolved or rejected. * * This method *returns a new promise* which is resolved or rejected via the return value of the * `successCallback`, `errorCallback`. It also notifies via the return value of the * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback * method. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, * but to do so without modifying the final value. This is useful to release resources or do some * clean-up that needs to be done whether the promise was rejected or resolved. See the [full * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * * # Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily * possible to create a chain of promises: * * ```js * promiseB = promiseA.then(function(result) { * return result + 1; * }); * * // promiseB will be resolved immediately after promiseA is resolved and its value * // will be the result of promiseA incremented by 1 * ``` * * It is possible to create chains of any length and since a promise can be resolved with another * promise (which will defer its resolution further), it is possible to pause/defer resolution of * the promises at any point in the chain. This makes it possible to implement powerful APIs like * $http's response interceptors. * * * # Differences between Kris Kowal's Q and $q * * There are two main differences: * * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation * mechanism in angular, which means faster propagation of resolution or rejection into your * models and avoiding unnecessary browser repaints, which would result in flickering UI. * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. * * # Testing * * ```js * it('should simulate promise', inject(function($q, $rootScope) { * var deferred = $q.defer(); * var promise = deferred.promise; * var resolvedValue; * * promise.then(function(value) { resolvedValue = value; }); * expect(resolvedValue).toBeUndefined(); * * // Simulate resolving of promise * deferred.resolve(123); * // Note that the 'then' function does not get called synchronously. * // This is because we want the promise API to always be async, whether or not * // it got called synchronously or asynchronously. * expect(resolvedValue).toBeUndefined(); * * // Propagate promise resolution to 'then' functions using $apply(). * $rootScope.$apply(); * expect(resolvedValue).toEqual(123); * })); * ``` * * @param {function(function, function)} resolver Function which is responsible for resolving or * rejecting the newly created promise. The first parameter is a function which resolves the * promise, the second parameter is a function which rejects the promise. * * @returns {Promise} The newly created promise. */ function $QProvider() { this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); }, $exceptionHandler); }]; } function $$QProvider() { this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); }, $exceptionHandler); }]; } /** * Constructs a promise manager. * * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. * @returns {object} Promise manager. */ function qFactory(nextTick, exceptionHandler) { var $qMinErr = minErr('$q', TypeError); function callOnce(self, resolveFn, rejectFn) { var called = false; function wrap(fn) { return function(value) { if (called) return; called = true; fn.call(self, value); }; } return [wrap(resolveFn), wrap(rejectFn)]; } /** * @ngdoc method * @name ng.$q#defer * @kind function * * @description * Creates a `Deferred` object which represents a task which will finish in the future. * * @returns {Deferred} Returns a new instance of deferred. */ var defer = function() { return new Deferred(); }; function Promise() { this.$$state = { status: 0 }; } Promise.prototype = { then: function(onFulfilled, onRejected, progressBack) { var result = new Deferred(); this.$$state.pending = this.$$state.pending || []; this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); return result.promise; }, "catch": function(callback) { return this.then(null, callback); }, "finally": function(callback, progressBack) { return this.then(function(value) { return handleCallback(value, true, callback); }, function(error) { return handleCallback(error, false, callback); }, progressBack); } }; //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native function simpleBind(context, fn) { return function(value) { fn.call(context, value); }; } function processQueue(state) { var fn, promise, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; for (var i = 0, ii = pending.length; i < ii; ++i) { promise = pending[i][0]; fn = pending[i][state.status]; try { if (isFunction(fn)) { promise.resolve(fn(state.value)); } else if (state.status === 1) { promise.resolve(state.value); } else { promise.reject(state.value); } } catch (e) { promise.reject(e); exceptionHandler(e); } } } function scheduleProcessQueue(state) { if (state.processScheduled || !state.pending) return; state.processScheduled = true; nextTick(function() { processQueue(state); }); } function Deferred() { this.promise = new Promise(); //Necessary to support unbound execution :/ this.resolve = simpleBind(this, this.resolve); this.reject = simpleBind(this, this.reject); this.notify = simpleBind(this, this.notify); } Deferred.prototype = { resolve: function(val) { if (this.promise.$$state.status) return; if (val === this.promise) { this.$$reject($qMinErr( 'qcycle', "Expected promise to be resolved with value other than itself '{0}'", val)); } else { this.$$resolve(val); } }, $$resolve: function(val) { var then, fns; fns = callOnce(this, this.$$resolve, this.$$reject); try { if ((isObject(val) || isFunction(val))) then = val && val.then; if (isFunction(then)) { this.promise.$$state.status = -1; then.call(val, fns[0], fns[1], this.notify); } else { this.promise.$$state.value = val; this.promise.$$state.status = 1; scheduleProcessQueue(this.promise.$$state); } } catch (e) { fns[1](e); exceptionHandler(e); } }, reject: function(reason) { if (this.promise.$$state.status) return; this.$$reject(reason); }, $$reject: function(reason) { this.promise.$$state.value = reason; this.promise.$$state.status = 2; scheduleProcessQueue(this.promise.$$state); }, notify: function(progress) { var callbacks = this.promise.$$state.pending; if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { nextTick(function() { var callback, result; for (var i = 0, ii = callbacks.length; i < ii; i++) { result = callbacks[i][0]; callback = callbacks[i][3]; try { result.notify(isFunction(callback) ? callback(progress) : progress); } catch (e) { exceptionHandler(e); } } }); } } }; /** * @ngdoc method * @name $q#reject * @kind function * * @description * Creates a promise that is resolved as rejected with the specified `reason`. This api should be * used to forward rejection in a chain of promises. If you are dealing with the last promise in * a promise chain, you don't need to worry about it. * * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via * a promise error callback and you want to forward the error to the promise derived from the * current promise, you have to "rethrow" the error by returning a rejection constructed via * `reject`. * * ```js * promiseB = promiseA.then(function(result) { * // success: do something and resolve promiseB * // with the old or a new result * return result; * }, function(reason) { * // error: handle the error if possible and * // resolve promiseB with newPromiseOrValue, * // otherwise forward the rejection to promiseB * if (canHandle(reason)) { * // handle the error and recover * return newPromiseOrValue; * } * return $q.reject(reason); * }); * ``` * * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ var reject = function(reason) { var result = new Deferred(); result.reject(reason); return result.promise; }; var makePromise = function makePromise(value, resolved) { var result = new Deferred(); if (resolved) { result.resolve(value); } else { result.reject(value); } return result.promise; }; var handleCallback = function handleCallback(value, isResolved, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { return makePromise(e, false); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { return makePromise(value, isResolved); }, function(error) { return makePromise(error, false); }); } else { return makePromise(value, isResolved); } }; /** * @ngdoc method * @name $q#when * @kind function * * @description * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. * This is useful when you are dealing with an object that might or might not be a promise, or if * the promise comes from a source that can't be trusted. * * @param {*} value Value or a promise * @returns {Promise} Returns a promise of the passed value or promise */ var when = function(value, callback, errback, progressBack) { var result = new Deferred(); result.resolve(value); return result.promise.then(callback, errback, progressBack); }; /** * @ngdoc method * @name $q#all * @kind function * * @description * Combines multiple promises into a single promise that is resolved when all of the input * promises are resolved. * * @param {Array.|Object.} promises An array or hash of promises. * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, * each value corresponding to the promise at the same index/key in the `promises` array/hash. * If any of the promises is resolved with a rejection, this resulting promise will be rejected * with the same rejection value. */ function all(promises) { var deferred = new Deferred(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { if (results.hasOwnProperty(key)) return; results[key] = value; if (!(--counter)) deferred.resolve(results); }, function(reason) { if (results.hasOwnProperty(key)) return; deferred.reject(reason); }); }); if (counter === 0) { deferred.resolve(results); } return deferred.promise; } var $Q = function Q(resolver) { if (!isFunction(resolver)) { throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); } if (!(this instanceof Q)) { // More useful when $Q is the Promise itself. return new Q(resolver); } var deferred = new Deferred(); function resolveFn(value) { deferred.resolve(value); } function rejectFn(reason) { deferred.reject(reason); } resolver(resolveFn, rejectFn); return deferred.promise; }; $Q.defer = defer; $Q.reject = reject; $Q.when = when; $Q.all = all; return $Q; } function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || $window.webkitRequestAnimationFrame; var cancelAnimationFrame = $window.cancelAnimationFrame || $window.webkitCancelAnimationFrame || $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; var raf = rafSupported ? function(fn) { var id = requestAnimationFrame(fn); return function() { cancelAnimationFrame(id); }; } : function(fn) { var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 return function() { $timeout.cancel(timer); }; }; raf.supported = rafSupported; return raf; }]; } /** * DESIGN NOTES * * The design decisions behind the scope are heavily favored for speed and memory consumption. * * The typical use of scope is to watch the expressions, which most of the time return the same * value as last time so we optimize the operation. * * Closures construction is expensive in terms of speed as well as memory: * - No closures, instead use prototypical inheritance for API * - Internal state needs to be stored on scope directly, which means that private state is * exposed as $$____ properties * * Loop operations are optimized by using while(count--) { ... } * - this means that in order to keep the same order of execution as addition we have to add * items to the array at the beginning (unshift) instead of at the end (push) * * Child scopes are created and removed often * - Using an array would be slow since inserts in middle are expensive so we use linked list * * There are few watches then a lot of observers. This is why you don't want the observer to be * implemented in the same way as watch. Watch requires return of initialization function which * are expensive to construct. */ /** * @ngdoc provider * @name $rootScopeProvider * @description * * Provider for the $rootScope service. */ /** * @ngdoc method * @name $rootScopeProvider#digestTtl * @description * * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and * assuming that the model is unstable. * * The current default is 10 iterations. * * In complex applications it's possible that the dependencies between `$watch`s will result in * several digest iterations. However if an application needs more than the default 10 digest * iterations for its model to stabilize then you should investigate what is causing the model to * continuously change during the digest. * * Increasing the TTL could have performance implications, so you should not change it without * proper justification. * * @param {number} limit The number of digest iterations. */ /** * @ngdoc service * @name $rootScope * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. * All other scopes are descendant scopes of the root scope. Scopes provide separation * between the model and the view, via a mechanism for watching the model for changes. * They also provide an event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider() { var TTL = 10; var $rootScopeMinErr = minErr('$rootScope'); var lastDirtyWatch = null; var applyAsyncId = null; this.digestTtl = function(value) { if (arguments.length) { TTL = value; } return TTL; }; function createChildScopeClass(parent) { function ChildScope() { this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$$watchersCount = 0; this.$id = nextUid(); this.$$ChildScope = null; } ChildScope.prototype = parent; return ChildScope; } this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', function($injector, $exceptionHandler, $parse, $browser) { function destroyChildScope($event) { $event.currentScope.$$destroyed = true; } /** * @ngdoc type * @name $rootScope.Scope * * @description * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the * {@link auto.$injector $injector}. Child scopes are created using the * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when * compiled HTML template is executed.) * * Here is a simple scope snippet to show how you can interact with the scope. * ```html * * ``` * * # Inheritance * A scope can inherit from a parent scope, as in this example: * ```js var parent = $rootScope; var child = parent.$new(); parent.salutation = "Hello"; expect(child.salutation).toEqual('Hello'); child.salutation = "Welcome"; expect(child.salutation).toEqual('Welcome'); expect(parent.salutation).toEqual('Hello'); * ``` * * When interacting with `Scope` in tests, additional helper methods are available on the * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional * details. * * * @param {Object.=} providers Map of service factory which need to be * provided for the current scope. Defaults to {@link ng}. * @param {Object.=} instanceCache Provides pre-instantiated services which should * append/override services provided by `providers`. This is handy * when unit-testing and having the need to override a default * service. * @returns {Object} Newly created scope. * */ function Scope() { this.$id = nextUid(); this.$$phase = this.$parent = this.$$watchers = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = null; this.$root = this; this.$$destroyed = false; this.$$listeners = {}; this.$$listenerCount = {}; this.$$isolateBindings = null; } /** * @ngdoc property * @name $rootScope.Scope#$id * * @description * Unique scope ID (monotonically increasing) useful for debugging. */ /** * @ngdoc property * @name $rootScope.Scope#$parent * * @description * Reference to the parent scope. */ /** * @ngdoc property * @name $rootScope.Scope#$root * * @description * Reference to the root scope. */ Scope.prototype = { constructor: Scope, /** * @ngdoc method * @name $rootScope.Scope#$new * @kind function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and * thus stop participating in model change detection and listener notification by invoking. * * @param {boolean} isolate If true, then the scope does not prototypically inherit from the * parent scope. The scope is isolated, as it can not see parent scope properties. * When creating widgets, it is useful for the widget to not accidentally read parent * state. * * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` * of the newly created scope. Defaults to `this` scope if not provided. * This is used when creating a transclude scope to correctly place it * in the scope hierarchy while maintaining the correct prototypical * inheritance. * * @returns {Object} The newly created child scope. * */ $new: function(isolate, parent) { var child; parent = parent || this; if (isolate) { child = new Scope(); child.$root = this.$root; } else { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. if (!this.$$ChildScope) { this.$$ChildScope = createChildScopeClass(this); } child = new this.$$ChildScope(); } child.$parent = parent; child.$$prevSibling = parent.$$childTail; if (parent.$$childHead) { parent.$$childTail.$$nextSibling = child; parent.$$childTail = child; } else { parent.$$childHead = parent.$$childTail = child; } // When the new scope is not isolated or we inherit from `this`, and // the parent scope is destroyed, the property `$$destroyed` is inherited // prototypically. In all other cases, this property needs to be set // when the parent scope is destroyed. // The listener needs to be added after the parent is set if (isolate || parent != this) child.$on('$destroy', destroyChildScope); return child; }, /** * @ngdoc method * @name $rootScope.Scope#$watch * @kind function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest * $digest()} and should return the value that will be watched. (Since * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the * `watchExpression` can execute multiple times per * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) * via the `!==` Javascript operator, unless `objectEquality == true` * (see next point) * - When `objectEquality == true`, inequality of the `watchExpression` is determined * according to the {@link angular.equals} function. To save the value of the object for * later comparison, the {@link angular.copy} function is used. This therefore means that * watching complex objects will have adverse memory and performance implications. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. * * * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a * change is detected, be prepared for multiple calls to your listener.) * * After a watcher is registered with the scope, the `listener` fn is called asynchronously * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the * watcher. In rare cases, this is undesirable because the listener is called when the result * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * * * * # Example * ```js // let's assume that scope was dependency injected as the $rootScope var scope = $rootScope; scope.name = 'misko'; scope.counter = 0; expect(scope.counter).toEqual(0); scope.$watch('name', function(newValue, oldValue) { scope.counter = scope.counter + 1; }); expect(scope.counter).toEqual(0); scope.$digest(); // the listener is always called during the first $digest loop after it was registered expect(scope.counter).toEqual(1); scope.$digest(); // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); scope.name = 'adam'; scope.$digest(); expect(scope.counter).toEqual(2); // Using a function as a watchExpression var food; scope.foodCounter = 0; expect(scope.foodCounter).toEqual(0); scope.$watch( // This function returns the value being watched. It is called for each turn of the $digest loop function() { return food; }, // This is the change listener, called when the value returned from the above function changes function(newValue, oldValue) { if ( newValue !== oldValue ) { // Only increment the counter if the value changed scope.foodCounter = scope.foodCounter + 1; } } ); // No digest has been run so the counter will be zero expect(scope.foodCounter).toEqual(0); // Run the digest but since food has not changed count will still be zero scope.$digest(); expect(scope.foodCounter).toEqual(0); // Update food and run digest. Now the counter will increment food = 'cheeseburger'; scope.$digest(); expect(scope.foodCounter).toEqual(1); * ``` * * * * @param {(function()|string)} watchExpression Expression that is evaluated on each * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers * a call to the `listener`. * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value * of `watchExpression` changes. * * - `newVal` contains the current value of the `watchExpression` * - `oldVal` contains the previous value of the `watchExpression` * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { var get = $parse(watchExp); if (get.$$watchDelegate) { return get.$$watchDelegate(this, listener, objectEquality, get); } var scope = this, array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch = null; if (!isFunction(listener)) { watcher.fn = noop; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }, /** * @ngdoc method * @name $rootScope.Scope#$watchGroup * @kind function * * @description * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. * If any one expression in the collection changes the `listener` is executed. * * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every * call to $digest() to see if any items changes. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.} watchExpressions Array of expressions that will be individually * watched using {@link ng.$rootScope.Scope#$watch $watch()} * * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any * expression in `watchExpressions` changes * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching * those of `watchExpression` * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching * those of `watchExpression` * The `scope` refers to the current scope. * @returns {function()} Returns a de-registration function for all listeners. */ $watchGroup: function(watchExpressions, listener) { var oldValues = new Array(watchExpressions.length); var newValues = new Array(watchExpressions.length); var deregisterFns = []; var self = this; var changeReactionScheduled = false; var firstRun = true; if (!watchExpressions.length) { // No expressions means we call the listener ASAP var shouldCall = true; self.$evalAsync(function() { if (shouldCall) listener(newValues, newValues, self); }); return function deregisterWatchGroup() { shouldCall = false; }; } if (watchExpressions.length === 1) { // Special case size of one return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { newValues[0] = value; oldValues[0] = oldValue; listener(newValues, (value === oldValue) ? newValues : oldValues, scope); }); } forEach(watchExpressions, function(expr, i) { var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { newValues[i] = value; oldValues[i] = oldValue; if (!changeReactionScheduled) { changeReactionScheduled = true; self.$evalAsync(watchGroupAction); } }); deregisterFns.push(unwatchFn); }); function watchGroupAction() { changeReactionScheduled = false; if (firstRun) { firstRun = false; listener(newValues, newValues, self); } else { listener(newValues, oldValues, self); } } return function deregisterWatchGroup() { while (deregisterFns.length) { deregisterFns.shift()(); } }; }, /** * @ngdoc method * @name $rootScope.Scope#$watchCollection * @kind function * * @description * Shallow watches the properties of an object and fires whenever any of the properties change * (for arrays, this implies watching the array items; for object maps, this implies watching * the properties). If a change is detected, the `listener` callback is fired. * * - The `obj` collection is observed via standard $watch operation and is examined on every * call to $digest() to see if any items have been added, removed, or moved. * - The `listener` is called whenever anything within the `obj` has changed. Examples include * adding, removing, and moving items belonging to an object or array. * * * # Example * ```js $scope.names = ['igor', 'matias', 'misko', 'james']; $scope.dataCount = 4; $scope.$watchCollection('names', function(newNames, oldNames) { $scope.dataCount = newNames.length; }); expect($scope.dataCount).toEqual(4); $scope.$digest(); //still at 4 ... no changes expect($scope.dataCount).toEqual(4); $scope.names.pop(); $scope.$digest(); //now there's been a change expect($scope.dataCount).toEqual(3); * ``` * * * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The * expression value should evaluate to an object or an array which is observed on each * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the * collection will trigger a call to the `listener`. * * @param {function(newCollection, oldCollection, scope)} listener a callback function called * when a change is detected. * - The `newCollection` object is the newly modified data obtained from the `obj` expression * - The `oldCollection` object is a copy of the former collection data. * Due to performance considerations, the`oldCollection` value is computed only if the * `listener` function declares two or more arguments. * - The `scope` argument refers to the current scope. * * @returns {function()} Returns a de-registration function for this listener. When the * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { $watchCollectionInterceptor.$stateful = true; var self = this; // the current value, updated on each dirty-check run var newValue; // a shallow copy of the newValue from the last dirty-check run, // updated to match newValue during dirty-check run var oldValue; // a shallow copy of the newValue from when the last change happened var veryOldValue; // only track veryOldValue if the listener is asking for it var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; function $watchCollectionInterceptor(_value) { newValue = _value; var newLength, key, bothNaN, newItem, oldItem; // If the new value is undefined, then return undefined as the watch may be a one-time watch if (isUndefined(newValue)) return; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { oldValue = newValue; changeDetected++; } } else if (isArrayLike(newValue)) { if (oldValue !== internalArray) { // we are transitioning from something which was not an array into array. oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++; } newLength = newValue.length; if (oldLength !== newLength) { // if lengths do not match we need to trigger change notification changeDetected++; oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; } } } else { if (oldValue !== internalObject) { // we are transitioning from something which was not an object into object. oldValue = internalObject = {}; oldLength = 0; changeDetected++; } // copy the items to oldValue and look for changes. newLength = 0; for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; if (key in oldValue) { bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[key] = newItem; } } else { oldLength++; oldValue[key] = newItem; changeDetected++; } } } if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; for (key in oldValue) { if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } } } } return changeDetected; } function $watchCollectionAction() { if (initRun) { initRun = false; listener(newValue, newValue, self); } else { listener(newValue, veryOldValue, self); } // make a copy for the next time a collection is changed if (trackVeryOldValue) { if (!isObject(newValue)) { //primitive veryOldValue = newValue; } else if (isArrayLike(newValue)) { veryOldValue = new Array(newValue.length); for (var i = 0; i < newValue.length; i++) { veryOldValue[i] = newValue[i]; } } else { // if object veryOldValue = {}; for (var key in newValue) { if (hasOwnProperty.call(newValue, key)) { veryOldValue[key] = newValue[key]; } } } } } return this.$watch(changeDetector, $watchCollectionAction); }, /** * @ngdoc method * @name $rootScope.Scope#$digest * @kind function * * @description * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} * until no more listeners are firing. This means that it is possible to get into an infinite * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of * iterations exceeds 10. * * Usually, you don't call `$digest()` directly in * {@link ng.directive:ngController controllers} or in * {@link ng.$compileProvider#directive directives}. * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. * * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * * # Example * ```js var scope = ...; scope.name = 'misko'; scope.counter = 0; expect(scope.counter).toEqual(0); scope.$watch('name', function(newValue, oldValue) { scope.counter = scope.counter + 1; }); expect(scope.counter).toEqual(0); scope.$digest(); // the listener is always called during the first $digest loop after it was registered expect(scope.counter).toEqual(1); scope.$digest(); // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); scope.name = 'adam'; scope.$digest(); expect(scope.counter).toEqual(2); * ``` * */ $digest: function() { var watch, value, last, watchers, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest $browser.$$checkUrlChange(); if (this === $rootScope && applyAsyncId !== null) { // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then // cancel the scheduled $apply and flush the queue of expressions to be evaluated. $browser.defer.cancel(applyAsyncId); flushApplyAsync(); } lastDirtyWatch = null; do { // "while dirty" loop dirty = false; current = target; while (asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; watchLog[logIdx].push({ msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, newVal: value, oldVal: last }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }, /** * @ngdoc event * @name $rootScope.Scope#$destroy * @eventType broadcast on scope being destroyed * * @description * Broadcasted when a scope and its children are being destroyed. * * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to * clean up DOM bindings before an element is removed from the DOM. */ /** * @ngdoc method * @name $rootScope.Scope#$destroy * @kind function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer * propagate to the current scope and its children. Removal also implies that the current * scope is eligible for garbage collection. * * The `$destroy()` is usually used by directives such as * {@link ng.directive:ngRepeat ngRepeat} for managing the * unrolling of the loop. * * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. * Application code can register a `$destroy` event handler that will give it a chance to * perform any necessary cleanup. * * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to * clean up DOM bindings before an element is removed from the DOM. */ $destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; if (this === $rootScope) return; for (var eventName in this.$$listenerCount) { decrementListenerCount(this, this.$$listenerCount[eventName], eventName); } // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; // Disable listeners, watchers and apply/digest methods this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; this.$on = this.$watch = this.$watchGroup = function() { return noop; }; this.$$listeners = {}; // All of the code below is bogus code that works around V8's memory leak via optimized code // and inline caches. // // see: // - https://code.google.com/p/v8/issues/detail?id=2073#c26 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = this.$root = this.$$watchers = null; }, /** * @ngdoc method * @name $rootScope.Scope#$eval * @kind function * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in * the expression are propagated (uncaught). This is useful when evaluating Angular * expressions. * * # Example * ```js var scope = ng.$rootScope.Scope(); scope.a = 1; scope.b = 2; expect(scope.$eval('a+b')).toEqual(3); expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); * ``` * * @param {(string|function())=} expression An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * * @param {(object)=} locals Local variables object, useful for overriding values in scope. * @returns {*} The result of evaluating the expression. */ $eval: function(expr, locals) { return $parse(expr)(this, locals); }, /** * @ngdoc method * @name $rootScope.Scope#$evalAsync * @kind function * * @description * Executes the expression on the current scope at a later point in time. * * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only * that: * * - it will execute after the function that scheduled the evaluation (preferably before DOM * rendering). * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after * `expression` execution. * * Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. * * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle * will be scheduled. However, it is encouraged to always call code that changes the model * from within an `$apply` call. That includes code evaluated via `$evalAsync`. * * @param {(string|function())=} expression An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * * @param {(object)=} locals Local variables object, useful for overriding values in scope. */ $evalAsync: function(expr, locals) { // if we are outside of an $digest loop and this is the first time we are scheduling async // task also schedule async auto-flush if (!$rootScope.$$phase && !asyncQueue.length) { $browser.defer(function() { if (asyncQueue.length) { $rootScope.$digest(); } }); } asyncQueue.push({scope: this, expression: expr, locals: locals}); }, $$postDigest: function(fn) { postDigestQueue.push(fn); }, /** * @ngdoc method * @name $rootScope.Scope#$apply * @kind function * * @description * `$apply()` is used to execute an expression in angular from outside of the angular * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). * Because we are calling into the angular framework we need to perform proper scope life * cycle of {@link ng.$exceptionHandler exception handling}, * {@link ng.$rootScope.Scope#$digest executing watches}. * * ## Life cycle * * # Pseudo-Code of `$apply()` * ```js function $apply(expr) { try { return $eval(expr); } catch (e) { $exceptionHandler(e); } finally { $root.$digest(); } } * ``` * * * Scope's `$apply()` method transitions through the following stages: * * 1. The {@link guide/expression expression} is executed using the * {@link ng.$rootScope.Scope#$eval $eval()} method. * 2. Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * * @param {(string|function())=} exp An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with current `scope` parameter. * * @returns {*} The result of evaluating the expression. */ $apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }, /** * @ngdoc method * @name $rootScope.Scope#$applyAsync * @kind function * * @description * Schedule the invocation of $apply to occur at a later time. The actual time difference * varies across browsers, but is typically around ~10 milliseconds. * * This can be used to queue up multiple expressions which need to be evaluated in the same * digest. * * @param {(string|function())=} exp An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with current `scope` parameter. */ $applyAsync: function(expr) { var scope = this; expr && applyAsyncQueue.push($applyAsyncExpression); scheduleApplyAsync(); function $applyAsyncExpression() { scope.$eval(expr); } }, /** * @ngdoc method * @name $rootScope.Scope#$on * @kind function * * @description * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for * discussion of event life cycle. * * The event listener function format is: `function(event, args...)`. The `event` object * passed into the listener has the following attributes: * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or * `$broadcast`-ed. * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel * further event propagation (available only for events that were `$emit`-ed). * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag * to true. * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. * * @param {string} name Event name to listen on. * @param {function(event, ...args)} listener Function to call when the event is emitted. * @returns {function()} Returns a deregistration function for this listener. */ $on: function(name, listener) { var namedListeners = this.$$listeners[name]; if (!namedListeners) { this.$$listeners[name] = namedListeners = []; } namedListeners.push(listener); var current = this; do { if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++; } while ((current = current.$parent)); var self = this; return function() { var indexOfListener = namedListeners.indexOf(listener); if (indexOfListener !== -1) { namedListeners[indexOfListener] = null; decrementListenerCount(self, 1, name); } }; }, /** * @ngdoc method * @name $rootScope.Scope#$emit * @kind function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$emit` was called. All * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event traverses upwards toward the root scope and calls all * registered listeners along the way. The event will stop propagating if one of the listeners * cancels it. * * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to emit. * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { var empty = [], namedListeners, scope = this, stopPropagation = false, event = { name: name, targetScope: scope, stopPropagation: function() {stopPropagation = true;}, preventDefault: function() { event.defaultPrevented = true; }, defaultPrevented: false }, listenerArgs = concat([event], arguments, 1), i, length; do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; for (i = 0, length = namedListeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!namedListeners[i]) { namedListeners.splice(i, 1); i--; length--; continue; } try { //allow all listeners attached to the current scope to run namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { event.currentScope = null; return event; } //traverse upwards scope = scope.$parent; } while (scope); event.currentScope = null; return event; }, /** * @ngdoc method * @name $rootScope.Scope#$broadcast * @kind function * * @description * Dispatches an event `name` downwards to all child scopes (and their children) notifying the * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$broadcast` was called. All * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event propagates to all direct and indirect scopes of the current * scope and calls all registered listeners along the way. The event cannot be canceled. * * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to broadcast. * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} */ $broadcast: function(name, args) { var target = this, current = target, next = target, event = { name: name, targetScope: target, preventDefault: function() { event.defaultPrevented = true; }, defaultPrevented: false }; if (!target.$$listenerCount[name]) return event; var listenerArgs = concat([event], arguments, 1), listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; for (i = 0, length = listeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!listeners[i]) { listeners.splice(i, 1); i--; length--; continue; } try { listeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest // (though it differs due to having the extra check for $$listenerCount) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } event.currentScope = null; return event; } }; var $rootScope = new Scope(); //The internal queues. Expose them on the $rootScope for debugging/testing purposes. var asyncQueue = $rootScope.$$asyncQueue = []; var postDigestQueue = $rootScope.$$postDigestQueue = []; var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; return $rootScope; function beginPhase(phase) { if ($rootScope.$$phase) { throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); } $rootScope.$$phase = phase; } function clearPhase() { $rootScope.$$phase = null; } function decrementListenerCount(current, count, name) { do { current.$$listenerCount[name] -= count; if (current.$$listenerCount[name] === 0) { delete current.$$listenerCount[name]; } } while ((current = current.$parent)); } /** * function used as an initial value for watchers. * because it's unique we can easily tell it apart from other values */ function initWatchVal() {} function flushApplyAsync() { while (applyAsyncQueue.length) { try { applyAsyncQueue.shift()(); } catch (e) { $exceptionHandler(e); } } applyAsyncId = null; } function scheduleApplyAsync() { if (applyAsyncId === null) { applyAsyncId = $browser.defer(function() { $rootScope.$apply(flushApplyAsync); }); } } }]; } /** * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ function $$SanitizeUriProvider() { var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * * @param {RegExp=} regexp New regexp to whitelist urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ this.aHrefSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { aHrefSanitizationWhitelist = regexp; return this; } return aHrefSanitizationWhitelist; }; /** * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during img[src] sanitization. * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * * Any url about to be assigned to img[src] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * * @param {RegExp=} regexp New regexp to whitelist urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ this.imgSrcSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { imgSrcSanitizationWhitelist = regexp; return this; } return imgSrcSanitizationWhitelist; }; this.$get = function() { return function sanitizeUri(uri, isImage) { var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; var normalizedVal; normalizedVal = urlResolve(uri).href; if (normalizedVal !== '' && !normalizedVal.match(regex)) { return 'unsafe:' + normalizedVal; } return uri; }; }; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Any commits to this file should be reviewed with security in mind. * * Changes to this file can potentially create security vulnerabilities. * * An approval from 2 Core members with history of modifying * * this file is required. * * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { HTML: 'html', CSS: 'css', URL: 'url', // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a // url. (e.g. ng-include, script src, templateUrl) RESOURCE_URL: 'resourceUrl', JS: 'js' }; // Helper functions follow. function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; } else if (isString(matcher)) { // Strings match exactly except for 2 wildcards - '*' and '**'. // '*' matches any character except those from the set ':/.?&'. // '**' matches any character (like .* in a RegExp). // More than 2 *'s raises an error as it's ill defined. if (matcher.indexOf('***') > -1) { throw $sceMinErr('iwcard', 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). replace('\\*\\*', '.*'). replace('\\*', '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. // Match entire URL / disallow partial matches. // Flags are reset (i.e. no global, ignoreCase or multiline) return new RegExp('^' + matcher.source + '$'); } else { throw $sceMinErr('imatcher', 'Matchers may only be "self", string patterns or RegExp objects'); } } function adjustMatchers(matchers) { var adjustedMatchers = []; if (isDefined(matchers)) { forEach(matchers, function(matcher) { adjustedMatchers.push(adjustMatcher(matcher)); }); } return adjustedMatchers; } /** * @ngdoc service * @name $sceDelegate * @kind function * * @description * * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularJS. * * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things * work because `$sce` delegates to `$sceDelegate` for these operations. * * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. * * The default instance of `$sceDelegate` should work out of the box with little pain. While you * can override it completely to change the behavior of `$sce`, the common case would * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist * $sceDelegateProvider.resourceUrlWhitelist} and {@link * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} */ /** * @ngdoc provider * @name $sceDelegateProvider * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure * that the URLs used for sourcing Angular templates are safe. Refer {@link * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} * * For the general details about this service in Angular, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. * * **Example**: Consider the following case. * * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: * * ``` * angular.module('myApp', []).config(function($sceDelegateProvider) { * $sceDelegateProvider.resourceUrlWhitelist([ * // Allow same origin resource loads. * 'self', * // Allow loading from our assets domain. Notice the difference between * and **. * 'http://srv*.assets.example.com/**' * ]); * * // The blacklist overrides the whitelist so the open redirect here is blocked. * $sceDelegateProvider.resourceUrlBlacklist([ * 'http://myapp.example.com/clickThru**' * ]); * }); * ``` */ function $SceDelegateProvider() { this.SCE_CONTEXTS = SCE_CONTEXTS; // Resource URLs can also be trusted by policy. var resourceUrlWhitelist = ['self'], resourceUrlBlacklist = []; /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlWhitelist * @kind function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value * provided. This must be an array or null. A snapshot of this array is used so further * changes to the array are ignored. * * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items * allowed in this array. * * Note: **an empty whitelist array will block all URLs**! * * @return {Array} the currently set whitelist array. * * The **default value** when no whitelist has been explicitly set is `['self']` allowing only * same origin resource requests. * * @description * Sets/Gets the whitelist of trusted resource URLs. */ this.resourceUrlWhitelist = function(value) { if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value); } return resourceUrlWhitelist; }; /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlBlacklist * @kind function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value * provided. This must be an array or null. A snapshot of this array is used so further * changes to the array are ignored. * * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items * allowed in this array. * * The typical usage for the blacklist is to **block * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as * these would otherwise be trusted but actually return content from the redirected domain. * * Finally, **the blacklist overrides the whitelist** and has the final say. * * @return {Array} the currently set blacklist array. * * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there * is no blacklist.) * * @description * Sets/Gets the blacklist of trusted resource URLs. */ this.resourceUrlBlacklist = function(value) { if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value); } return resourceUrlBlacklist; }; this.$get = ['$injector', function($injector) { var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); }; if ($injector.has('$sanitize')) { htmlSanitizer = $injector.get('$sanitize'); } function matchUrl(matcher, parsedUrl) { if (matcher === 'self') { return urlIsSameOrigin(parsedUrl); } else { // definitely a regex. See adjustMatchers() return !!matcher.exec(parsedUrl.href); } } function isResourceUrlAllowedByPolicy(url) { var parsedUrl = urlResolve(url.toString()); var i, n, allowed = false; // Ensure that at least one item from the whitelist allows this url. for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { allowed = true; break; } } if (allowed) { // Ensure that no item from the blacklist blocked this url. for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { allowed = false; break; } } } return allowed; } function generateHolderType(Base) { var holderType = function TrustedValueHolderType(trustedValue) { this.$$unwrapTrustedValue = function() { return trustedValue; }; }; if (Base) { holderType.prototype = new Base(); } holderType.prototype.valueOf = function sceValueOf() { return this.$$unwrapTrustedValue(); }; holderType.prototype.toString = function sceToString() { return this.$$unwrapTrustedValue().toString(); }; return holderType; } var trustedValueHolderBase = generateHolderType(), byType = {}; byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); /** * @ngdoc method * @name $sceDelegate#trustAs * * @description * Returns an object that is trusted by angular for use in specified strict * contextual escaping contexts (such as ng-bind-html, ng-include, any src * attribute interpolation, any dom event binding attribute interpolation * such as for onclick, etc.) that uses the provided value. * See {@link ng.$sce $sce} for enabling strict contextual escaping. * * @param {string} type The kind of context in which this value is safe for use. e.g. url, * resourceUrl, html, js and css. * @param {*} value The value that that should be considered trusted/safe. * @returns {*} A value that can be used to stand in for the provided `value` in places * where Angular expects a $sce.trustAs() return value. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); if (!Constructor) { throw $sceMinErr('icontext', 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', type, trustedValue); } if (trustedValue === null || trustedValue === undefined || trustedValue === '') { return trustedValue; } // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting // mutable objects, we ensure here that the value passed in is actually a string. if (typeof trustedValue !== 'string') { throw $sceMinErr('itype', 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', type); } return new Constructor(trustedValue); } /** * @ngdoc method * @name $sceDelegate#valueOf * * @description * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. * * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} * call or anything else. * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ function valueOf(maybeTrusted) { if (maybeTrusted instanceof trustedValueHolderBase) { return maybeTrusted.$$unwrapTrustedValue(); } else { return maybeTrusted; } } /** * @ngdoc method * @name $sceDelegate#getTrusted * * @description * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and * returns the originally supplied value if the queried context type is a supertype of the * created type. If this condition isn't satisfied, throws an exception. * * @param {string} type The kind of context in which this value is to be used. * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} call. * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. */ function getTrusted(type, maybeTrusted) { if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } // If we get here, then we may only take one of two actions. // 1. sanitize the value for the requested type, or // 2. throw an exception. if (type === SCE_CONTEXTS.RESOURCE_URL) { if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { throw $sceMinErr('insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { return htmlSanitizer(maybeTrusted); } throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } return { trustAs: trustAs, getTrusted: getTrusted, valueOf: valueOf }; }]; } /** * @ngdoc provider * @name $sceProvider * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. * - enable/disable Strict Contextual Escaping (SCE) in a module * - override the default implementation with a custom delegate * * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ /* jshint maxlen: false*/ /** * @ngdoc service * @name $sce * @kind function * * @description * * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. * * # Strict Contextual Escaping * * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain * contexts to result in a value that is marked as safe to use for that context. One example of * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer * to these contexts as privileged or SCE contexts. * * As of version 1.2, Angular ships with SCE enabled by default. * * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow * one to execute arbitrary javascript by the use of the expression() syntax. Refer * to learn more about them. * You can ensure your document is in standards mode and not quirks mode by adding `` * to the top of your HTML document. * * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * * Here's an example of a binding in a privileged context: * * ``` * *
    * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE * disabled, this application allows the user to render arbitrary HTML into the DIV. * In a more realistic example, one may be rendering user comments, blog articles, etc. via * bindings. (HTML is just one example of a context where rendering user controlled input creates * security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. * * How would you ensure that every place that used these types of bindings was bound to a value that * was sanitized by your library (or returned as safe for rendering by your server?) How can you * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some * properties/fields and forgot to update the binding to the sanitized value? * * To be secure by default, you want to ensure that any such bindings are disallowed unless you can * determine that something explicitly says it's safe to use a value for binding in that * context. You can then audit your code (a simple grep would do) to ensure that this is only done * for those values that you can easily tell are safe - because they were received from your server, * sanitized by your library, etc. You can organize your codebase to help with this - perhaps * allowing only the files in a specific directory to do this. Ensuring that the internal API * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. * * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to * obtain values that will be accepted by SCE / privileged contexts. * * * ## How does it work? * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * * ``` * var ngBindHtmlDirective = ['$sce', function($sce) { * return function(scope, element, attr) { * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { * element.html(value || ''); * }); * }; * }]; * ``` * * ## Impact on loading templates * * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as * `templateUrl`'s specified by {@link guide/directive directives}. * * By default, Angular only loads templates from the same domain and protocol as the application * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. * * *Please note*: * The browser's * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) * policy apply in addition to this and may further restrict whether the template is successfully * loaded. This means that without the right CORS policy, loading templates from a different domain * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * * If your expressions are constant literals, they're automatically trusted and you don't need to * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. * `
    `) just works. * * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. * It blocks loading templates from other domains or loading templates over http from an https * served document. You can change these by setting your own custom {@link * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. * * This significantly reduces the overhead. It is far easier to pay the small overhead and have an * application that's secure and can be audited to verify that with much more ease than bolting * security onto an application later. * * * ## What trusted context types are supported? * * | Context | Notes | * |---------------------|----------------| * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
    * * Each element in these arrays must be one of the following: * * - **'self'** * - The special **string**, `'self'`, can be used to match against all URLs of the **same * domain** as the application document using the **same protocol**. * - **String** (except the special value `'self'`) * - The string is matched against the full *normalized / absolute URL* of the resource * being tested (substring matches are not good enough.) * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters * match themselves. * - `*`: matches zero or more occurrences of any character other than one of the following 6 * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use * in a whitelist. * - `**`: matches zero or more occurrences of *any* character. As such, it's not * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). * - **RegExp** (*see caveat below*) * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to * accidentally introduce a bug when one updates a complex expression (imho, all regexes should * have good test coverage.). For instance, the use of `.` in the regex is correct only in a * small number of cases. A `.` character in the regex used when matching the scheme or a * subdomain could be matched against a `:` or literal `.` that was likely not intended. It * is highly recommended to use the string patterns and only fall back to regular expressions * if they as a last resort. * - The regular expression must be an instance of RegExp (i.e. not a string.) It is * matched against the **entire** *normalized / absolute URL* of the resource being tested * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags * present on the RegExp (such as multiline, global, ignoreCase) are ignored. * - If you are generating your JavaScript from some other templating engine (not * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), * remember to escape your regular expression (and be aware that you might need more than * one level of escaping depending on your templating engine and the way you interpolated * the value.) Do make use of your platform's escaping mechanism as it might be good * enough before coding your own. e.g. Ruby has * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). * Javascript lacks a similar built in function for escaping. Take a look at Google * Closure library's [goog.string.regExpEscape(s)]( * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). * * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. * * ## Show me an example using SCE. * * * *
    *

    * User comments
    * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when * $sanitize is available. If $sanitize isn't available, this results in an error instead of an * exploit. *
    *
    * {{userComment.name}}: * *
    *
    *
    *
    *
    * * * angular.module('mySceApp', ['ngSanitize']) * .controller('AppController', ['$http', '$templateCache', '$sce', * function($http, $templateCache, $sce) { * var self = this; * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { * self.userComments = userComments; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * 'Hover over this text.'); * }]); * * * * [ * { "name": "Alice", * "htmlComment": * "Is anyone reading this?" * }, * { "name": "Bob", * "htmlComment": "Yes! Am I the only other one?" * } * ] * * * * describe('SCE doc demo', function() { * it('should sanitize untrusted values', function() { * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) * .toBe('Is anyone reading this?'); * }); * * it('should NOT sanitize explicitly trusted values', function() { * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( * 'Hover over this text.'); * }); * }); * *
    * * * * ## Can I disable SCE completely? * * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and * you're migrating them a module at a time. * * That said, here's how you can completely disable SCE: * * ``` * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { * // Completely disable SCE. For demonstration purposes only! * // Do not use in new projects. * $sceProvider.enabled(false); * }); * ``` * */ /* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; /** * @ngdoc method * @name $sceProvider#enabled * @kind function * * @param {boolean=} value If provided, then enables/disables SCE. * @return {boolean} true if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. */ this.enabled = function(value) { if (arguments.length) { enabled = !!value; } return enabled; }; /* Design notes on the default implementation for SCE. * * The API contract for the SCE delegate * ------------------------------------- * The SCE delegate object must provide the following 3 methods: * * - trustAs(contextEnum, value) * This method is used to tell the SCE service that the provided value is OK to use in the * contexts specified by contextEnum. It must return an object that will be accepted by * getTrusted() for a compatible contextEnum and return this value. * * - valueOf(value) * For values that were not produced by trustAs(), return them as is. For values that were * produced by trustAs(), return the corresponding input value to trustAs. Basically, if * trustAs is wrapping the given values into some type, this operation unwraps it when given * such a value. * * - getTrusted(contextEnum, value) * This function should return the a value that is safe to use in the context specified by * contextEnum or throw and exception otherwise. * * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be * opaque or wrapped in some holder object. That happens to be an implementation detail. For * instance, an implementation could maintain a registry of all trusted objects by context. In * such a case, trustAs() would return the same object that was passed in. getTrusted() would * return the same object passed in if it was found in the registry under a compatible context or * throw an exception otherwise. An implementation might only wrap values some of the time based * on some criteria. getTrusted() might return a value and not throw an exception for special * constants or objects even if not wrapped. All such implementations fulfill this contract. * * * A note on the inheritance model for SCE contexts * ------------------------------------------------ * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This * is purely an implementation details. * * The contract is simply this: * * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) * will also succeed. * * Inheritance happens to capture this in a natural way. In some future, we * may not use inheritance anymore. That is OK because no code outside of * sce.js and sceSpecs.js would need to be aware of this detail. */ this.$get = ['$parse', '$sceDelegate', function( $parse, $sceDelegate) { // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. if (enabled && msie < 8) { throw $sceMinErr('iequirks', 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' + 'mode. You can fix this by adding the text to the top of your HTML ' + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); } var sce = shallowCopy(SCE_CONTEXTS); /** * @ngdoc method * @name $sce#isEnabled * @kind function * * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. * * @description * Returns a boolean indicating if SCE is enabled. */ sce.isEnabled = function() { return enabled; }; sce.trustAs = $sceDelegate.trustAs; sce.getTrusted = $sceDelegate.getTrusted; sce.valueOf = $sceDelegate.valueOf; if (!enabled) { sce.trustAs = sce.getTrusted = function(type, value) { return value; }; sce.valueOf = identity; } /** * @ngdoc method * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * * @param {string} type The kind of SCE context in which this result will be used. * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); if (parsed.literal && parsed.constant) { return parsed; } else { return $parse(expr, function(value) { return sce.getTrusted(type, value); }); } }; /** * @ngdoc method * @name $sce#trustAs * * @description * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, * returns an object that is trusted by angular for use in specified strict contextual * escaping contexts (such as ng-bind-html, ng-include, any src attribute * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual * escaping. * * @param {string} type The kind of context in which this value is safe for use. e.g. url, * resource_url, html, js and css. * @param {*} value The value that that should be considered trusted/safe. * @returns {*} A value that can be used to stand in for the provided `value` in places * where Angular expects a $sce.trustAs() return value. */ /** * @ngdoc method * @name $sce#trustAsHtml * * @description * Shorthand method. `$sce.trustAsHtml(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * * @param {*} value The value to trustAs. * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method * @name $sce#trustAsUrl * * @description * Shorthand method. `$sce.trustAsUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * * @param {*} value The value to trustAs. * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method * @name $sce#trustAsResourceUrl * * @description * Shorthand method. `$sce.trustAsResourceUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * * @param {*} value The value to trustAs. * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the return * value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method * @name $sce#trustAsJs * * @description * Shorthand method. `$sce.trustAsJs(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * * @param {*} value The value to trustAs. * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives * only accept expressions that are either literal constants or are the * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method * @name $sce#getTrusted * * @description * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the * originally supplied value if the queried context type is a supertype of the created type. * If this condition isn't satisfied, throws an exception. * * @param {string} type The kind of context in which this value is to be used. * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} * call. * @returns {*} The value the was originally provided to * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. * Otherwise, throws an exception. */ /** * @ngdoc method * @name $sce#getTrustedHtml * * @description * Shorthand method. `$sce.getTrustedHtml(value)` → * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** * @ngdoc method * @name $sce#getTrustedCss * * @description * Shorthand method. `$sce.getTrustedCss(value)` → * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** * @ngdoc method * @name $sce#getTrustedUrl * * @description * Shorthand method. `$sce.getTrustedUrl(value)` → * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** * @ngdoc method * @name $sce#getTrustedResourceUrl * * @description * Shorthand method. `$sce.getTrustedResourceUrl(value)` → * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** * @ngdoc method * @name $sce#getTrustedJs * * @description * Shorthand method. `$sce.getTrustedJs(value)` → * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** * @ngdoc method * @name $sce#parseAsHtml * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ /** * @ngdoc method * @name $sce#parseAsCss * * @description * Shorthand method. `$sce.parseAsCss(value)` → * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ /** * @ngdoc method * @name $sce#parseAsUrl * * @description * Shorthand method. `$sce.parseAsUrl(value)` → * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ /** * @ngdoc method * @name $sce#parseAsResourceUrl * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ /** * @ngdoc method * @name $sce#parseAsJs * * @description * Shorthand method. `$sce.parseAsJs(value)` → * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. */ // Shorthand delegations. var parse = sce.parseAs, getTrusted = sce.getTrusted, trustAs = sce.trustAs; forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); sce[camelCase("parse_as_" + lName)] = function(expr) { return parse(enumValue, expr); }; sce[camelCase("get_trusted_" + lName)] = function(value) { return getTrusted(enumValue, value); }; sce[camelCase("trust_as_" + lName)] = function(value) { return trustAs(enumValue, value); }; }); return sce; }]; } /** * !!! This is an undocumented "private" service !!! * * @name $sniffer * @requires $window * @requires $document * * @property {boolean} history Does the browser support html5 history api ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * * @description * This is very simple implementation of testing browser's features. */ function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, android = int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, vendorPrefix, vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, animations = false, match; if (bodyStyle) { for (var prop in bodyStyle) { if (match = vendorRegex.exec(prop)) { vendorPrefix = match[0]; vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); break; } } if (!vendorPrefix) { vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; } transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); if (android && (!transitions || !animations)) { transitions = isString(document.body.style.webkitTransition); animations = isString(document.body.style.webkitAnimation); } } return { // Android has history.pushState, but it does not update location correctly // so let's not use the history API at all. // http://code.google.com/p/android/issues/detail?id=17471 // https://github.com/angular/angular.js/issues/904 // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined // jshint -W018 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. // IE10+ implements 'input' event but it erroneously fires under various situations, // e.g. when placeholder changes, or a form is focused. if (event === 'input' && msie <= 11) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); eventSupport[event] = 'on' + event in divElm; } return eventSupport[event]; }, csp: csp(), vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android }; }]; } var $compileMinErr = minErr('$compile'); /** * @ngdoc service * @name $templateRequest * * @description * The `$templateRequest` service downloads the provided template using `$http` and, upon success, * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data * of the HTTP request is empty, a `$compile` error will be thrown (the exception can be thwarted * by setting the 2nd parameter of the function to true). * * @param {string} tpl The HTTP request template URL * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty * * @return {Promise} the HTTP Promise for the given. * * @property {number} totalPendingRequests total amount of pending template requests being downloaded. */ function $TemplateRequestProvider() { this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { function handleRequestFn(tpl, ignoreRequestError) { handleRequestFn.totalPendingRequests++; var transformResponse = $http.defaults && $http.defaults.transformResponse; if (isArray(transformResponse)) { transformResponse = transformResponse.filter(function(transformer) { return transformer !== defaultHttpResponseTransform; }); } else if (transformResponse === defaultHttpResponseTransform) { transformResponse = null; } var httpOptions = { cache: $templateCache, transformResponse: transformResponse }; return $http.get(tpl, httpOptions) ['finally'](function() { handleRequestFn.totalPendingRequests--; }) .then(function(response) { return response.data; }, handleError); function handleError(resp) { if (!ignoreRequestError) { throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); } return $q.reject(resp); } } handleRequestFn.totalPendingRequests = 0; return handleRequestFn; }]; } function $$TestabilityProvider() { this.$get = ['$rootScope', '$browser', '$location', function($rootScope, $browser, $location) { /** * @name $testability * * @description * The private $$testability service provides a collection of methods for use when debugging * or by automated test and debugging tools. */ var testability = {}; /** * @name $$testability#findBindings * * @description * Returns an array of elements that are bound (via ng-bind or {{}}) * to expressions matching the input. * * @param {Element} element The element root to search from. * @param {string} expression The binding expression to match. * @param {boolean} opt_exactMatch If true, only returns exact matches * for the expression. Filters and whitespace are ignored. */ testability.findBindings = function(element, expression, opt_exactMatch) { var bindings = element.getElementsByClassName('ng-binding'); var matches = []; forEach(bindings, function(binding) { var dataBinding = angular.element(binding).data('$binding'); if (dataBinding) { forEach(dataBinding, function(bindingName) { if (opt_exactMatch) { var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); if (matcher.test(bindingName)) { matches.push(binding); } } else { if (bindingName.indexOf(expression) != -1) { matches.push(binding); } } }); } }); return matches; }; /** * @name $$testability#findModels * * @description * Returns an array of elements that are two-way found via ng-model to * expressions matching the input. * * @param {Element} element The element root to search from. * @param {string} expression The model expression to match. * @param {boolean} opt_exactMatch If true, only returns exact matches * for the expression. */ testability.findModels = function(element, expression, opt_exactMatch) { var prefixes = ['ng-', 'data-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var attributeEquals = opt_exactMatch ? '=' : '*='; var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; var elements = element.querySelectorAll(selector); if (elements.length) { return elements; } } }; /** * @name $$testability#getLocation * * @description * Shortcut for getting the location in a browser agnostic way. Returns * the path, search, and hash. (e.g. /path?a=b#hash) */ testability.getLocation = function() { return $location.url(); }; /** * @name $$testability#setLocation * * @description * Shortcut for navigating to a location without doing a full page reload. * * @param {string} url The location url (path, search and hash, * e.g. /path?a=b#hash) to go to. */ testability.setLocation = function(url) { if (url !== $location.url()) { $location.url(url); $rootScope.$digest(); } }; /** * @name $$testability#whenStable * * @description * Calls the callback when $timeout and $http requests are completed. * * @param {function} callback */ testability.whenStable = function(callback) { $browser.notifyWhenNoOutstandingRequests(callback); }; return testability; }]; } function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; /** * @ngdoc service * @name $timeout * * @description * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch * block and delegates any exceptions to * {@link ng.$exceptionHandler $exceptionHandler} service. * * The return value of registering a timeout function is a promise, which will be resolved when * the timeout is reached and the timeout function is executed. * * To cancel a timeout request, call `$timeout.cancel(promise)`. * * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to * synchronously flush the queue of deferred functions. * * @param {function()} fn A function, whose execution should be delayed. * @param {number=} [delay=0] Delay in milliseconds. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { var skipApply = (isDefined(invokeApply) && !invokeApply), deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, timeoutId; timeoutId = $browser.defer(function() { try { deferred.resolve(fn()); } catch (e) { deferred.reject(e); $exceptionHandler(e); } finally { delete deferreds[promise.$$timeoutId]; } if (!skipApply) $rootScope.$apply(); }, delay); promise.$$timeoutId = timeoutId; deferreds[timeoutId] = deferred; return promise; } /** * @ngdoc method * @name $timeout#cancel * * @description * Cancels a task associated with the `promise`. As a result of this, the promise will be * resolved with a rejection. * * @param {Promise=} promise Promise returned by the `$timeout` function. * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully * canceled. */ timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { deferreds[promise.$$timeoutId].reject('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); } return false; }; return timeout; }]; } // NOTE: The usage of window and document instead of $window and $document here is // deliberate. This service depends on the specific behavior of anchor nodes created by the // browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and // cause us to break tests. In addition, when the browser resolves a URL for XHR, it // doesn't know about mocked locations and resolves URLs to the real document - which is // exactly the behavior needed here. There is little value is mocking these out for this // service. var urlParsingNode = document.createElement("a"); var originUrl = urlResolve(window.location.href); /** * * Implementation Notes for non-IE browsers * ---------------------------------------- * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, * results both in the normalizing and parsing of the URL. Normalizing means that a relative * URL will be resolved into an absolute URL in the context of the application document. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related * properties are all populated to reflect the normalized URL. This approach has wide * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE * --------------------------- * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other * browsers. However, the parsed components will not be set if the URL assigned did not specify * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We * work around that by performing the parsing in a 2nd step by taking a previously normalized * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the * properties such as protocol, hostname, port, etc. * * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one * uses the inner HTML approach to assign the URL as part of an HTML snippet - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that * method and IE < 8 is unsupported. * * References: * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * http://url.spec.whatwg.org/#urlutils * https://github.com/angular/angular.js/pull/2902 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * * @kind function * @param {string} url The URL to be parsed. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. * * | member name | Description | * |---------------|----------------| * | href | A normalized version of the provided URL if it was not an absolute URL | * | protocol | The protocol including the trailing colon | * | host | The host and port (if the port is non-default) of the normalizedUrl | * | search | The search params, minus the question mark | * | hash | The hash string, minus the hash symbol * | hostname | The hostname * | port | The port, without ":" * | pathname | The pathname, beginning with "/" * */ function urlResolve(url) { var href = url; if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. urlParsingNode.setAttribute("href", href); href = urlParsingNode.href; } urlParsingNode.setAttribute('href', href); // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils return { href: urlParsingNode.href, protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', host: urlParsingNode.host, search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', hostname: urlParsingNode.hostname, port: urlParsingNode.port, pathname: (urlParsingNode.pathname.charAt(0) === '/') ? urlParsingNode.pathname : '/' + urlParsingNode.pathname }; } /** * Parse a request URL and determine whether this is a same-origin request as the application document. * * @param {string|object} requestUrl The url of the request as a string that will be resolved * or a parsed URL object. * @returns {boolean} Whether the request is for the same origin as the application document. */ function urlIsSameOrigin(requestUrl) { var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; return (parsed.protocol === originUrl.protocol && parsed.host === originUrl.host); } /** * @ngdoc service * @name $window * * @description * A reference to the browser's `window` object. While `window` * is globally available in JavaScript, it causes testability problems, because * it is a global variable. In angular we always refer to it through the * `$window` service, so it may be overridden, removed or mocked for testing. * * Expressions, like the one defined for the `ngClick` directive in the example * below, are evaluated with respect to the current scope. Therefore, there is * no risk of inadvertently coding in a dependency on a global value in such an * expression. * * @example
    it('should display the greeting in the input box', function() { element(by.model('greeting')).sendKeys('Hello, E2E Tests'); // If we click the button it will block the test runner // element(':button').click(); });
    */ function $WindowProvider() { this.$get = valueFn(window); } /* global currencyFilter: true, dateFilter: true, filterFilter: true, jsonFilter: true, limitToFilter: true, lowercaseFilter: true, numberFilter: true, orderByFilter: true, uppercaseFilter: true, */ /** * @ngdoc provider * @name $filterProvider * @description * * Filters are just functions which transform input to an output. However filters need to be * Dependency Injected. To achieve this a filter definition consists of a factory function which is * annotated with dependencies and is responsible for creating a filter function. * * ```js * // Filter registration * function MyModule($provide, $filterProvider) { * // create a service to demonstrate injection (not always needed) * $provide.value('greet', function(name){ * return 'Hello ' + name + '!'; * }); * * // register a filter factory which uses the * // greet service to demonstrate DI. * $filterProvider.register('greet', function(greet){ * // return the filter function which uses the greet service * // to generate salutation * return function(text) { * // filters need to be forgiving so check input validity * return text && greet(text) || text; * }; * }); * } * ``` * * The filter function is registered with the `$injector` under the filter name suffix with * `Filter`. * * ```js * it('should be the same instance', inject( * function($filterProvider) { * $filterProvider.register('reverse', function(){ * return ...; * }); * }, * function($filter, reverseFilter) { * expect($filter('reverse')).toBe(reverseFilter); * }); * ``` * * * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ /** * @ngdoc service * @name $filter * @kind function * @description * Filters are used for formatting data displayed to the user. * * The general syntax in templates is as follows: * * {{ expression [| filter_name[:parameter_value] ... ] }} * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function * @example

    {{ originalText }}

    {{ filteredText }}

    angular.module('filterExample', []) .controller('MainCtrl', function($scope, $filter) { $scope.originalText = 'hello'; $scope.filteredText = $filter('uppercase')($scope.originalText); });
    */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; /** * @ngdoc method * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ function register(name, factory) { if (isObject(name)) { var filters = {}; forEach(name, function(filter, key) { filters[key] = register(key, filter); }); return filters; } else { return $provide.factory(name + suffix, factory); } } this.register = register; this.$get = ['$injector', function($injector) { return function(name) { return $injector.get(name + suffix); }; }]; //////////////////////////////////////// /* global currencyFilter: false, dateFilter: false, filterFilter: false, jsonFilter: false, limitToFilter: false, lowercaseFilter: false, numberFilter: false, orderByFilter: false, uppercaseFilter: false, */ register('currency', currencyFilter); register('date', dateFilter); register('filter', filterFilter); register('json', jsonFilter); register('limitTo', limitToFilter); register('lowercase', lowercaseFilter); register('number', numberFilter); register('orderBy', orderByFilter); register('uppercase', uppercaseFilter); } /** * @ngdoc filter * @name filter * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. * * @param {Array} array The source array. * @param {string|Object|function()} expression The predicate to be used for selecting items from * `array`. * * Can be one of: * * - `string`: The string is used for matching against the contents of the `array`. All strings or * objects with string properties in `array` that match this string will be returned. This also * applies to nested object properties. * The predicate can be negated by prefixing the string with `!`. * * - `Object`: A pattern object can be used to filter specific properties on objects contained * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any * property of the object or its nested object properties. That's equivalent to the simple * substring match with a `string` as described above. The predicate can be negated by prefixing * the string with `!`. * For example `{name: "!M"}` predicate will return an array of items which have property `name` * not containing "M". * * Note that a named property will match properties on the same level only, while the special * `$` property will match properties on the same level or deeper. E.g. an array item like * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but * **will** be matched by `{$: 'John'}`. * * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The * function is called for each element of `array`. The final result is an array of those * elements that the predicate returned true for. * * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in * determining if the expected value (from the filter expression) and actual value (from * the object in the array) should be considered a match. * * Can be one of: * * - `function(actual, expected)`: * The function will be given the object value and the predicate value to compare and * should return true if both values should be considered equal. * * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. * This is essentially strict comparison of expected and actual. * * - `false|undefined`: A short hand for a function which will look for a substring match in case * insensitive way. * * @example
    Search:
    NamePhone
    {{friend.name}} {{friend.phone}}

    Any:
    Name only
    Phone only
    Equality
    NamePhone
    {{friendObj.name}} {{friendObj.phone}}
    var expectFriendNames = function(expectedNames, key) { element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { arr.forEach(function(wd, i) { expect(wd.getText()).toMatch(expectedNames[i]); }); }); }; it('should search across all fields when filtering with a string', function() { var searchText = element(by.model('searchText')); searchText.clear(); searchText.sendKeys('m'); expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); searchText.clear(); searchText.sendKeys('76'); expectFriendNames(['John', 'Julie'], 'friend'); }); it('should search in specific fields when filtering with a predicate object', function() { var searchAny = element(by.model('search.$')); searchAny.clear(); searchAny.sendKeys('i'); expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); }); it('should use a equal comparison when comparator is true', function() { var searchName = element(by.model('search.name')); var strict = element(by.model('strict')); searchName.clear(); searchName.sendKeys('Julie'); strict.click(); expectFriendNames(['Julie'], 'friendObj'); });
    */ function filterFilter() { return function(array, expression, comparator) { if (!isArray(array)) return array; var predicateFn; var matchAgainstAnyProp; switch (typeof expression) { case 'function': predicateFn = expression; break; case 'boolean': case 'number': case 'string': matchAgainstAnyProp = true; //jshint -W086 case 'object': //jshint +W086 predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); break; default: return array; } return array.filter(predicateFn); }; } // Helper functions for `filterFilter` function createPredicateFn(expression, comparator, matchAgainstAnyProp) { var shouldMatchPrimitives = isObject(expression) && ('$' in expression); var predicateFn; if (comparator === true) { comparator = equals; } else if (!isFunction(comparator)) { comparator = function(actual, expected) { if (isObject(actual) || isObject(expected)) { // Prevent an object to be considered equal to a string like `'[object'` return false; } actual = lowercase('' + actual); expected = lowercase('' + expected); return actual.indexOf(expected) !== -1; }; } predicateFn = function(item) { if (shouldMatchPrimitives && !isObject(item)) { return deepCompare(item, expression.$, comparator, false); } return deepCompare(item, expression, comparator, matchAgainstAnyProp); }; return predicateFn; } function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { var actualType = (actual !== null) ? typeof actual : 'null'; var expectedType = (expected !== null) ? typeof expected : 'null'; if ((expectedType === 'string') && (expected.charAt(0) === '!')) { return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); } else if (isArray(actual)) { // In case `actual` is an array, consider it a match // if ANY of it's items matches `expected` return actual.some(function(item) { return deepCompare(item, expected, comparator, matchAgainstAnyProp); }); } switch (actualType) { case 'object': var key; if (matchAgainstAnyProp) { for (key in actual) { if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) { return true; } } return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false); } else if (expectedType === 'object') { for (key in expected) { var expectedVal = expected[key]; if (isFunction(expectedVal) || isUndefined(expectedVal)) { continue; } var matchAnyProperty = key === '$'; var actualVal = matchAnyProperty ? actual : actual[key]; if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) { return false; } } return true; } else { return comparator(actual, expected); } break; case 'function': return false; default: return comparator(actual, expected); } } /** * @ngdoc filter * @name currency * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default * symbol for current locale is used. * * @param {number} amount Input to filter. * @param {string=} symbol Currency symbol or identifier to be displayed. * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale * @returns {string} Formatted number. * * * @example

    default currency symbol ($): {{amount | currency}}
    custom currency identifier (USD$): {{amount | currency:"USD$"}} no fractions (0): {{amount | currency:"USD$":0}}
    it('should init with 1234.56', function() { expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { if (browser.params.browser == 'safari') { // Safari does not understand the minus key. See // https://github.com/angular/protractor/issues/481 return; } element(by.model('amount')).clear(); element(by.model('amount')).sendKeys('-1234'); expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); expect(element(by.id('currency-custom')).getText()).toBe('(USD$1,234.00)'); expect(element(by.id('currency-no-fractions')).getText()).toBe('(USD$1,234)'); });
    */ currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(amount, currencySymbol, fractionSize) { if (isUndefined(currencySymbol)) { currencySymbol = formats.CURRENCY_SYM; } if (isUndefined(fractionSize)) { fractionSize = formats.PATTERNS[1].maxFrac; } // if null or undefined pass it through return (amount == null) ? amount : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). replace(/\u00A4/g, currencySymbol); }; } /** * @ngdoc filter * @name number * @kind function * * @description * Formats a number as text. * * If the input is not a number an empty string is returned. * * @param {number|string} number Number to format. * @param {(number|string)=} fractionSize Number of decimal places to round the number to. * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example
    Enter number:
    Default formatting: {{val | number}}
    No fractions: {{val | number:0}}
    Negative number: {{-val | number:4}}
    it('should format numbers', function() { expect(element(by.id('number-default')).getText()).toBe('1,234.568'); expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); }); it('should update', function() { element(by.model('val')).clear(); element(by.model('val')).sendKeys('3374.333'); expect(element(by.id('number-default')).getText()).toBe('3,374.333'); expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); });
    */ numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { // if null or undefined pass it through return (number == null) ? number : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize); }; } var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (!isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); var numStr = number + '', formatedText = '', parts = []; var hasExponent = false; if (numStr.indexOf('e') !== -1) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { number = 0; } else { formatedText = numStr; hasExponent = true; } } if (!hasExponent) { var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; // determine fractionSize if it is not specified if (isUndefined(fractionSize)) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } // safely round numbers in JS without hitting imprecisions of floating-point arithmetics // inspired by: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; var i, pos = 0, lgroup = pattern.lgSize, group = pattern.gSize; if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; for (i = 0; i < pos; i++) { if ((pos - i) % group === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); } } for (i = pos; i < whole.length; i++) { if ((whole.length - i) % lgroup === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); } // format fraction part. while (fraction.length < fractionSize) { fraction += '0'; } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); } else { if (fractionSize > 0 && number < 1) { formatedText = number.toFixed(fractionSize); number = parseFloat(formatedText); } } if (number === 0) { isNegative = false; } parts.push(isNegative ? pattern.negPre : pattern.posPre, formatedText, isNegative ? pattern.negSuf : pattern.posSuf); return parts.join(''); } function padNumber(num, digits, trim) { var neg = ''; if (num < 0) { neg = '-'; num = -num; } num = '' + num; while (num.length < digits) num = '0' + num; if (trim) num = num.substr(num.length - digits); return neg + num; } function dateGetter(name, size, offset, trim) { offset = offset || 0; return function(date) { var value = date['get' + name](); if (offset > 0 || value > -offset) value += offset; if (value === 0 && offset == -12) value = 12; return padNumber(value, size, trim); }; } function dateStrGetter(name, shortForm) { return function(date, formats) { var value = date['get' + name](); var get = uppercase(shortForm ? ('SHORT' + name) : name); return formats[get][value]; }; } function timeZoneGetter(date) { var zone = -1 * date.getTimezoneOffset(); var paddedZone = (zone >= 0) ? "+" : ""; paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + padNumber(Math.abs(zone % 60), 2); return paddedZone; } function getFirstThursdayOfYear(year) { // 0 = index of January var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); // 4 = index of Thursday (+1 to account for 1st = 5) // 11 = index of *next* Thursday (+1 account for 1st = 12) return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); } function getThursdayThisWeek(datetime) { return new Date(datetime.getFullYear(), datetime.getMonth(), // 4 = index of Thursday datetime.getDate() + (4 - datetime.getDay())); } function weekGetter(size) { return function(date) { var firstThurs = getFirstThursdayOfYear(date.getFullYear()), thisThurs = getThursdayThisWeek(date); var diff = +thisThurs - +firstThurs, result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week return padNumber(result, size); }; } function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } function eraGetter(date, formats) { return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1]; } function longEraGetter(date, formats) { return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1]; } var DATE_FORMATS = { yyyy: dateGetter('FullYear', 4), yy: dateGetter('FullYear', 2, 0, true), y: dateGetter('FullYear', 1), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), H: dateGetter('Hours', 1), hh: dateGetter('Hours', 2, -12), h: dateGetter('Hours', 1, -12), mm: dateGetter('Minutes', 2), m: dateGetter('Minutes', 1), ss: dateGetter('Seconds', 2), s: dateGetter('Seconds', 1), // while ISO 8601 requires fractions to be prefixed with `.` or `,` // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions sss: dateGetter('Milliseconds', 3), EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, Z: timeZoneGetter, ww: weekGetter(2), w: weekGetter(1), G: eraGetter, GG: eraGetter, GGG: eraGetter, GGGG: longEraGetter }; var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter * @name date * @kind function * * @description * Formats `date` to a string based on the requested `format`. * * `format` string can be composed of the following elements: * * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) * * `'MMMM'`: Month in year (January-December) * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) * * `'hh'`: Hour in AM/PM, padded (01-12) * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) * * `'sss'`: Millisecond in second, padded (000-999) * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD') * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini') * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale * (e.g. Sep 3, 2010 12:05:08 PM) * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, * `mediumDate` is used. * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported. * If not specified, the timezone of the browser will be used. * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example {{1288323623006 | date:'medium'}}: {{1288323623006 | date:'medium'}}
    {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
    it('should format date', function() { expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); });
    */ dateFilter.$inject = ['$locale']; function dateFilter($locale) { var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; // 1 2 3 4 5 6 7 8 9 10 11 function jsonStringToDate(string) { var match; if (match = string.match(R_ISO8601_STR)) { var date = new Date(0), tzHour = 0, tzMin = 0, dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, timeSetter = match[8] ? date.setUTCHours : date.setHours; if (match[9]) { tzHour = int(match[9] + match[10]); tzMin = int(match[9] + match[11]); } dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); var h = int(match[4] || 0) - tzHour; var m = int(match[5] || 0) - tzMin; var s = int(match[6] || 0); var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } return string; } return function(date, format, timezone) { var text = '', parts = [], fn, match; format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { date = new Date(date); } if (!isDate(date)) { return date; } while (format) { match = DATE_FORMATS_SPLIT.exec(format); if (match) { parts = concat(parts, match, 1); format = parts.pop(); } else { parts.push(format); format = null; } } if (timezone && timezone === 'UTC') { date = new Date(date.getTime()); date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); } forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS) : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); }); return text; }; } /** * @ngdoc filter * @name json * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. * * This filter is mostly useful for debugging. When using the double curly {{value}} notation * the binding is automatically converted to JSON. * * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. * @returns {string} JSON string. * * * @example
    {{ {'name':'value'} | json }}
    {{ {'name':'value'} | json:4 }}
    it('should jsonify filtered objects', function() { expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); });
    * */ function jsonFilter() { return function(object, spacing) { if (isUndefined(spacing)) { spacing = 2; } return toJson(object, spacing); }; } /** * @ngdoc filter * @name lowercase * @kind function * @description * Converts string to lowercase. * @see angular.lowercase */ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter * @name uppercase * @kind function * @description * Converts string to uppercase. * @see angular.uppercase */ var uppercaseFilter = valueFn(uppercase); /** * @ngdoc filter * @name limitTo * @kind function * * @description * Creates a new array or string containing only a specified number of elements. The elements * are taken from either the beginning or the end of the source array, string or number, as specified by * the value and sign (positive or negative) of `limit`. If a number is used as input, it is * converted to a string. * * @param {Array|string|number} input Source array, string or number to be limited. * @param {string|number} limit The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string * are copied. The `limit` will be trimmed if it exceeds `array.length` * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array * had less than `limit` elements. * * @example
    Limit {{numbers}} to:

    Output numbers: {{ numbers | limitTo:numLimit }}

    Limit {{letters}} to:

    Output letters: {{ letters | limitTo:letterLimit }}

    Limit {{longNumber}} to:

    Output long number: {{ longNumber | limitTo:longNumberLimit }}

    var numLimitInput = element(by.model('numLimit')); var letterLimitInput = element(by.model('letterLimit')); var longNumberLimitInput = element(by.model('longNumberLimit')); var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); it('should limit the number array to first three items', function() { expect(numLimitInput.getAttribute('value')).toBe('3'); expect(letterLimitInput.getAttribute('value')).toBe('3'); expect(longNumberLimitInput.getAttribute('value')).toBe('3'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); expect(limitedLetters.getText()).toEqual('Output letters: abc'); expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); }); // There is a bug in safari and protractor that doesn't like the minus key // it('should update the output when -3 is entered', function() { // numLimitInput.clear(); // numLimitInput.sendKeys('-3'); // letterLimitInput.clear(); // letterLimitInput.sendKeys('-3'); // longNumberLimitInput.clear(); // longNumberLimitInput.sendKeys('-3'); // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); // }); it('should not exceed the maximum size of input array', function() { numLimitInput.clear(); numLimitInput.sendKeys('100'); letterLimitInput.clear(); letterLimitInput.sendKeys('100'); longNumberLimitInput.clear(); longNumberLimitInput.sendKeys('100'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); });
    */ function limitToFilter() { return function(input, limit) { if (isNumber(input)) input = input.toString(); if (!isArray(input) && !isString(input)) return input; if (Math.abs(Number(limit)) === Infinity) { limit = Number(limit); } else { limit = int(limit); } //NaN check on limit if (limit) { return limit > 0 ? input.slice(0, limit) : input.slice(limit); } else { return isString(input) ? "" : []; } }; } /** * @ngdoc filter * @name orderBy * @kind function * * @description * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically * for strings and numerically for numbers. Note: if you notice numbers are not being sorted * correctly, make sure they are actually being saved as numbers and not strings. * * @param {Array} array The array to sort. * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. * - `string`: An Angular expression. The result of this expression is used to compare elements * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by * 3 first characters of a property called `name`). The result of a constant expression * is interpreted as a property name to be used in comparisons (for example `"special name"` * to sort object by the value of their `special name` property). An expression can be * optionally prefixed with `+` or `-` to control ascending or descending sort order * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * * If the predicate is missing or empty then it defaults to `'+'`. * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * * @example * The example below demonstrates a simple ngRepeat, where the data is sorted * by age in descending order (predicate is set to `'-age'`). * `reverse` is not set, which means it defaults to `false`.
    Name Phone Number Age
    {{friend.name}} {{friend.phone}} {{friend.age}}
    * * The predicate and reverse parameters can be controlled dynamically through scope properties, * as shown in the next example. * @example
    Sorting predicate = {{predicate}}; reverse = {{reverse}}

    [ unsorted ]
    Name (^) Phone Number Age
    {{friend.name}} {{friend.phone}} {{friend.age}}
    * * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the * desired parameters. * * Example: * * @example
    Name (^) Phone Number Age
    {{friend.name}} {{friend.phone}} {{friend.age}}
    angular.module('orderByExample', []) .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { var orderBy = $filter('orderBy'); $scope.friends = [ { name: 'John', phone: '555-1212', age: 10 }, { name: 'Mary', phone: '555-9876', age: 19 }, { name: 'Mike', phone: '555-4321', age: 21 }, { name: 'Adam', phone: '555-5678', age: 35 }, { name: 'Julie', phone: '555-8765', age: 29 } ]; $scope.order = function(predicate, reverse) { $scope.friends = orderBy($scope.friends, predicate, reverse); }; $scope.order('-age',false); }]);
    */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse) { return function(array, sortPredicate, reverseOrder) { if (!(isArrayLike(array))) return array; sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; if (sortPredicate.length === 0) { sortPredicate = ['+']; } sortPredicate = sortPredicate.map(function(predicate) { var descending = false, get = predicate || identity; if (isString(predicate)) { if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } if (predicate === '') { // Effectively no predicate was passed so we compare identity return reverseComparator(compare, descending); } get = $parse(predicate); if (get.constant) { var key = get(); return reverseComparator(function(a, b) { return compare(a[key], b[key]); }, descending); } } return reverseComparator(function(a, b) { return compare(get(a),get(b)); }, descending); }); return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); function comparator(o1, o2) { for (var i = 0; i < sortPredicate.length; i++) { var comp = sortPredicate[i](o1, o2); if (comp !== 0) return comp; } return 0; } function reverseComparator(comp, descending) { return descending ? function(a, b) {return comp(b,a);} : comp; } function isPrimitive(value) { switch (typeof value) { case 'number': /* falls through */ case 'boolean': /* falls through */ case 'string': return true; default: return false; } } function objectToString(value) { if (value === null) return 'null'; if (typeof value.valueOf === 'function') { value = value.valueOf(); if (isPrimitive(value)) return value; } if (typeof value.toString === 'function') { value = value.toString(); if (isPrimitive(value)) return value; } return ''; } function compare(v1, v2) { var t1 = typeof v1; var t2 = typeof v2; if (t1 === t2 && t1 === "object") { v1 = objectToString(v1); v2 = objectToString(v2); } if (t1 === t2) { if (t1 === "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); } if (v1 === v2) return 0; return v1 < v2 ? -1 : 1; } else { return t1 < t2 ? -1 : 1; } } }; } function ngDirective(directive) { if (isFunction(directive)) { directive = { link: directive }; } directive.restrict = directive.restrict || 'AC'; return valueFn(directive); } /** * @ngdoc directive * @name a * @restrict E * * @description * Modifies the default behavior of the html A tag so that the default action is prevented when * the href attribute is empty. * * This change permits the easy creation of action links with the `ngClick` directive * without changing the location or causing page reloads, e.g.: * `Add Item` */ var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { if (!attr.href && !attr.xlinkHref && !attr.name) { return function(scope, element) { // If the linked element is not an anchor tag anymore, do nothing if (element[0].nodeName.toLowerCase() !== 'a') return; // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? 'xlink:href' : 'href'; element.on('click', function(event) { // if we have no href url, then don't navigate anywhere. if (!element.attr(href)) { event.preventDefault(); } }); }; } } }); /** * @ngdoc directive * @name ngHref * @restrict A * @priority 99 * * @description * Using Angular markup like `{{hash}}` in an href attribute will * make the link go to the wrong URL if the user clicks it before * Angular has a chance to replace the `{{hash}}` markup with its * value. Until Angular replaces the markup the link will be broken * and will most likely return a 404 error. The `ngHref` directive * solves this problem. * * The wrong way to write it: * ```html * link1 * ``` * * The correct way to write it: * ```html * link1 * ``` * * @element A * @param {template} ngHref any string which can contain `{{}}` markup. * * @example * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes * in links and their different behaviors:
    link 1 (link, don't reload)
    link 2 (link, don't reload)
    link 3 (link, reload!)
    anchor (link, don't reload)
    anchor (no link)
    link (link, change location)
    it('should execute ng-click but not reload when href without value', function() { element(by.id('link-1')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('1'); expect(element(by.id('link-1')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when href empty string', function() { element(by.id('link-2')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('2'); expect(element(by.id('link-2')).getAttribute('href')).toBe(''); }); it('should execute ng-click and change url when ng-href specified', function() { expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); element(by.id('link-3')).click(); // At this point, we navigate away from an Angular page, so we need // to use browser.driver to get the base webdriver. browser.wait(function() { return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/123$/); }); }, 5000, 'page should navigate to /123'); }); xit('should execute ng-click but not reload when href empty string and name specified', function() { element(by.id('link-4')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('4'); expect(element(by.id('link-4')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when no href but name specified', function() { element(by.id('link-5')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('5'); expect(element(by.id('link-5')).getAttribute('href')).toBe(null); }); it('should only change url when only ng-href', function() { element(by.model('value')).clear(); element(by.model('value')).sendKeys('6'); expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); element(by.id('link-6')).click(); // At this point, we navigate away from an Angular page, so we need // to use browser.driver to get the base webdriver. browser.wait(function() { return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/6$/); }); }, 5000, 'page should navigate to /6'); });
    */ /** * @ngdoc directive * @name ngSrc * @restrict A * @priority 99 * * @description * Using Angular markup like `{{hash}}` in a `src` attribute doesn't * work right: The browser will fetch from the URL with the literal * text `{{hash}}` until Angular replaces the expression inside * `{{hash}}`. The `ngSrc` directive solves this problem. * * The buggy way to write it: * ```html * * ``` * * The correct way to write it: * ```html * * ``` * * @element IMG * @param {template} ngSrc any string which can contain `{{}}` markup. */ /** * @ngdoc directive * @name ngSrcset * @restrict A * @priority 99 * * @description * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't * work right: The browser will fetch from the URL with the literal * text `{{hash}}` until Angular replaces the expression inside * `{{hash}}`. The `ngSrcset` directive solves this problem. * * The buggy way to write it: * ```html * * ``` * * The correct way to write it: * ```html * * ``` * * @element IMG * @param {template} ngSrcset any string which can contain `{{}}` markup. */ /** * @ngdoc directive * @name ngDisabled * @restrict A * @priority 100 * * @description * * This directive sets the `disabled` attribute on the element if the * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * * A special directive is necessary because we cannot use interpolation inside the `disabled` * attribute. The following example would make the button enabled on Chrome/Firefox * but not on older IEs: * * ```html * *
    * *
    * ``` * * This is because the HTML specification does not require browsers to preserve the values of * boolean attributes such as `disabled` (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. * * @example Click me to toggle:
    it('should toggle button', function() { expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); element(by.model('checked')).click(); expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); });
    * * @element INPUT * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, * then the `disabled` attribute will be set on the element */ /** * @ngdoc directive * @name ngChecked * @restrict A * @priority 100 * * @description * The HTML specification does not require browsers to preserve the values of boolean attributes * such as checked. (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. * The `ngChecked` directive solves this problem for the `checked` attribute. * This complementary directive is not removed by the browser and so provides * a permanent reliable place to store the binding information. * @example Check me to check both:
    it('should check both checkBoxes', function() { expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); element(by.model('master')).click(); expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); });
    * * @element INPUT * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, * then special attribute "checked" will be set on the element */ /** * @ngdoc directive * @name ngReadonly * @restrict A * @priority 100 * * @description * The HTML specification does not require browsers to preserve the values of boolean attributes * such as readonly. (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. * The `ngReadonly` directive solves this problem for the `readonly` attribute. * This complementary directive is not removed by the browser and so provides * a permanent reliable place to store the binding information. * @example Check me to make text readonly:
    it('should toggle readonly attr', function() { expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); element(by.model('checked')).click(); expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); });
    * * @element INPUT * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, * then special attribute "readonly" will be set on the element */ /** * @ngdoc directive * @name ngSelected * @restrict A * @priority 100 * * @description * The HTML specification does not require browsers to preserve the values of boolean attributes * such as selected. (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. * The `ngSelected` directive solves this problem for the `selected` attribute. * This complementary directive is not removed by the browser and so provides * a permanent reliable place to store the binding information. * * @example Check me to select:
    it('should select Greetings!', function() { expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); element(by.model('selected')).click(); expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); });
    * * @element OPTION * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, * then special attribute "selected" will be set on the element */ /** * @ngdoc directive * @name ngOpen * @restrict A * @priority 100 * * @description * The HTML specification does not require browsers to preserve the values of boolean attributes * such as open. (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. * The `ngOpen` directive solves this problem for the `open` attribute. * This complementary directive is not removed by the browser and so provides * a permanent reliable place to store the binding information. * @example Check me check multiple:
    Show/Hide me
    it('should toggle open', function() { expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); element(by.model('open')).click(); expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); });
    * * @element DETAILS * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, * then special attribute "open" will be set on the element */ var ngAttributeAliasDirectives = {}; // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported if (propName == "multiple") return; var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { attr.$set(attrName, !!value); }); } }; }; }); // aliased input attrs are evaluated forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { ngAttributeAliasDirectives[ngAttr] = function() { return { priority: 100, link: function(scope, element, attr) { //special case ngPattern when a literal regular expression value //is used as the expression (this way we don't have to watch anything). if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { var match = attr.ngPattern.match(REGEX_STRING_REGEXP); if (match) { attr.$set("ngPattern", new RegExp(match[1], match[2])); return; } } scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { attr.$set(ngAttr, value); }); } }; }; }); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { priority: 99, // it needs to run after the attributes are interpolated link: function(scope, element, attr) { var propName = attrName, name = attrName; if (attrName === 'href' && toString.call(element.prop('href')) === '[object SVGAnimatedString]') { name = 'xlinkHref'; attr.$attr[name] = 'xlink:href'; propName = null; } attr.$observe(normalized, function(value) { if (!value) { if (attrName === 'href') { attr.$set(name, null); } return; } attr.$set(name, value); // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. // we use attr[attrName] value since $set can sanitize the url. if (msie && propName) element.prop(propName, attr[name]); }); } }; }; }); /* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true */ var nullFormCtrl = { $addControl: noop, $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, $setPristine: noop, $setSubmitted: noop }, SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { control.$name = name; } /** * @ngdoc type * @name form.FormController * * @property {boolean} $pristine True if user has not interacted with the form yet. * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. * @property {boolean} $submitted True if user has submitted the form even if its invalid. * * @property {Object} $error Is an object hash, containing references to controls or * forms with failing validators, where: * * - keys are validation tokens (error names), * - values are arrays of controls or forms that have a failing validator for given error name. * * Built-in validation tokens: * * - `email` * - `max` * - `maxlength` * - `min` * - `minlength` * - `number` * - `pattern` * - `required` * - `url` * - `date` * - `datetimelocal` * - `time` * - `week` * - `month` * * @description * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance * of `FormController`. * */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, controls = []; var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; // init state form.$error = {}; form.$$success = {}; form.$pending = undefined; form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); form.$dirty = false; form.$pristine = true; form.$valid = true; form.$invalid = false; form.$submitted = false; parentForm.$addControl(form); /** * @ngdoc method * @name form.FormController#$rollbackViewValue * * @description * Rollback all form controls pending updates to the `$modelValue`. * * Updates may be pending by a debounced event or because the input is waiting for a some future * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ form.$rollbackViewValue = function() { forEach(controls, function(control) { control.$rollbackViewValue(); }); }; /** * @ngdoc method * @name form.FormController#$commitViewValue * * @description * Commit all form controls pending updates to the `$modelValue`. * * Updates may be pending by a debounced event or because the input is waiting for a some future * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ form.$commitViewValue = function() { forEach(controls, function(control) { control.$commitViewValue(); }); }; /** * @ngdoc method * @name form.FormController#$addControl * * @description * Register a control with the form. * * Input elements using ngModelController do this automatically when they are linked. */ form.$addControl = function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); controls.push(control); if (control.$name) { form[control.$name] = control; } }; // Private API: rename a form control form.$$renameControl = function(control, newName) { var oldName = control.$name; if (form[oldName] === control) { delete form[oldName]; } form[newName] = control; control.$name = newName; }; /** * @ngdoc method * @name form.FormController#$removeControl * * @description * Deregister a control from the form. * * Input elements using ngModelController do this automatically when they are destroyed. */ form.$removeControl = function(control) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; } forEach(form.$pending, function(value, name) { form.$setValidity(name, null, control); }); forEach(form.$error, function(value, name) { form.$setValidity(name, null, control); }); forEach(form.$$success, function(value, name) { form.$setValidity(name, null, control); }); arrayRemove(controls, control); }; /** * @ngdoc method * @name form.FormController#$setValidity * * @description * Sets the validity of a form control. * * This method will also propagate to parent forms. */ addSetValidityMethod({ ctrl: this, $element: element, set: function(object, property, controller) { var list = object[property]; if (!list) { object[property] = [controller]; } else { var index = list.indexOf(controller); if (index === -1) { list.push(controller); } } }, unset: function(object, property, controller) { var list = object[property]; if (!list) { return; } arrayRemove(list, controller); if (list.length === 0) { delete object[property]; } }, parentForm: parentForm, $animate: $animate }); /** * @ngdoc method * @name form.FormController#$setDirty * * @description * Sets the form to a dirty state. * * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ form.$setDirty = function() { $animate.removeClass(element, PRISTINE_CLASS); $animate.addClass(element, DIRTY_CLASS); form.$dirty = true; form.$pristine = false; parentForm.$setDirty(); }; /** * @ngdoc method * @name form.FormController#$setPristine * * @description * Sets the form to its pristine state. * * This method can be called to remove the 'ng-dirty' class and set the form to its pristine * state (ng-pristine class). This method will also propagate to all the controls contained * in this form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ form.$setPristine = function() { $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; form.$submitted = false; forEach(controls, function(control) { control.$setPristine(); }); }; /** * @ngdoc method * @name form.FormController#$setUntouched * * @description * Sets the form to its untouched state. * * This method can be called to remove the 'ng-touched' class and set the form controls to their * untouched state (ng-untouched class). * * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ form.$setUntouched = function() { forEach(controls, function(control) { control.$setUntouched(); }); }; /** * @ngdoc method * @name form.FormController#$setSubmitted * * @description * Sets the form to its submitted state. */ form.$setSubmitted = function() { $animate.addClass(element, SUBMITTED_CLASS); form.$submitted = true; parentForm.$setSubmitted(); }; } /** * @ngdoc directive * @name ngForm * @restrict EAC * * @description * Nestable alias of {@link ng.directive:form `form`} directive. HTML * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a * sub-group of controls needs to be determined. * * Note: the purpose of `ngForm` is to group controls, * but not to be a replacement for the `
    ` tag with all of its capabilities * (e.g. posting to the server, ...). * * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into * related scope, under this name. * */ /** * @ngdoc directive * @name form * @restrict E * * @description * Directive that instantiates * {@link form.FormController FormController}. * * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * * # Alias: {@link ng.directive:ngForm `ngForm`} * * In Angular, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `` elements, so * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to * `` but can be nested. This allows you to have nested forms, which is very useful when * using Angular validation directives in forms that are dynamically generated using the * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an * `ngForm` directive and nest these in an outer `form` element. * * * # CSS classes * - `ng-valid` is set if the form is valid. * - `ng-invalid` is set if the form is invalid. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. * - `ng-submitted` is set if the form was submitted. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * * * # Submitting a form and preventing the default action * * Since the role of forms in client-side Angular applications is different than in classical * roundtrip apps, it is desirable for the browser not to translate the form submission into a full * page reload that sends the data to the server. Instead some javascript logic should be triggered * to handle the form submission in an application-specific way. * * For this reason, Angular prevents the default action (form submission to the server) unless the * `` element has an `action` attribute specified. * * You can use one of the following two ways to specify what javascript method should be called when * a form is submitted: * * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element * - {@link ng.directive:ngClick ngClick} directive on the first * button or input field of type submit (input[type=submit]) * * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} * or {@link ng.directive:ngClick ngClick} directives. * This is because of the following form submission rules in the HTML specification: * * - If a form has only one input field then hitting enter in this field triggers form submit * (`ngSubmit`) * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter * doesn't trigger submit * - if a form has one or more input fields and one or more buttons or input[type=submit] then * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * * ## Animation Hooks * * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any * other validations that are performed within the form. Animations in ngForm are similar to how * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well * as JS animations. * * The following example shows a simple way to utilize CSS transitions to style a form element * that has been rendered as invalid after it has been validated: * *
     * //be sure to include ngAnimate as a module to hook into more
     * //advanced animations
     * .my-form {
     *   transition:0.5s linear all;
     *   background: white;
     * }
     * .my-form.ng-invalid {
     *   background: red;
     *   color:white;
     * }
     * 
    * * @example userType: Required!
    userType = {{userType}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    it('should initialize to model', function() { var userType = element(by.binding('userType')); var valid = element(by.binding('myForm.input.$valid')); expect(userType.getText()).toContain('guest'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { var userType = element(by.binding('userType')); var valid = element(by.binding('myForm.input.$valid')); var userInput = element(by.model('userType')); userInput.clear(); userInput.sendKeys(''); expect(userType.getText()).toEqual('userType ='); expect(valid.getText()).toContain('false'); });
    * * @param {string=} name Name of the form. If specified, the form controller will be published into * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { var formDirective = { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, compile: function ngFormCompile(formElement, attr) { // Setup initial state of the control formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); return { pre: function ngFormPreLink(scope, formElement, attr, controller) { // if `action` attr is not present on the form, prevent the default action (submission) if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. var handleFormSubmission = function(event) { scope.$apply(function() { controller.$commitViewValue(); controller.$setSubmitted(); }); event.preventDefault(); }; addEventListenerFn(formElement[0], 'submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); }, 0, false); }); } var parentFormCtrl = controller.$$parentForm; if (nameAttr) { setter(scope, null, controller.$name, controller, controller.$name); attr.$observe(nameAttr, function(newValue) { if (controller.$name === newValue) return; setter(scope, null, controller.$name, undefined, controller.$name); parentFormCtrl.$$renameControl(controller, newValue); setter(scope, null, controller.$name, controller, controller.$name); }); } formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); if (nameAttr) { setter(scope, null, attr[nameAttr], undefined, controller.$name); } extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); } }; } }; return formDirective; }]; }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); /* global VALID_CLASS: false, INVALID_CLASS: false, PRISTINE_CLASS: false, DIRTY_CLASS: false, UNTOUCHED_CLASS: false, TOUCHED_CLASS: false, $ngModelMinErr: false, */ // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var inputType = { /** * @ngdoc input * @name input[text] * * @description * Standard HTML text input with angular data binding, inherited by most of the `input` elements. * * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Adds `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of * any length. * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object then this is used directly. * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. * This parameter is ignored for input[type=password] controls, which will never trim the * input. * * @example
    Single word: Required! Single word only! text = {{example.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var text = element(by.binding('example.text')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.text')); it('should initialize to model', function() { expect(text.getText()).toContain('guest'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { input.clear(); input.sendKeys(''); expect(text.getText()).toEqual('text ='); expect(valid.getText()).toContain('false'); }); it('should be invalid if multi word', function() { input.clear(); input.sendKeys('hello world'); expect(valid.getText()).toContain('false'); });
    */ 'text': textInputType, /** * @ngdoc input * @name input[date] * * @description * Input with date validation and transformation. In browsers that do not yet support * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many * modern browsers do not yet support this input type, it is important to provide cues to users on the * expected input format via a placeholder or label. * * The model must always be a Date object, otherwise Angular will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * * The timezone to be used to read/write the `Date` instance in the model can be defined using * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a * valid ISO date string (yyyy-MM-dd). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be * a valid ISO date string (yyyy-MM-dd). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Pick a date in 2013: Required! Not a valid date! value = {{example.value | date: "yyyy-MM-dd"}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls // for various browsers (see https://github.com/angular/protractor/issues/562). function setInput(val) { // set the value of the element and force validation. var scr = "var ipt = document.getElementById('exampleInput'); " + "ipt.value = '" + val + "';" + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; browser.executeScript(scr); } it('should initialize to model', function() { expect(value.getText()).toContain('2013-10-22'); expect(valid.getText()).toContain('myForm.input.$valid = true'); }); it('should be invalid if empty', function() { setInput(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); it('should be invalid if over max', function() { setInput('2015-01-01'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); });
    */ 'date': createDateInputType('date', DATE_REGEXP, createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), 'yyyy-MM-dd'), /** * @ngdoc input * @name input[datetime-local] * * @description * Input with datetime validation and transformation. In browsers that do not yet support * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. * * The model must always be a Date object, otherwise Angular will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * * The timezone to be used to read/write the `Date` instance in the model can be defined using * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Pick a date between in 2013: Required! Not a valid date! value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls // for various browsers (https://github.com/angular/protractor/issues/562). function setInput(val) { // set the value of the element and force validation. var scr = "var ipt = document.getElementById('exampleInput'); " + "ipt.value = '" + val + "';" + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; browser.executeScript(scr); } it('should initialize to model', function() { expect(value.getText()).toContain('2010-12-28T14:57:00'); expect(valid.getText()).toContain('myForm.input.$valid = true'); }); it('should be invalid if empty', function() { setInput(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); it('should be invalid if over max', function() { setInput('2015-01-01T23:59:00'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); });
    */ 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), 'yyyy-MM-ddTHH:mm:ss.sss'), /** * @ngdoc input * @name input[time] * * @description * Input with time validation and transformation. In browsers that do not yet support * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. * * The model must always be a Date object, otherwise Angular will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * * The timezone to be used to read/write the `Date` instance in the model can be defined using * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a * valid ISO time format (HH:mm:ss). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a * valid ISO time format (HH:mm:ss). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Pick a between 8am and 5pm: Required! Not a valid date! value = {{example.value | date: "HH:mm:ss"}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value | date: "HH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls // for various browsers (https://github.com/angular/protractor/issues/562). function setInput(val) { // set the value of the element and force validation. var scr = "var ipt = document.getElementById('exampleInput'); " + "ipt.value = '" + val + "';" + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; browser.executeScript(scr); } it('should initialize to model', function() { expect(value.getText()).toContain('14:57:00'); expect(valid.getText()).toContain('myForm.input.$valid = true'); }); it('should be invalid if empty', function() { setInput(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); it('should be invalid if over max', function() { setInput('23:59:00'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); });
    */ 'time': createDateInputType('time', TIME_REGEXP, createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), 'HH:mm:ss.sss'), /** * @ngdoc input * @name input[week] * * @description * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 * week format (yyyy-W##), for example: `2013-W02`. * * The model must always be a Date object, otherwise Angular will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * * The timezone to be used to read/write the `Date` instance in the model can be defined using * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a * valid ISO week format (yyyy-W##). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be * a valid ISO week format (yyyy-W##). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Pick a date between in 2013: Required! Not a valid date! value = {{example.value | date: "yyyy-Www"}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value | date: "yyyy-Www"')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls // for various browsers (https://github.com/angular/protractor/issues/562). function setInput(val) { // set the value of the element and force validation. var scr = "var ipt = document.getElementById('exampleInput'); " + "ipt.value = '" + val + "';" + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; browser.executeScript(scr); } it('should initialize to model', function() { expect(value.getText()).toContain('2013-W01'); expect(valid.getText()).toContain('myForm.input.$valid = true'); }); it('should be invalid if empty', function() { setInput(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); it('should be invalid if over max', function() { setInput('2015-W01'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); });
    */ 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), /** * @ngdoc input * @name input[month] * * @description * Input with month validation and transformation. In browsers that do not yet support * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 * month format (yyyy-MM), for example: `2009-01`. * * The model must always be a Date object, otherwise Angular will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * If the model is not set to the first of the month, the next view to model update will set it * to the first of the month. * * The timezone to be used to read/write the `Date` instance in the model can be defined using * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be * a valid ISO month format (yyyy-MM). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must * be a valid ISO month format (yyyy-MM). * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Pick a month in 2013: Required! Not a valid month! value = {{example.value | date: "yyyy-MM"}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value | date: "yyyy-MM"')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls // for various browsers (https://github.com/angular/protractor/issues/562). function setInput(val) { // set the value of the element and force validation. var scr = "var ipt = document.getElementById('exampleInput'); " + "ipt.value = '" + val + "';" + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; browser.executeScript(scr); } it('should initialize to model', function() { expect(value.getText()).toContain('2013-10'); expect(valid.getText()).toContain('myForm.input.$valid = true'); }); it('should be invalid if empty', function() { setInput(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('myForm.input.$valid = false'); }); it('should be invalid if over max', function() { setInput('2015-01'); expect(value.getText()).toContain(''); expect(valid.getText()).toContain('myForm.input.$valid = false'); });
    */ 'month': createDateInputType('month', MONTH_REGEXP, createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), 'yyyy-MM'), /** * @ngdoc input * @name input[number] * * @description * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * * The model must always be a number, otherwise Angular will throw an error. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of * any length. * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object then this is used directly. * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Number: Required! Not valid number! value = {{example.value}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    var value = element(by.binding('example.value')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('example.value')); it('should initialize to model', function() { expect(value.getText()).toContain('12'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { input.clear(); input.sendKeys(''); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('false'); }); it('should be invalid if over max', function() { input.clear(); input.sendKeys('123'); expect(value.getText()).toEqual('value ='); expect(valid.getText()).toContain('false'); });
    */ 'number': numberInputType, /** * @ngdoc input * @name input[url] * * @description * Text input with URL validation. Sets the `url` validation error key if the content is not a * valid URL. * *
    * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify * the built-in validators (see the {@link guide/forms Forms guide}) *
    * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of * any length. * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object then this is used directly. * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    URL: Required! Not valid url! text = {{url.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    myForm.$error.url = {{!!myForm.$error.url}}
    var text = element(by.binding('url.text')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('url.text')); it('should initialize to model', function() { expect(text.getText()).toContain('http://google.com'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { input.clear(); input.sendKeys(''); expect(text.getText()).toEqual('text ='); expect(valid.getText()).toContain('false'); }); it('should be invalid if not url', function() { input.clear(); input.sendKeys('box'); expect(valid.getText()).toContain('false'); });
    */ 'url': urlInputType, /** * @ngdoc input * @name input[email] * * @description * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * *
    * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) *
    * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of * any length. * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object then this is used directly. * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Email: Required! Not valid email! text = {{email.text}}
    myForm.input.$valid = {{myForm.input.$valid}}
    myForm.input.$error = {{myForm.input.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    myForm.$error.email = {{!!myForm.$error.email}}
    var text = element(by.binding('email.text')); var valid = element(by.binding('myForm.input.$valid')); var input = element(by.model('email.text')); it('should initialize to model', function() { expect(text.getText()).toContain('me@example.com'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { input.clear(); input.sendKeys(''); expect(text.getText()).toEqual('text ='); expect(valid.getText()).toContain('false'); }); it('should be invalid if not email', function() { input.clear(); input.sendKeys('xxx'); expect(valid.getText()).toContain('false'); });
    */ 'email': emailInputType, /** * @ngdoc input * @name input[radio] * * @description * HTML radio button. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string} value The value to which the expression should be set when selected. * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {string} ngValue Angular expression which sets the value to which the expression should * be set when selected. * * @example
    Red
    Green
    Blue
    color = {{color.name | json}}
    Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
    it('should change state', function() { var color = element(by.binding('color.name')); expect(color.getText()).toContain('blue'); element.all(by.model('color.name')).get(0).click(); expect(color.getText()).toContain('red'); });
    */ 'radio': radioInputType, /** * @ngdoc input * @name input[checkbox] * * @description * HTML checkbox. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {expression=} ngTrueValue The value to which the expression should be set when selected. * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
    Value1:
    Value2:
    value1 = {{checkboxModel.value1}}
    value2 = {{checkboxModel.value2}}
    it('should change state', function() { var value1 = element(by.binding('checkboxModel.value1')); var value2 = element(by.binding('checkboxModel.value2')); expect(value1.getText()).toContain('true'); expect(value2.getText()).toContain('YES'); element(by.model('checkboxModel.value1')).click(); element(by.model('checkboxModel.value2')).click(); expect(value1.getText()).toContain('false'); expect(value2.getText()).toContain('NO'); });
    */ 'checkbox': checkboxInputType, 'hidden': noop, 'button': noop, 'submit': noop, 'reset': noop, 'file': noop }; function stringBasedInputType(ctrl) { ctrl.$formatters.push(function(value) { return ctrl.$isEmpty(value) ? value : value.toString(); }); } function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); } function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { var composing = false; element.on('compositionstart', function(data) { composing = true; }); element.on('compositionend', function() { composing = false; listener(); }); } var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); timeout = null; } if (composing) return; var value = element.val(), event = ev && ev.type; // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming // If input type is 'password', the value is never trimmed if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } // If a control is suffering from bad input (due to native validators), browsers discard its // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the // control's value is the same empty value twice in a row. if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { ctrl.$setViewValue(value, event); } }; // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the // input event on backspace, delete or cut if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { var timeout; var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { timeout = null; if (!input || input.value !== origValue) { listener(ev); } }); } }; element.on('keydown', function(event) { var key = event.keyCode; // ignore // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it if ($sniffer.hasEvent('paste')) { element.on('paste cut', deferListener); } } // if user paste into input using mouse on older browser // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; } function weekParser(isoWeek, existingDate) { if (isDate(isoWeek)) { return isoWeek; } if (isString(isoWeek)) { WEEK_REGEXP.lastIndex = 0; var parts = WEEK_REGEXP.exec(isoWeek); if (parts) { var year = +parts[1], week = +parts[2], hours = 0, minutes = 0, seconds = 0, milliseconds = 0, firstThurs = getFirstThursdayOfYear(year), addDays = (week - 1) * 7; if (existingDate) { hours = existingDate.getHours(); minutes = existingDate.getMinutes(); seconds = existingDate.getSeconds(); milliseconds = existingDate.getMilliseconds(); } return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); } } return NaN; } function createDateParser(regexp, mapping) { return function(iso, date) { var parts, map; if (isDate(iso)) { return iso; } if (isString(iso)) { // When a date is JSON'ified to wraps itself inside of an extra // set of double quotes. This makes the date parsing code unable // to match the date string and parse it as a date. if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { iso = iso.substring(1, iso.length - 1); } if (ISO_DATE_REGEXP.test(iso)) { return new Date(iso); } regexp.lastIndex = 0; parts = regexp.exec(iso); if (parts) { parts.shift(); if (date) { map = { yyyy: date.getFullYear(), MM: date.getMonth() + 1, dd: date.getDate(), HH: date.getHours(), mm: date.getMinutes(), ss: date.getSeconds(), sss: date.getMilliseconds() / 1000 }; } else { map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; } forEach(parts, function(part, index) { if (index < mapping.length) { map[mapping[index]] = +part; } }); return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); } } return NaN; }; } function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; var previousDate; ctrl.$$parserName = type; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (regexp.test(value)) { // Note: We cannot read ctrl.$modelValue, as there might be a different // parser/formatter in the processing chain so that the model // contains some different data format! var parsedDate = parseDate(value, previousDate); if (timezone === 'UTC') { parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); } return parsedDate; } return undefined; }); ctrl.$formatters.push(function(value) { if (value && !isDate(value)) { throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); } if (isValidDate(value)) { previousDate = value; if (previousDate && timezone === 'UTC') { var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); previousDate = new Date(previousDate.getTime() + timezoneOffset); } return $filter('date')(value, format, timezone); } else { previousDate = null; return ''; } }); if (isDefined(attr.min) || attr.ngMin) { var minVal; ctrl.$validators.min = function(value) { return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; }; attr.$observe('min', function(val) { minVal = parseObservedDateValue(val); ctrl.$validate(); }); } if (isDefined(attr.max) || attr.ngMax) { var maxVal; ctrl.$validators.max = function(value) { return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; }; attr.$observe('max', function(val) { maxVal = parseObservedDateValue(val); ctrl.$validate(); }); } function isValidDate(value) { // Invalid Date: getTime() returns NaN return value && !(value.getTime && value.getTime() !== value.getTime()); } function parseObservedDateValue(val) { return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; } }; } function badInputChecker(scope, element, attr, ctrl) { var node = element[0]; var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); if (nativeValidation) { ctrl.$parsers.push(function(value) { var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): // - also sets validity.badInput (should only be validity.typeMismatch). // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) // - can ignore this case as we can still read out the erroneous email... return validity.badInput && !validity.typeMismatch ? undefined : value; }); } } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (NUMBER_REGEXP.test(value)) return parseFloat(value); return undefined; }); ctrl.$formatters.push(function(value) { if (!ctrl.$isEmpty(value)) { if (!isNumber(value)) { throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); } value = value.toString(); } return value; }); if (isDefined(attr.min) || attr.ngMin) { var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; attr.$observe('min', function(val) { if (isDefined(val) && !isNumber(val)) { val = parseFloat(val, 10); } minVal = isNumber(val) && !isNaN(val) ? val : undefined; // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } if (isDefined(attr.max) || attr.ngMax) { var maxVal; ctrl.$validators.max = function(value) { return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; attr.$observe('max', function(val) { if (isDefined(val) && !isNumber(val)) { val = parseFloat(val, 10); } maxVal = isNumber(val) && !isNaN(val) ? val : undefined; // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { // Note: no badInputChecker here by purpose as `url` is only a validation // in browsers, i.e. we can always read out input.value even if it is not valid! baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); ctrl.$$parserName = 'url'; ctrl.$validators.url = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { // Note: no badInputChecker here by purpose as `url` is only a validation // in browsers, i.e. we can always read out input.value even if it is not valid! baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); ctrl.$$parserName = 'email'; ctrl.$validators.email = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; } function radioInputType(scope, element, attr, ctrl) { // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { if (element[0].checked) { ctrl.$setViewValue(attr.value, ev && ev.type); } }; element.on('click', listener); ctrl.$render = function() { var value = attr.value; element[0].checked = (value == ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); } function parseConstantExpr($parse, context, name, expression, fallback) { var parseFn; if (isDefined(expression)) { parseFn = $parse(expression); if (!parseFn.constant) { throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + '`{1}`.', name, expression); } return parseFn(context); } return fallback; } function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); var listener = function(ev) { ctrl.$setViewValue(element[0].checked, ev && ev.type); }; element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert // it to a boolean. ctrl.$isEmpty = function(value) { return value === false; }; ctrl.$formatters.push(function(value) { return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { return value ? trueValue : falseValue; }); } /** * @ngdoc directive * @name textarea * @restrict E * * @description * HTML textarea element control with angular data-binding. The data-binding and validation * properties of this element are exactly the same as those of the * {@link ng.directive:input input element}. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. */ /** * @ngdoc directive * @name input * @restrict E * * @description * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding, * input state control, and validation. * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers. * *
    * **Note:** Not every feature offered is available for all input types. * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`. *
    * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {boolean=} ngRequired Sets `required` attribute if set to true * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. * This parameter is ignored for input[type=password] controls, which will never trim the * input. * * @example
    User name: Required!
    Last name: Too short! Too long!

    user = {{user}}
    myForm.userName.$valid = {{myForm.userName.$valid}}
    myForm.userName.$error = {{myForm.userName.$error}}
    myForm.lastName.$valid = {{myForm.lastName.$valid}}
    myForm.lastName.$error = {{myForm.lastName.$error}}
    myForm.$valid = {{myForm.$valid}}
    myForm.$error.required = {{!!myForm.$error.required}}
    myForm.$error.minlength = {{!!myForm.$error.minlength}}
    myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
    var user = element(by.exactBinding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); var formValid = element(by.binding('myForm.$valid')); var userNameInput = element(by.model('user.name')); var userLastInput = element(by.model('user.last')); it('should initialize to model', function() { expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); expect(userNameValid.getText()).toContain('true'); expect(formValid.getText()).toContain('true'); }); it('should be invalid if empty when required', function() { userNameInput.clear(); userNameInput.sendKeys(''); expect(user.getText()).toContain('{"last":"visitor"}'); expect(userNameValid.getText()).toContain('false'); expect(formValid.getText()).toContain('false'); }); it('should be valid if empty when min length is set', function() { userLastInput.clear(); userLastInput.sendKeys(''); expect(user.getText()).toContain('{"name":"guest","last":""}'); expect(lastNameValid.getText()).toContain('true'); expect(formValid.getText()).toContain('true'); }); it('should be invalid if less than required min length', function() { userLastInput.clear(); userLastInput.sendKeys('xx'); expect(user.getText()).toContain('{"name":"guest"}'); expect(lastNameValid.getText()).toContain('false'); expect(lastNameError.getText()).toContain('minlength'); expect(formValid.getText()).toContain('false'); }); it('should be invalid if longer than max length', function() { userLastInput.clear(); userLastInput.sendKeys('some ridiculously long name'); expect(user.getText()).toContain('{"name":"guest"}'); expect(lastNameValid.getText()).toContain('false'); expect(lastNameError.getText()).toContain('maxlength'); expect(formValid.getText()).toContain('false'); });
    */ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', require: ['?ngModel'], link: { pre: function(scope, element, attr, ctrls) { if (ctrls[0]) { (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, $browser, $filter, $parse); } } } }; }]; var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; /** * @ngdoc directive * @name ngValue * * @description * Binds the given expression to the value of `
    it('should load template defined inside script tag', function() { element(by.css('#tpl-link')).click(); expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); }); */ var scriptDirective = ['$templateCache', function($templateCache) { return { restrict: 'E', terminal: true, compile: function(element, attr) { if (attr.type == 'text/ng-template') { var templateUrl = attr.id, text = element[0].text; $templateCache.put(templateUrl, text); } } }; }]; var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive * @name select * @restrict E * * @description * HTML `SELECT` element with angular data-binding. * * # `ngOptions` * * The `ngOptions` attribute can be used to dynamically generate a list of `