Skip to content

Instantly share code, notes, and snippets.

@nzifnab
Last active October 11, 2015 01:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nzifnab/3779098 to your computer and use it in GitHub Desktop.
Save nzifnab/3779098 to your computer and use it in GitHub Desktop.
Coffeescript for stat calculator
###
I don't advise trying to edit this file directly. This was coded using coffeescript.
Coffeescript source here: https://gist.github.com/3779098
###
`function onOpen(){}`
`function populateProfile(){}`
`function clearUpgradeFields(){}`
`function clearAllFields(){}`
`function spreadsheetName(){}`
`function heroSelectClickHandler(){}`
flattenArray = (arr) ->
return [] if arr.length == 0
arr.reduce (sum, val) -> sum.concat(val)
padArray = (arr, size, value) ->
length = Math.abs(size) - arr.length
newArr = [].concat(arr)
return newArr if (length <= 0)
i = 0
while i < length
if size < 0 then newArr.unshift(value) else newArr.push(value)
i++
newArr
class CellRanges
###
If you moved things around, you'll need to update the
cell ranges here. Standard cell range notation.
###
@SHEET_VERSION: "O5"
@THIS_SHEET: "O7"
@MAIN_SHEET: "N5"
@ADVANCED_SHEET: "N7"
@BATTLE_TAG: "X4"
@HERO_ID: "X5"
@CLASS_ABBREV: "X6"
@LEVEL: "X7"
@PARAGON_LEVEL: "X9"
@LOCALIZATION: "X10"
@LOGGER: "Z1"
@HEAD: "B3:B13"
@SHOULDERS: "F3:F13"
@HANDS: "B16:B26"
@BRACERS: "F16:F26"
@TORSO: "J16:J26"
@LEGS: "B29:B39"
@FEET: "F29:F39"
@WAIST: "J29:J39"
@NECK: "B42:B54"
@RIGHTFINGER: "F42:F54"
@LEFTFINGER: "J42:J54"
@MAINHAND: "B57:B73"
@OFFHAND: "F57:F73"
@SET: "J3:J13"
@HEAD_UPGRADE: "C3:C13"
@SHOULDERS_UPGRADE: "G3:G13"
@HANDS_UPGRADE: "C16:C26"
@BRACERS_UPGRADE: "G16:G26"
@TORSO_UPGRADE: "K16:K26"
@LEGS_UPGRADE: "C29:C39"
@FEET_UPGRADE: "G29:G39"
@WAIST_UPGRADE: "K29:K39"
@NECK_UPGRADE: "C42:C54"
@RIGHTFINGER_UPGRADE: "G42:G54"
@LEFTFINGER_UPGRADE: "K42:K54"
@MAINHAND_UPGRADE: "C57:C73"
@OFFHAND_UPGRADE: "G57:G73"
@SET_UPGRADE: "K3:K13"
@HEAD_ADVANCED: "B3:B12"
@SHOULDERS_ADVANCED: "F3:F12"
@HANDS_ADVANCED: "B15:B24"
@BRACERS_ADVANCED: "F15:F24"
@TORSO_ADVANCED: "J15:J24"
@LEGS_ADVANCED: "B27:B36"
@FEET_ADVANCED: "F27:F36"
@WAIST_ADVANCED: "J27:J36"
@NECK_ADVANCED: "B39:B48"
@RIGHTFINGER_ADVANCED: "F39:F48"
@LEFTFINGER_ADVANCED: "J39:J48"
@MAINHAND_ADVANCED: "B51:B60"
@OFFHAND_ADVANCED: "F51:F62"
@SET_ADVANCED: "J3:J12"
@HEAD_ADVANCED_UPGRADE: "C3:C12"
@SHOULDERS_ADVANCED_UPGRADE: "G3:G12"
@HANDS_ADVANCED_UPGRADE: "C15:C24"
@BRACERS_ADVANCED_UPGRADE: "G15:G24"
@TORSO_ADVANCED_UPGRADE: "K15:K24"
@LEGS_ADVANCED_UPGRADE: "C27:C36"
@FEET_ADVANCED_UPGRADE: "G27:G36"
@WAIST_ADVANCED_UPGRADE: "K27:K36"
@NECK_ADVANCED_UPGRADE: "C39:C48"
@RIGHTFINGER_ADVANCED_UPGRADE: "G39:G48"
@LEFTFINGER_ADVANCED_UPGRADE: "K39:K48"
@MAINHAND_ADVANCED_UPGRADE: "C51:C60"
@OFFHAND_ADVANCED_UPGRADE: "G51:G62"
@SET_ADVANCED_UPGRADE: "K3:K12"
@FOLLOWER_BUFFS: "X12:X16"
@PLAYER_BUFFS: "X17:X41"
@verticalSize: (range) ->
matches = @[range].match /^[A-Z]+(\d+)\:[A-Z]+(\d+)$/
minRange = parseInt(matches[1])
maxRange = parseInt(matches[2])
totalCells = maxRange - minRange + 1
onOpen = ->
SpreadsheetApp.getActiveSpreadsheet().addMenu(
'Diablo', [
{ name: 'Populate Profile', functionName: 'populateProfile' },
{ name: 'Clear Upgrade Fields', functionName: 'clearUpgradeFields' },
{ name: 'Clear All', functionName: 'clearAllFields' }
]
)
spreadsheetName = (dummy) ->
SpreadsheetApp.getActiveSheet().getName()
populateProfile = ->
try
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet())
profile = new Profile(sheets)
return D3Logger.log(sheets.main, "") and false if profile.battleTag == 'cancel'
profile.populateSheet()
D3Logger.log(sheets.main, "")
catch error
Browser.msgBox("An error occurred when attempting to perform that action. Error: #{error}") unless error == "IGNOREME"
clearUpgradeFields = ->
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet())
for slot in ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"]
sheets.main.getRange(CellRanges["#{slot.toUpperCase()}_UPGRADE"]).setValue('')
sheets.advanced.getRange(CellRanges["#{slot.toUpperCase()}_ADVANCED_UPGRADE"]).setValue('')
clearAllFields = ->
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet())
for slot in ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"]
try
sheets.main.getRange(CellRanges[slot.toUpperCase()]).setValue('')
sheets.main.getRange(CellRanges["#{slot.toUpperCase()}_UPGRADE"]).setValue('')
sheets.advanced.getRange(CellRanges["#{slot.toUpperCase()}_ADVANCED"]).setValue('')
sheets.advanced.getRange(CellRanges["#{slot.toUpperCase()}_ADVANCED_UPGRADE"]).setValue('')
catch error
Browser.msgBox("an error occurred when clearing out the #{slot} slot: #{error}")
for range in ["battle_tag", "hero_id", "class_abbrev", "level", "paragon_level", "logger"]
sheets.main.getRange(CellRanges[range.toUpperCase()]).setValue('')
for range in ["follower_buffs", "player_buffs"]
sheets.main.getRange(CellRanges[range.toUpperCase()]).setValue('N')
heroSelectClickHandler = (e) ->
Profile.heroSelectClickHandler(e)
class API
@parse: (path) ->
response = UrlFetchApp.fetch("http://#{API.locale}.battle.net/api/d3/#{path}")
JSON.parse(response.getContentText())
@locale: 'us'
class D3Logger
@log: (sheet, text) ->
###
sheet.getRange(CellRanges.LOGGER).setValue(text)
###
class SheetCollection
constructor: (sheet) ->
@spreadsheet = sheet.getParent()
@version = sheet.getRange(CellRanges.SHEET_VERSION).getValue()
mainSheetName = sheet.getRange(CellRanges.MAIN_SHEET).getValue()
advancedSheetName = sheet.getRange(CellRanges.ADVANCED_SHEET).getValue()
@main = @findByName(mainSheetName)
@advanced = @findByName(advancedSheetName)
findByName: (name) ->
@all ||= do =>
sheets = {}
for sheet in @spreadsheet.getSheets()
sheets[sheet.getRange(CellRanges.THIS_SHEET).getValue()] = sheet
sheets
@all[name]
class Profile
constructor: (@sheets) ->
D3Logger.log(@sheets.main, "Collecting BattleTag")
battleTagCell = @sheets.main.getRange(CellRanges.BATTLE_TAG)
@battleTag = battleTagCell.getValue()
if !@battleTag? || @battleTag == ""
@battleTag = Browser.inputBox("Enter your battletag: Example#1234")
return false if @battleTag == 'cancel'
@battleTag = @battleTag.split("#").join('-')
battleTagCell.setValue(@battleTag.split('-').join('#'))
localeCell = @sheets.main.getRange(CellRanges.LOCALIZATION)
API.locale = localeCell.getValue()
API.locale = if API.locale == '' then 'us' else API.locale
heroes: ->
@_heroes ||= do =>
try
D3Logger.log(@sheets.main, "Parsing Profile Data")
data = API.parse("profile/#{@battleTag}/")
(new Hero(@sheets.main, hero) for hero in data.heroes)
catch error
Browser.msgBox("An error occurred when accessing your profile data. Is your battleTag correct? Error: #{error}")
throw "IGNOREME"
createHeroSelectionInterface: (options) ->
D3Logger.log(@sheets.main, "")
heroSelect = UiApp.createApplication().setTitle("Select which hero to use")
handler = heroSelect.createServerHandler("heroSelectClickHandler")
panel = heroSelect.createVerticalPanel()
buttons = []
for option in options
btn = heroSelect.createButton(option.description).setId(option.id).addClickHandler(handler)
buttons.push btn
panel.add(btn)
heroSelect.add(panel)
@sheets.spreadsheet.show(heroSelect)
selectedHero: ->
@_selectedHero ||= do =>
heroId = @sheets.main.getRange(CellRanges.HERO_ID).getValue()
if !heroId? || heroId == ""
options = ({id: hero.id, description: "#{hero.name} &lt;#{if hero.hardcore then "<span style='color:red'>hardcore</span> " else ""}#{hero['class']}&gt; - Level #{hero.level} (#{hero.paragonLevel})"} for hero in @heroes())
@createHeroSelectionInterface(options)
false
else
D3Logger.log(@sheets.main, "Parsing Hero Data")
try
hero = API.parse("profile/#{@battleTag}/hero/#{heroId}")
@sheets.main.getRange(CellRanges.CLASS_ABBREV).setValue(Hero.abbrev(hero['class']))
@sheets.main.getRange(CellRanges.LEVEL).setValue(hero.level)
@sheets.main.getRange(CellRanges.PARAGON_LEVEL).setValue(hero.paragonLevel)
new Hero(@sheets, hero)
catch error
Browser.msgBox("An error occurred when accessing your hero data. Bnet may be temporarily down or the rate limit for requests has been reached (or something else?). Error: #{error}")
throw "IGNOREME"
populateItems: ->
return false unless (hero = @selectedHero())
for slot in ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"]
@sheets.main.getRange(CellRanges[slot.toUpperCase()]).setValue('')
@sheets.advanced.getRange(CellRanges["#{slot.toUpperCase()}_ADVANCED"]).setValue('')
for item in hero.items()
item.populateSpreadsheet(@sheets.main)
ItemSet.populateSpreadsheet(@sheets)
true
populateSkills: ->
try
return false unless (hero = @selectedHero())
hero.skills().populateSpreadsheet(@sheets.main)
catch error
Browser.msgBox("An error occurred: #{error}") unless error == 'IGNOREME'
true
populateSheet: ->
try
return false unless @populateItems()
return false unless @populateSkills()
catch error
Browser.msgBox("An error occurred: #{error}") unless error == 'IGNOREME'
true
@heroSelectClickHandler: (e) ->
heroId = e.parameter.source
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet())
sheets.main.getRange(CellRanges.HERO_ID).setValue(heroId)
profile = new Profile(sheets)
profile.populateSheet()
UiApp.getActiveApplication().close()
class Hero
@ABBREV_MAP: {
'wizard': 'W',
'barbarian': 'B',
'witch-doctor': 'WD',
'demon-hunter': 'DH',
'monk': 'M'
}
constructor: (@sheets, @data) ->
{@name, @id, @level, @paragonLevel, @class, @hardcore} = @data
@resists = {Fire: 0, Lightning: 0, Poison: 0, Physical: 0, Arcane: 0, Cold: 0}
items: ->
@_items ||= do =>
items = []
for own slot, item of @data.items
itemKlass = Item.klassForSlot(slot)
i = new itemKlass(@sheets, slot, item)
if @oneWithEverything()
for own type of @resists
@resists[type] += i.attributes().resists[type]
items.push(i)
if @oneWithEverything()
maxResistValue = 0
for own type, val of @resists
if val >= maxResistValue
maxResistType = type
maxResistValue = val
for item in items
item.attributes().allResist += item.attributes().resists[maxResistType]
items
oneWithEverything: ->
@class == 'monk' && @skills().has('One With Everything')[0] == 'Y'
skills: ->
@_skills ||= do =>
new Skills(@sheets, @class, @data.skills.passive.concat(@data.skills.active))
@abbrev: (name) ->
Hero.ABBREV_MAP[name]
class Skills
constructor: (@sheets, @klass, @data) ->
@names = []
for skill in @data
@names.push(skill.skill.name) if skill.skill?
@names.push(skill.rune.name) if skill.rune?
populateSpreadsheet: ->
@sheets.main.getRange(CellRanges.PLAYER_BUFFS).setValues(@displayableSkills())
has: (name) ->
if name in @names then ['Y'] else ['N']
displayableSkills: ->
skills = switch @klass
when "wizard"
[
[""],
@has('Energy Armor'),
@has('Pinpoint Barrier'),
@has('Prismatic Armor'),
@has('Familiar'),
@has('Sparkflint'),
@has('Vigoron'),
@has('Magic Weapon'),
@has('Force Weapon'),
@has('Blood Magic'),
@has('Frost Nova'),
@has('Deep Freeze'),
@has('Bone Chill'),
@has('Ice Armor'),
@has('Crystallize'),
@has('Slow Time'),
@has('Time Warp'),
@has('Stretch Time'),
@has('Safe Passage'),
[""],
@has('Blur'),
@has('Glass Cannon'),
@has("Galvanizing Ward")
]
when 'demon-hunter'
[
[""],
@has('Boar Companion'),
@has('Sentry'),
@has('Aid Station'),
@has('Guardian Turret'),
@has('Bait the Trap'),
@has('Hardened'),
@has('Shadow Power'),
@has('Gloom'),
[""],
@has('Steady Aim'),
@has('Archery'),
@has('Sharpshooter'),
@has('Perfectionist'),
@has('Brooding'),
@has('Cull the Weak')
]
when 'barbarian'
[
[""],
@has('War Cry'),
@has('Hardened Wrath'),
@has('Invigorate'),
@has("Veteran's Warning"),
@has("Impunity"),
@has("Battle Rage"),
@has("Marauder's Rage"),
@has('Punish'),
@has('Overpower'),
@has('Crushing Advance'),
@has('Killing Spree'),
@has('Best Served Cold'),
@has('Threatening Shout'),
@has('Falter'),
[""],
@has("Ruthless"),
@has("Nerves of Steel"),
@has("Weapons Master"),
@has("Superstition"),
@has("Tough as Nails"),
@has('Bloodthirst'),
@has('Inspiring Presence')
]
when 'monk'
[
[""],
@has('Deadly Reach'),
@has('Foresight'),
@has('Keen Eye'),
@has('Blazing Wrath'),
@has("Earth Ally"),
@has("Mantra of Evasion"),
@has("Hard Target"),
@has("Transgression"),
@has("Mantra of Healing"),
@has("Heavenly Body"),
@has("Time of Need"),
@has("Sustenance"),
@has("Boon of Inspiration"),
@has("Mantra of Conviction"),
@has("Overawe"),
@has("Intimidation"),
@has("Reclamation"),
@has("Concussion"),
@has("Lightning Flash"),
[""],
@has("Resolve"),
@has("Seize the Initiative"),
@has("The Guardian's Path"),
@has("Sixth Sense")
]
when 'witch-doctor'
[
[""],
@has("Life Link"),
@has("Provoke the Pack"),
@has("Soul Harvest"),
[""],
@has("Jungle Fortitude"),
@has("Pierce the Veil"),
@has("Blood Ritual"),
@has("Zombie Handler")
]
else
throw "Unrecognized class: #{@klass}"
totalCells = CellRanges.verticalSize('PLAYER_BUFFS')
padArray(skills, totalCells, ['N'])
class ItemSet
@recordedSets: {}
constructor: (@data) ->
@quantity = 0
{@ranks} = @data
attributeStrings: ->
@_attrs ||= flattenArray(rank.attributes for rank in @ranks when parseInt(rank.required) <= @quantity)
attributesRaw: ->
@_attributesRaw ||= Item.parseAttributesRaw(@attributeStrings())
@attributesRaw: ->
@_attributesRaw ||= Item.combineRawValues(set.attributesRaw() for own slug, set of @recordedSets when set.attributeStrings().length > 0)
@record: (data) ->
@recordedSets[data.slug] ||= new ItemSet(data)
@recordedSets[data.slug].quantity += 1
@recordedSets[data.slug]
@populateSpreadsheet: (sheets) ->
(new SetBonuses(sheets, 'set', {attributesRaw: @attributesRaw()})).populateSpreadsheet()
true
class Item
constructor: (@sheets, @slot, @basicData) ->
[@name, @itemId] = [@basicData.name, @basicData.tooltipParams]
attributes: ->
@_attributes ||= do =>
D3Logger.log(@sheets.main, "Parsing item: #{@slot}")
try
data = API.parse("data/#{@itemId}")
@type = Item.parseType(data.type.id)
@set = ItemSet.record(data.set) if data.set?
Item.parseAttributes(data)
catch error
Browser.msgBox("An error occurred when attempting to access item properties for the #{@slot} slot. Error: #{error}")
throw "IGNOREME"
populateSpreadsheet: ->
mainRange = @sheets.main.getRange(CellRanges[@slot.toUpperCase()])
advancedRange = @sheets.advanced.getRange(CellRanges["#{@slot.toUpperCase()}_ADVANCED"])
mainRange.setValues(@displayableAttributes(@attributes()))
advancedRange.setValues(@displayableAttributes(@attributes(), advanced: true))
displayableAttributes: (attrs, options = {}) ->
options.advanced ?= false
if options.advanced
[
[attrs.loh], [attrs.ls], [attrs.lifeRegen], [attrs.globes], [attrs.blockChance], [attrs.meleeDR],
[attrs.rangedDR], [attrs.eliteDR], [attrs.eliteBonus], [attrs.demonBonus]
]
else
[
[attrs.armor], [attrs.strength], [attrs.dexterity],
[attrs.intelligence], [attrs.vitality], [attrs.allResist], [attrs.lifePercent],
[attrs.critDamage], [attrs.critPercent], [attrs.ias],
[attrs.elementalPercent]
]
@parseType: (type) ->
switch type
when "CeremonialDagger"
"WD Knife"
when "FistWeapon"
"Fist"
when "MightyWeapon1H"
"Mighty"
when "MightyWeapon2H"
"2H Mighty"
when "Axe2H"
"2H Axe"
when "Mace2H"
"2H Mace"
when "Sword2H"
"2H Sword"
when "HandXBow"
"Hand Crossbow"
when "Orb"
"W Source"
when "Mojo"
"WD Mojo"
when "Quiver"
"DH Quiver"
else type
@klassForSlot: (slot) ->
switch slot
when "rightFinger", "leftFinger", "neck"
Jewelry
when "mainHand"
Weapon
when "offHand"
Offhand
else
Armor
@parseAttributesRaw: (rawData) ->
attributes = {}
for rawStr in rawData
if(match = rawStr.match(/^\+(\d+) (Dexterity|Intelligence|Strength|Vitality)$/))
name = "#{match[2]}_Item"
val = attributes[name]?.min || 0
val += parseInt(match[1])
else if(match = rawStr.match(/^\+(\d+) Resistance to All Elements$/))
name = "Resistance_All"
val = attributes[name]?.min || 0
val += parseInt(match[1])
else if(match = rawStr.match(/^Critical Hit Chance Increased by (\d+\.\d+)%$/))
name = "Crit_Percent_Bonus_Capped"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Attack Speed Increased by (\d+)%$/))
name = "Attacks_Per_Second_Item_Percent"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^\+(\d+)% Life$/))
name = "Hitpoints_Max_Percent_Bonus_Item"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Critical Hit Damage Increased by (\d+)%$/))
name = "Crit_Damage_Percent"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
# Advanced Stats
else if(match = rawStr.match(/^Reduces damage from melee attacks by (\d+)%\.?$/))
name = "Damage_Percent_Reduction_From_Melee"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Reduces damage from ranged attacks by (\d+)%\.?$/))
name = "Damage_Percent_Reduction_From_Ranged"
val += attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Increases Damage Against Elites by (\d+)%$/))
name = "Damage_Percent_Bonus_Vs_Elites"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^\+(\d+)% Damage to Demons$/))
name = "Damage_Percent_Bonus_Vs_Monster_Type#Demon"
val = attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Reduces damage from elites by (\d+)%\.?$/))
name = "Damage_Percent_Reduction_From_Elites"
val += attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^Regenerates (\d+) Life per Second$/))
name = "Hitpoints_Regen_Per_Second"
val += attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
else if(match = rawStr.match(/^(\d+\.\d+)% of Damage Dealt Is Converted to Life$/))
name = "Steal_Health_Percent"
val += attributes[name]?.min || 0
val += parseFloat(match[1]) / 100
attributes[name] = {min: val, max: val}
attributes
@parseAttributes: (data) ->
# Basic
armor = 0
strength = 0
dexterity = 0
intelligence = 0
vitality = 0
allResist = 0
resists = {Fire: 0, Lightning: 0, Poison: 0, Physical: 0, Arcane: 0, Cold: 0}
lifePercent = 0
critDamage = 0
critPercent = 0
ias = 0
elementalPercent = 0
minBaseDmg = 0
minBonusDmg = 0
minPhysBonusDmg = 0
minJewelryDmg = 0
maxJewelryDmg = 0
deltaBonusDmg = 0
deltaBaseDmg = 0
deltaPhysBonusDmg = 0
dmgPercent = 0
aps = 0
# Advanced
loh = 0
ls = 0
lifeRegen = 0
blockChance = 0
meleeDR = 0
rangedDR = 0
eliteDR = 0
eliteBonus = 0
demonBonus = 0
blockMin = 0
blockMax = 0
globes = 0
for attributes in [data.attributesRaw].concat(if data.gems then (gem.attributesRaw for gem in data.gems) else [])
for own name, values of attributes
switch name
when "Armor_Item"
armor += parseFloat(values.min)
when "Armor_Bonus_Item"
armor += parseFloat(values.min)
when "Strength_Item"
strength += parseFloat(values.min)
when "Dexterity_Item"
dexterity += parseFloat(values.min)
when "Intelligence_Item"
intelligence += parseFloat(values.min)
when "Vitality_Item"
vitality += parseFloat(values.min)
when "Resistance_All"
allResist += parseFloat(values.min)
when "Resistance#Fire", "Resistance#Lightning", "Resistance#Poison", "Resistance#Physical", "Resistance#Arcane", "Resistance#Cold"
resists[name.split('#')[1]] += parseFloat(values.min)
when "Hitpoints_Max_Percent_Bonus_Item"
lifePercent += parseFloat(values.min)*100
when "Crit_Damage_Percent"
critDamage += parseFloat(values.min)*100
when "Crit_Percent_Bonus_Capped"
critPercent += parseFloat(values.min)*100
when "Attacks_Per_Second_Percent", "Attacks_Per_Second_Item_Percent"
ias += parseFloat(values.min)*100
when "Damage_Type_Percent_Bonus#Fire", "Damage_Type_Percent_Bonus#Lightning", "Damage_Type_Percent_Bonus#Poison", "Damage_Type_Percent_Bonus#Arcane", "Damage_Type_Percent_Bonus#Cold"
elementalPercent += parseFloat(values.min)*100
when "Damage_Weapon_Min#Physical", "Damage_Min#Physical"
minBaseDmg += parseFloat(values.min)
minJewelryDmg += parseFloat(values.min)
maxJewelryDmg += parseFloat(values.min)
when "Damage_Bonus_Min#Physical"
minBaseDmg += parseFloat(values.min)
minJewelryDmg += parseFloat(values.min)
when "Damage_Weapon_Bonus_Min#Physical"
minPhysBonusDmg += parseFloat(values.min)
when "Damage_Weapon_Delta#Physical", "Damage_Delta#Physical"
deltaBaseDmg += parseFloat(values.min)
maxJewelryDmg += parseFloat(values.min)
when "Damage_Weapon_Bonus_Delta#Physical"
deltaPhysBonusDmg += parseFloat(values.min)
when "Damage_Weapon_Percent_Bonus#Physical"
dmgPercent += parseFloat(values.min)
when "Attacks_Per_Second_Item_Bonus"
aps += parseFloat(values.min)
when "Hitpoints_On_Hit"
loh += parseFloat(values.min)
when "Damage_Percent_Bonus_Vs_Elites"
eliteBonus += parseFloat(values.min)*100
when "Damage_Percent_Bonus_Vs_Monster_Type#Demon"
demonBonus += parseFloat(values.min)*100
when "Steal_Health_Percent"
ls += parseFloat(values.min)*100
when "Hitpoints_Regen_Per_Second"
lifeRegen += parseFloat(values.min)
when "Block_Chance_Item", "Block_Chance_Bonus_Item"*100
blockChance += parseFloat(values.min)
when "Damage_Percent_Reduction_From_Melee"*100
meleeDR += parseFloat(values.min)
when "Damage_Percent_Reduction_From_Elites"*100
eliteDR += parseFloat(values.min)
when "Damage_Percent_Reduction_From_Ranged"*100
rangedDR += parseFloat(values.min)
when "Block_Amount_Item_Min"
blockMin += parseFloat(values.min)
when "Block_Amount_Item_Delta"
blockMin += parseFloat(values.min)
blockMax += parseFloat(values.min)
when "Health_Globe_Bonus_Health"
globes += parseFloat(values.min)
else
if name.match(/^Damage_Weapon_Min#/) || name.match(/^Damage_Weapon_Bonus_Min#/)
minBonusDmg += parseFloat(values.min)
else if name.match(/^Damage_Weapon_Delta#/ || name.match(/^Damage_Weapon_Bonus_Delta#/))
deltaBonusDmg += parseFloat(values.min)
minTooltipDmg = (minPhysBonusDmg + minBaseDmg) * (1 + dmgPercent) + minBonusDmg
maxTooltipDmg = if (minBaseDmg == 0 && minPhysBonusDmg == 0) then 0 else (Math.max(minPhysBonusDmg + minBaseDmg, minBaseDmg + deltaBaseDmg - 1) + 1 + deltaPhysBonusDmg) * (1 + dmgPercent) + minBonusDmg + deltaBonusDmg
{
armor, strength, dexterity, intelligence, vitality,
allResist, resists, lifePercent, critDamage, critPercent,
ias, elementalPercent, minPhysBonusDmg, deltaPhysBonusDmg,
minBaseDmg, deltaBaseDmg, minBonusDmg, minJewelryDmg, maxJewelryDmg,
deltaBonusDmg, dmgPercent, aps, minTooltipDmg, maxTooltipDmg,
loh, eliteBonus, demonBonus, ls, lifeRegen, blockChance, meleeDR,
eliteDR, rangedDR, blockMin, blockMax, globes
}
@combineRawValues: (arr) ->
result = {}
for obj in arr
for own key, val of obj
result[key] ||= {min: 0, max: 0}
result[key].min += val.min
result[key].max += val.max
result
class Armor extends Item
class Jewelry extends Item
displayableAttributes: (attrs, opts={advanced: false}) ->
if opts.advanced
super
else
[
[attrs.minJewelryDmg], [attrs.maxJewelryDmg]
].concat(super)
class Weapon extends Item
displayableAttributes: (attrs, opts={advanced: false}) ->
if opts.advanced
super
else
[
[@type], [attrs.minTooltipDmg], [attrs.maxTooltipDmg], [attrs.minBonusDmg], [attrs.minBonusDmg + attrs.deltaBonusDmg],
].concat(super).concat([
[attrs.aps]
])
class Offhand extends Weapon
displayableAttributes: (attrs, opts={advanced: false}) ->
if opts.advanced
super.concat([
[attrs.blockMin], [attrs.blockMax]
])
else
super
class SetBonuses extends Item
constructor: (@sheets, @slot, @basicData) ->
attributes: ->
@_attributes ||= do =>
Item.parseAttributes(@basicData)
/*
I don't advise trying to edit this file directly. This was coded using coffeescript.
Coffeescript source here: https://gist.github.com/3779098
*/
function onOpen(){};
function populateProfile(){};
function clearUpgradeFields(){};
function clearAllFields(){};
function spreadsheetName(){};
function heroSelectClickHandler(){};
var API, Armor, CellRanges, D3Logger, Hero, Item, ItemSet, Jewelry, Offhand, Profile, SetBonuses, SheetCollection, Skills, Weapon, clearAllFields, clearUpgradeFields, flattenArray, heroSelectClickHandler, onOpen, padArray, populateProfile, spreadsheetName,
__hasProp = {}.hasOwnProperty,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
flattenArray = function(arr) {
if (arr.length === 0) {
return [];
}
return arr.reduce(function(sum, val) {
return sum.concat(val);
});
};
padArray = function(arr, size, value) {
var i, length, newArr;
length = Math.abs(size) - arr.length;
newArr = [].concat(arr);
if (length <= 0) {
return newArr;
}
i = 0;
while (i < length) {
if (size < 0) {
newArr.unshift(value);
} else {
newArr.push(value);
}
i++;
}
return newArr;
};
CellRanges = (function() {
function CellRanges() {}
/*
If you moved things around, you'll need to update the
cell ranges here. Standard cell range notation.
*/
CellRanges.SHEET_VERSION = "O5";
CellRanges.THIS_SHEET = "O7";
CellRanges.MAIN_SHEET = "N5";
CellRanges.ADVANCED_SHEET = "N7";
CellRanges.BATTLE_TAG = "X4";
CellRanges.HERO_ID = "X5";
CellRanges.CLASS_ABBREV = "X6";
CellRanges.LEVEL = "X7";
CellRanges.PARAGON_LEVEL = "X9";
CellRanges.LOCALIZATION = "X10";
CellRanges.LOGGER = "Z1";
CellRanges.HEAD = "B3:B13";
CellRanges.SHOULDERS = "F3:F13";
CellRanges.HANDS = "B16:B26";
CellRanges.BRACERS = "F16:F26";
CellRanges.TORSO = "J16:J26";
CellRanges.LEGS = "B29:B39";
CellRanges.FEET = "F29:F39";
CellRanges.WAIST = "J29:J39";
CellRanges.NECK = "B42:B54";
CellRanges.RIGHTFINGER = "F42:F54";
CellRanges.LEFTFINGER = "J42:J54";
CellRanges.MAINHAND = "B57:B73";
CellRanges.OFFHAND = "F57:F73";
CellRanges.SET = "J3:J13";
CellRanges.HEAD_UPGRADE = "C3:C13";
CellRanges.SHOULDERS_UPGRADE = "G3:G13";
CellRanges.HANDS_UPGRADE = "C16:C26";
CellRanges.BRACERS_UPGRADE = "G16:G26";
CellRanges.TORSO_UPGRADE = "K16:K26";
CellRanges.LEGS_UPGRADE = "C29:C39";
CellRanges.FEET_UPGRADE = "G29:G39";
CellRanges.WAIST_UPGRADE = "K29:K39";
CellRanges.NECK_UPGRADE = "C42:C54";
CellRanges.RIGHTFINGER_UPGRADE = "G42:G54";
CellRanges.LEFTFINGER_UPGRADE = "K42:K54";
CellRanges.MAINHAND_UPGRADE = "C57:C73";
CellRanges.OFFHAND_UPGRADE = "G57:G73";
CellRanges.SET_UPGRADE = "K3:K13";
CellRanges.HEAD_ADVANCED = "B3:B12";
CellRanges.SHOULDERS_ADVANCED = "F3:F12";
CellRanges.HANDS_ADVANCED = "B15:B24";
CellRanges.BRACERS_ADVANCED = "F15:F24";
CellRanges.TORSO_ADVANCED = "J15:J24";
CellRanges.LEGS_ADVANCED = "B27:B36";
CellRanges.FEET_ADVANCED = "F27:F36";
CellRanges.WAIST_ADVANCED = "J27:J36";
CellRanges.NECK_ADVANCED = "B39:B48";
CellRanges.RIGHTFINGER_ADVANCED = "F39:F48";
CellRanges.LEFTFINGER_ADVANCED = "J39:J48";
CellRanges.MAINHAND_ADVANCED = "B51:B60";
CellRanges.OFFHAND_ADVANCED = "F51:F62";
CellRanges.SET_ADVANCED = "J3:J12";
CellRanges.HEAD_ADVANCED_UPGRADE = "C3:C12";
CellRanges.SHOULDERS_ADVANCED_UPGRADE = "G3:G12";
CellRanges.HANDS_ADVANCED_UPGRADE = "C15:C24";
CellRanges.BRACERS_ADVANCED_UPGRADE = "G15:G24";
CellRanges.TORSO_ADVANCED_UPGRADE = "K15:K24";
CellRanges.LEGS_ADVANCED_UPGRADE = "C27:C36";
CellRanges.FEET_ADVANCED_UPGRADE = "G27:G36";
CellRanges.WAIST_ADVANCED_UPGRADE = "K27:K36";
CellRanges.NECK_ADVANCED_UPGRADE = "C39:C48";
CellRanges.RIGHTFINGER_ADVANCED_UPGRADE = "G39:G48";
CellRanges.LEFTFINGER_ADVANCED_UPGRADE = "K39:K48";
CellRanges.MAINHAND_ADVANCED_UPGRADE = "C51:C60";
CellRanges.OFFHAND_ADVANCED_UPGRADE = "G51:G62";
CellRanges.SET_ADVANCED_UPGRADE = "K3:K12";
CellRanges.FOLLOWER_BUFFS = "X12:X16";
CellRanges.PLAYER_BUFFS = "X17:X41";
CellRanges.verticalSize = function(range) {
var matches, maxRange, minRange, totalCells;
matches = this[range].match(/^[A-Z]+(\d+)\:[A-Z]+(\d+)$/);
minRange = parseInt(matches[1]);
maxRange = parseInt(matches[2]);
return totalCells = maxRange - minRange + 1;
};
return CellRanges;
})();
onOpen = function() {
return SpreadsheetApp.getActiveSpreadsheet().addMenu('Diablo', [
{
name: 'Populate Profile',
functionName: 'populateProfile'
}, {
name: 'Clear Upgrade Fields',
functionName: 'clearUpgradeFields'
}, {
name: 'Clear All',
functionName: 'clearAllFields'
}
]);
};
spreadsheetName = function(dummy) {
return SpreadsheetApp.getActiveSheet().getName();
};
populateProfile = function() {
var profile, sheets;
try {
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet());
profile = new Profile(sheets);
if (profile.battleTag === 'cancel') {
return D3Logger.log(sheets.main, "") && false;
}
profile.populateSheet();
return D3Logger.log(sheets.main, "");
} catch (error) {
if (error !== "IGNOREME") {
return Browser.msgBox("An error occurred when attempting to perform that action. Error: " + error);
}
}
};
clearUpgradeFields = function() {
var sheets, slot, _i, _len, _ref, _results;
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet());
_ref = ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"];
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
slot = _ref[_i];
sheets.main.getRange(CellRanges["" + (slot.toUpperCase()) + "_UPGRADE"]).setValue('');
_results.push(sheets.advanced.getRange(CellRanges["" + (slot.toUpperCase()) + "_ADVANCED_UPGRADE"]).setValue(''));
}
return _results;
};
clearAllFields = function() {
var range, sheets, slot, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results;
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet());
_ref = ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
slot = _ref[_i];
try {
sheets.main.getRange(CellRanges[slot.toUpperCase()]).setValue('');
sheets.main.getRange(CellRanges["" + (slot.toUpperCase()) + "_UPGRADE"]).setValue('');
sheets.advanced.getRange(CellRanges["" + (slot.toUpperCase()) + "_ADVANCED"]).setValue('');
sheets.advanced.getRange(CellRanges["" + (slot.toUpperCase()) + "_ADVANCED_UPGRADE"]).setValue('');
} catch (error) {
Browser.msgBox("an error occurred when clearing out the " + slot + " slot: " + error);
}
}
_ref1 = ["battle_tag", "hero_id", "class_abbrev", "level", "paragon_level", "logger"];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
range = _ref1[_j];
sheets.main.getRange(CellRanges[range.toUpperCase()]).setValue('');
}
_ref2 = ["follower_buffs", "player_buffs"];
_results = [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
range = _ref2[_k];
_results.push(sheets.main.getRange(CellRanges[range.toUpperCase()]).setValue('N'));
}
return _results;
};
heroSelectClickHandler = function(e) {
return Profile.heroSelectClickHandler(e);
};
API = (function() {
function API() {}
API.parse = function(path) {
var response;
response = UrlFetchApp.fetch("http://" + API.locale + ".battle.net/api/d3/" + path);
return JSON.parse(response.getContentText());
};
API.locale = 'us';
return API;
})();
D3Logger = (function() {
function D3Logger() {}
D3Logger.log = function(sheet, text) {
/*
sheet.getRange(CellRanges.LOGGER).setValue(text)
*/
};
return D3Logger;
})();
SheetCollection = (function() {
function SheetCollection(sheet) {
var advancedSheetName, mainSheetName;
this.spreadsheet = sheet.getParent();
this.version = sheet.getRange(CellRanges.SHEET_VERSION).getValue();
mainSheetName = sheet.getRange(CellRanges.MAIN_SHEET).getValue();
advancedSheetName = sheet.getRange(CellRanges.ADVANCED_SHEET).getValue();
this.main = this.findByName(mainSheetName);
this.advanced = this.findByName(advancedSheetName);
}
SheetCollection.prototype.findByName = function(name) {
var _this = this;
this.all || (this.all = (function() {
var sheet, sheets, _i, _len, _ref;
sheets = {};
_ref = _this.spreadsheet.getSheets();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
sheet = _ref[_i];
sheets[sheet.getRange(CellRanges.THIS_SHEET).getValue()] = sheet;
}
return sheets;
})());
return this.all[name];
};
return SheetCollection;
})();
Profile = (function() {
function Profile(sheets) {
var battleTagCell, localeCell;
this.sheets = sheets;
D3Logger.log(this.sheets.main, "Collecting BattleTag");
battleTagCell = this.sheets.main.getRange(CellRanges.BATTLE_TAG);
this.battleTag = battleTagCell.getValue();
if (!(this.battleTag != null) || this.battleTag === "") {
this.battleTag = Browser.inputBox("Enter your battletag: Example#1234");
if (this.battleTag === 'cancel') {
return false;
}
}
this.battleTag = this.battleTag.split("#").join('-');
battleTagCell.setValue(this.battleTag.split('-').join('#'));
localeCell = this.sheets.main.getRange(CellRanges.LOCALIZATION);
API.locale = localeCell.getValue();
API.locale = API.locale === '' ? 'us' : API.locale;
}
Profile.prototype.heroes = function() {
var _this = this;
return this._heroes || (this._heroes = (function() {
var data, hero, _i, _len, _ref, _results;
try {
D3Logger.log(_this.sheets.main, "Parsing Profile Data");
data = API.parse("profile/" + _this.battleTag + "/");
_ref = data.heroes;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
hero = _ref[_i];
_results.push(new Hero(_this.sheets.main, hero));
}
return _results;
} catch (error) {
Browser.msgBox("An error occurred when accessing your profile data. Is your battleTag correct? Error: " + error);
throw "IGNOREME";
}
})());
};
Profile.prototype.createHeroSelectionInterface = function(options) {
var btn, buttons, handler, heroSelect, option, panel, _i, _len;
D3Logger.log(this.sheets.main, "");
heroSelect = UiApp.createApplication().setTitle("Select which hero to use");
handler = heroSelect.createServerHandler("heroSelectClickHandler");
panel = heroSelect.createVerticalPanel();
buttons = [];
for (_i = 0, _len = options.length; _i < _len; _i++) {
option = options[_i];
btn = heroSelect.createButton(option.description).setId(option.id).addClickHandler(handler);
buttons.push(btn);
panel.add(btn);
}
heroSelect.add(panel);
return this.sheets.spreadsheet.show(heroSelect);
};
Profile.prototype.selectedHero = function() {
var _this = this;
return this._selectedHero || (this._selectedHero = (function() {
var hero, heroId, options;
heroId = _this.sheets.main.getRange(CellRanges.HERO_ID).getValue();
if (!(heroId != null) || heroId === "") {
options = (function() {
var _i, _len, _ref, _results;
_ref = this.heroes();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
hero = _ref[_i];
_results.push({
id: hero.id,
description: "" + hero.name + " &lt;" + (hero.hardcore ? "<span style='color:red'>hardcore</span> " : "") + hero['class'] + "&gt; - Level " + hero.level + " (" + hero.paragonLevel + ")"
});
}
return _results;
}).call(_this);
_this.createHeroSelectionInterface(options);
return false;
} else {
D3Logger.log(_this.sheets.main, "Parsing Hero Data");
try {
hero = API.parse("profile/" + _this.battleTag + "/hero/" + heroId);
_this.sheets.main.getRange(CellRanges.CLASS_ABBREV).setValue(Hero.abbrev(hero['class']));
_this.sheets.main.getRange(CellRanges.LEVEL).setValue(hero.level);
_this.sheets.main.getRange(CellRanges.PARAGON_LEVEL).setValue(hero.paragonLevel);
return new Hero(_this.sheets, hero);
} catch (error) {
Browser.msgBox("An error occurred when accessing your hero data. Bnet may be temporarily down or the rate limit for requests has been reached (or something else?). Error: " + error);
throw "IGNOREME";
}
}
})());
};
Profile.prototype.populateItems = function() {
var hero, item, slot, _i, _j, _len, _len1, _ref, _ref1;
if (!(hero = this.selectedHero())) {
return false;
}
_ref = ["head", "shoulders", "hands", "bracers", "torso", "legs", "feet", "waist", "neck", "rightfinger", "leftfinger", "mainhand", "offhand", "set"];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
slot = _ref[_i];
this.sheets.main.getRange(CellRanges[slot.toUpperCase()]).setValue('');
this.sheets.advanced.getRange(CellRanges["" + (slot.toUpperCase()) + "_ADVANCED"]).setValue('');
}
_ref1 = hero.items();
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
item = _ref1[_j];
item.populateSpreadsheet(this.sheets.main);
}
ItemSet.populateSpreadsheet(this.sheets);
return true;
};
Profile.prototype.populateSkills = function() {
var hero;
try {
if (!(hero = this.selectedHero())) {
return false;
}
hero.skills().populateSpreadsheet(this.sheets.main);
} catch (error) {
if (error !== 'IGNOREME') {
Browser.msgBox("An error occurred: " + error);
}
}
return true;
};
Profile.prototype.populateSheet = function() {
try {
if (!this.populateItems()) {
return false;
}
if (!this.populateSkills()) {
return false;
}
} catch (error) {
if (error !== 'IGNOREME') {
Browser.msgBox("An error occurred: " + error);
}
}
return true;
};
Profile.heroSelectClickHandler = function(e) {
var heroId, profile, sheets;
heroId = e.parameter.source;
sheets = new SheetCollection(SpreadsheetApp.getActiveSheet());
sheets.main.getRange(CellRanges.HERO_ID).setValue(heroId);
profile = new Profile(sheets);
profile.populateSheet();
return UiApp.getActiveApplication().close();
};
return Profile;
})();
Hero = (function() {
Hero.ABBREV_MAP = {
'wizard': 'W',
'barbarian': 'B',
'witch-doctor': 'WD',
'demon-hunter': 'DH',
'monk': 'M'
};
function Hero(sheets, data) {
var _ref;
this.sheets = sheets;
this.data = data;
_ref = this.data, this.name = _ref.name, this.id = _ref.id, this.level = _ref.level, this.paragonLevel = _ref.paragonLevel, this["class"] = _ref["class"], this.hardcore = _ref.hardcore;
this.resists = {
Fire: 0,
Lightning: 0,
Poison: 0,
Physical: 0,
Arcane: 0,
Cold: 0
};
}
Hero.prototype.items = function() {
var _this = this;
return this._items || (this._items = (function() {
var i, item, itemKlass, items, maxResistType, maxResistValue, slot, type, val, _i, _len, _ref, _ref1, _ref2;
items = [];
_ref = _this.data.items;
for (slot in _ref) {
if (!__hasProp.call(_ref, slot)) continue;
item = _ref[slot];
itemKlass = Item.klassForSlot(slot);
i = new itemKlass(_this.sheets, slot, item);
if (_this.oneWithEverything()) {
_ref1 = _this.resists;
for (type in _ref1) {
if (!__hasProp.call(_ref1, type)) continue;
_this.resists[type] += i.attributes().resists[type];
}
}
items.push(i);
}
if (_this.oneWithEverything()) {
maxResistValue = 0;
_ref2 = _this.resists;
for (type in _ref2) {
if (!__hasProp.call(_ref2, type)) continue;
val = _ref2[type];
if (val >= maxResistValue) {
maxResistType = type;
maxResistValue = val;
}
}
for (_i = 0, _len = items.length; _i < _len; _i++) {
item = items[_i];
item.attributes().allResist += item.attributes().resists[maxResistType];
}
}
return items;
})());
};
Hero.prototype.oneWithEverything = function() {
return this["class"] === 'monk' && this.skills().has('One With Everything')[0] === 'Y';
};
Hero.prototype.skills = function() {
var _this = this;
return this._skills || (this._skills = (function() {
return new Skills(_this.sheets, _this["class"], _this.data.skills.passive.concat(_this.data.skills.active));
})());
};
Hero.abbrev = function(name) {
return Hero.ABBREV_MAP[name];
};
return Hero;
})();
Skills = (function() {
function Skills(sheets, klass, data) {
var skill, _i, _len, _ref;
this.sheets = sheets;
this.klass = klass;
this.data = data;
this.names = [];
_ref = this.data;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
skill = _ref[_i];
if (skill.skill != null) {
this.names.push(skill.skill.name);
}
if (skill.rune != null) {
this.names.push(skill.rune.name);
}
}
}
Skills.prototype.populateSpreadsheet = function() {
return this.sheets.main.getRange(CellRanges.PLAYER_BUFFS).setValues(this.displayableSkills());
};
Skills.prototype.has = function(name) {
if (__indexOf.call(this.names, name) >= 0) {
return ['Y'];
} else {
return ['N'];
}
};
Skills.prototype.displayableSkills = function() {
var skills, totalCells;
skills = (function() {
switch (this.klass) {
case "wizard":
return [[""], this.has('Energy Armor'), this.has('Pinpoint Barrier'), this.has('Prismatic Armor'), this.has('Familiar'), this.has('Sparkflint'), this.has('Vigoron'), this.has('Magic Weapon'), this.has('Force Weapon'), this.has('Blood Magic'), this.has('Frost Nova'), this.has('Deep Freeze'), this.has('Bone Chill'), this.has('Ice Armor'), this.has('Crystallize'), this.has('Slow Time'), this.has('Time Warp'), this.has('Stretch Time'), this.has('Safe Passage'), [""], this.has('Blur'), this.has('Glass Cannon'), this.has("Galvanizing Ward")];
case 'demon-hunter':
return [[""], this.has('Boar Companion'), this.has('Sentry'), this.has('Aid Station'), this.has('Guardian Turret'), this.has('Bait the Trap'), this.has('Hardened'), this.has('Shadow Power'), this.has('Gloom'), [""], this.has('Steady Aim'), this.has('Archery'), this.has('Sharpshooter'), this.has('Perfectionist'), this.has('Brooding'), this.has('Cull the Weak')];
case 'barbarian':
return [[""], this.has('War Cry'), this.has('Hardened Wrath'), this.has('Invigorate'), this.has("Veteran's Warning"), this.has("Impunity"), this.has("Battle Rage"), this.has("Marauder's Rage"), this.has('Punish'), this.has('Overpower'), this.has('Crushing Advance'), this.has('Killing Spree'), this.has('Best Served Cold'), this.has('Threatening Shout'), this.has('Falter'), [""], this.has("Ruthless"), this.has("Nerves of Steel"), this.has("Weapons Master"), this.has("Superstition"), this.has("Tough as Nails"), this.has('Bloodthirst'), this.has('Inspiring Presence')];
case 'monk':
return [[""], this.has('Deadly Reach'), this.has('Foresight'), this.has('Keen Eye'), this.has('Blazing Wrath'), this.has("Earth Ally"), this.has("Mantra of Evasion"), this.has("Hard Target"), this.has("Transgression"), this.has("Mantra of Healing"), this.has("Heavenly Body"), this.has("Time of Need"), this.has("Sustenance"), this.has("Boon of Inspiration"), this.has("Mantra of Conviction"), this.has("Overawe"), this.has("Intimidation"), this.has("Reclamation"), this.has("Concussion"), this.has("Lightning Flash"), [""], this.has("Resolve"), this.has("Seize the Initiative"), this.has("The Guardian's Path"), this.has("Sixth Sense")];
case 'witch-doctor':
return [[""], this.has("Life Link"), this.has("Provoke the Pack"), this.has("Soul Harvest"), [""], this.has("Jungle Fortitude"), this.has("Pierce the Veil"), this.has("Blood Ritual"), this.has("Zombie Handler")];
default:
throw "Unrecognized class: " + this.klass;
}
}).call(this);
totalCells = CellRanges.verticalSize('PLAYER_BUFFS');
return padArray(skills, totalCells, ['N']);
};
return Skills;
})();
ItemSet = (function() {
ItemSet.recordedSets = {};
function ItemSet(data) {
this.data = data;
this.quantity = 0;
this.ranks = this.data.ranks;
}
ItemSet.prototype.attributeStrings = function() {
var rank;
return this._attrs || (this._attrs = flattenArray((function() {
var _i, _len, _ref, _results;
_ref = this.ranks;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
rank = _ref[_i];
if (parseInt(rank.required) <= this.quantity) {
_results.push(rank.attributes);
}
}
return _results;
}).call(this)));
};
ItemSet.prototype.attributesRaw = function() {
return this._attributesRaw || (this._attributesRaw = Item.parseAttributesRaw(this.attributeStrings()));
};
ItemSet.attributesRaw = function() {
var set, slug;
return this._attributesRaw || (this._attributesRaw = Item.combineRawValues((function() {
var _ref, _results;
_ref = this.recordedSets;
_results = [];
for (slug in _ref) {
if (!__hasProp.call(_ref, slug)) continue;
set = _ref[slug];
if (set.attributeStrings().length > 0) {
_results.push(set.attributesRaw());
}
}
return _results;
}).call(this)));
};
ItemSet.record = function(data) {
var _base, _name;
(_base = this.recordedSets)[_name = data.slug] || (_base[_name] = new ItemSet(data));
this.recordedSets[data.slug].quantity += 1;
return this.recordedSets[data.slug];
};
ItemSet.populateSpreadsheet = function(sheets) {
(new SetBonuses(sheets, 'set', {
attributesRaw: this.attributesRaw()
})).populateSpreadsheet();
return true;
};
return ItemSet;
})();
Item = (function() {
function Item(sheets, slot, basicData) {
var _ref;
this.sheets = sheets;
this.slot = slot;
this.basicData = basicData;
_ref = [this.basicData.name, this.basicData.tooltipParams], this.name = _ref[0], this.itemId = _ref[1];
}
Item.prototype.attributes = function() {
var _this = this;
return this._attributes || (this._attributes = (function() {
var data;
D3Logger.log(_this.sheets.main, "Parsing item: " + _this.slot);
try {
data = API.parse("data/" + _this.itemId);
_this.type = Item.parseType(data.type.id);
if (data.set != null) {
_this.set = ItemSet.record(data.set);
}
return Item.parseAttributes(data);
} catch (error) {
Browser.msgBox("An error occurred when attempting to access item properties for the " + _this.slot + " slot. Error: " + error);
throw "IGNOREME";
}
})());
};
Item.prototype.populateSpreadsheet = function() {
var advancedRange, mainRange;
mainRange = this.sheets.main.getRange(CellRanges[this.slot.toUpperCase()]);
advancedRange = this.sheets.advanced.getRange(CellRanges["" + (this.slot.toUpperCase()) + "_ADVANCED"]);
mainRange.setValues(this.displayableAttributes(this.attributes()));
return advancedRange.setValues(this.displayableAttributes(this.attributes(), {
advanced: true
}));
};
Item.prototype.displayableAttributes = function(attrs, options) {
var _ref;
if (options == null) {
options = {};
}
if ((_ref = options.advanced) == null) {
options.advanced = false;
}
if (options.advanced) {
return [[attrs.loh], [attrs.ls], [attrs.lifeRegen], [attrs.globes], [attrs.blockChance], [attrs.meleeDR], [attrs.rangedDR], [attrs.eliteDR], [attrs.eliteBonus], [attrs.demonBonus]];
} else {
return [[attrs.armor], [attrs.strength], [attrs.dexterity], [attrs.intelligence], [attrs.vitality], [attrs.allResist], [attrs.lifePercent], [attrs.critDamage], [attrs.critPercent], [attrs.ias], [attrs.elementalPercent]];
}
};
Item.parseType = function(type) {
switch (type) {
case "CeremonialDagger":
return "WD Knife";
case "FistWeapon":
return "Fist";
case "MightyWeapon1H":
return "Mighty";
case "MightyWeapon2H":
return "2H Mighty";
case "Axe2H":
return "2H Axe";
case "Mace2H":
return "2H Mace";
case "Sword2H":
return "2H Sword";
case "HandXBow":
return "Hand Crossbow";
case "Orb":
return "W Source";
case "Mojo":
return "WD Mojo";
case "Quiver":
return "DH Quiver";
default:
return type;
}
};
Item.klassForSlot = function(slot) {
switch (slot) {
case "rightFinger":
case "leftFinger":
case "neck":
return Jewelry;
case "mainHand":
return Weapon;
case "offHand":
return Offhand;
default:
return Armor;
}
};
Item.parseAttributesRaw = function(rawData) {
var attributes, match, name, rawStr, val, _i, _len, _ref, _ref1, _ref10, _ref11, _ref12, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
attributes = {};
for (_i = 0, _len = rawData.length; _i < _len; _i++) {
rawStr = rawData[_i];
if ((match = rawStr.match(/^\+(\d+) (Dexterity|Intelligence|Strength|Vitality)$/))) {
name = "" + match[2] + "_Item";
val = ((_ref = attributes[name]) != null ? _ref.min : void 0) || 0;
val += parseInt(match[1]);
} else if ((match = rawStr.match(/^\+(\d+) Resistance to All Elements$/))) {
name = "Resistance_All";
val = ((_ref1 = attributes[name]) != null ? _ref1.min : void 0) || 0;
val += parseInt(match[1]);
} else if ((match = rawStr.match(/^Critical Hit Chance Increased by (\d+\.\d+)%$/))) {
name = "Crit_Percent_Bonus_Capped";
val = ((_ref2 = attributes[name]) != null ? _ref2.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Attack Speed Increased by (\d+)%$/))) {
name = "Attacks_Per_Second_Item_Percent";
val = ((_ref3 = attributes[name]) != null ? _ref3.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^\+(\d+)% Life$/))) {
name = "Hitpoints_Max_Percent_Bonus_Item";
val = ((_ref4 = attributes[name]) != null ? _ref4.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Critical Hit Damage Increased by (\d+)%$/))) {
name = "Crit_Damage_Percent";
val = ((_ref5 = attributes[name]) != null ? _ref5.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Reduces damage from melee attacks by (\d+)%\.?$/))) {
name = "Damage_Percent_Reduction_From_Melee";
val = ((_ref6 = attributes[name]) != null ? _ref6.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Reduces damage from ranged attacks by (\d+)%\.?$/))) {
name = "Damage_Percent_Reduction_From_Ranged";
val += ((_ref7 = attributes[name]) != null ? _ref7.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Increases Damage Against Elites by (\d+)%$/))) {
name = "Damage_Percent_Bonus_Vs_Elites";
val = ((_ref8 = attributes[name]) != null ? _ref8.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^\+(\d+)% Damage to Demons$/))) {
name = "Damage_Percent_Bonus_Vs_Monster_Type#Demon";
val = ((_ref9 = attributes[name]) != null ? _ref9.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Reduces damage from elites by (\d+)%\.?$/))) {
name = "Damage_Percent_Reduction_From_Elites";
val += ((_ref10 = attributes[name]) != null ? _ref10.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^Regenerates (\d+) Life per Second$/))) {
name = "Hitpoints_Regen_Per_Second";
val += ((_ref11 = attributes[name]) != null ? _ref11.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
} else if ((match = rawStr.match(/^(\d+\.\d+)% of Damage Dealt Is Converted to Life$/))) {
name = "Steal_Health_Percent";
val += ((_ref12 = attributes[name]) != null ? _ref12.min : void 0) || 0;
val += parseFloat(match[1]) / 100;
}
attributes[name] = {
min: val,
max: val
};
}
return attributes;
};
Item.parseAttributes = function(data) {
var allResist, aps, armor, attributes, blockChance, blockMax, blockMin, critDamage, critPercent, deltaBaseDmg, deltaBonusDmg, deltaPhysBonusDmg, demonBonus, dexterity, dmgPercent, elementalPercent, eliteBonus, eliteDR, gem, globes, ias, intelligence, lifePercent, lifeRegen, loh, ls, maxJewelryDmg, maxTooltipDmg, meleeDR, minBaseDmg, minBonusDmg, minJewelryDmg, minPhysBonusDmg, minTooltipDmg, name, rangedDR, resists, strength, values, vitality, _i, _len, _ref;
armor = 0;
strength = 0;
dexterity = 0;
intelligence = 0;
vitality = 0;
allResist = 0;
resists = {
Fire: 0,
Lightning: 0,
Poison: 0,
Physical: 0,
Arcane: 0,
Cold: 0
};
lifePercent = 0;
critDamage = 0;
critPercent = 0;
ias = 0;
elementalPercent = 0;
minBaseDmg = 0;
minBonusDmg = 0;
minPhysBonusDmg = 0;
minJewelryDmg = 0;
maxJewelryDmg = 0;
deltaBonusDmg = 0;
deltaBaseDmg = 0;
deltaPhysBonusDmg = 0;
dmgPercent = 0;
aps = 0;
loh = 0;
ls = 0;
lifeRegen = 0;
blockChance = 0;
meleeDR = 0;
rangedDR = 0;
eliteDR = 0;
eliteBonus = 0;
demonBonus = 0;
blockMin = 0;
blockMax = 0;
globes = 0;
_ref = [data.attributesRaw].concat(data.gems ? (function() {
var _j, _len, _ref, _results;
_ref = data.gems;
_results = [];
for (_j = 0, _len = _ref.length; _j < _len; _j++) {
gem = _ref[_j];
_results.push(gem.attributesRaw);
}
return _results;
})() : []);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
attributes = _ref[_i];
for (name in attributes) {
if (!__hasProp.call(attributes, name)) continue;
values = attributes[name];
switch (name) {
case "Armor_Item":
armor += parseFloat(values.min);
break;
case "Armor_Bonus_Item":
armor += parseFloat(values.min);
break;
case "Strength_Item":
strength += parseFloat(values.min);
break;
case "Dexterity_Item":
dexterity += parseFloat(values.min);
break;
case "Intelligence_Item":
intelligence += parseFloat(values.min);
break;
case "Vitality_Item":
vitality += parseFloat(values.min);
break;
case "Resistance_All":
allResist += parseFloat(values.min);
break;
case "Resistance#Fire":
case "Resistance#Lightning":
case "Resistance#Poison":
case "Resistance#Physical":
case "Resistance#Arcane":
case "Resistance#Cold":
resists[name.split('#')[1]] += parseFloat(values.min);
break;
case "Hitpoints_Max_Percent_Bonus_Item":
lifePercent += parseFloat(values.min) * 100;
break;
case "Crit_Damage_Percent":
critDamage += parseFloat(values.min) * 100;
break;
case "Crit_Percent_Bonus_Capped":
critPercent += parseFloat(values.min) * 100;
break;
case "Attacks_Per_Second_Percent":
case "Attacks_Per_Second_Item_Percent":
ias += parseFloat(values.min) * 100;
break;
case "Damage_Type_Percent_Bonus#Fire":
case "Damage_Type_Percent_Bonus#Lightning":
case "Damage_Type_Percent_Bonus#Poison":
case "Damage_Type_Percent_Bonus#Arcane":
case "Damage_Type_Percent_Bonus#Cold":
elementalPercent += parseFloat(values.min) * 100;
break;
case "Damage_Weapon_Min#Physical":
case "Damage_Min#Physical":
minBaseDmg += parseFloat(values.min);
minJewelryDmg += parseFloat(values.min);
maxJewelryDmg += parseFloat(values.min);
break;
case "Damage_Bonus_Min#Physical":
minBaseDmg += parseFloat(values.min);
minJewelryDmg += parseFloat(values.min);
break;
case "Damage_Weapon_Bonus_Min#Physical":
minPhysBonusDmg += parseFloat(values.min);
break;
case "Damage_Weapon_Delta#Physical":
case "Damage_Delta#Physical":
deltaBaseDmg += parseFloat(values.min);
maxJewelryDmg += parseFloat(values.min);
break;
case "Damage_Weapon_Bonus_Delta#Physical":
deltaPhysBonusDmg += parseFloat(values.min);
break;
case "Damage_Weapon_Percent_Bonus#Physical":
dmgPercent += parseFloat(values.min);
break;
case "Attacks_Per_Second_Item_Bonus":
aps += parseFloat(values.min);
break;
case "Hitpoints_On_Hit":
loh += parseFloat(values.min);
break;
case "Damage_Percent_Bonus_Vs_Elites":
eliteBonus += parseFloat(values.min) * 100;
break;
case "Damage_Percent_Bonus_Vs_Monster_Type#Demon":
demonBonus += parseFloat(values.min) * 100;
break;
case "Steal_Health_Percent":
ls += parseFloat(values.min) * 100;
break;
case "Hitpoints_Regen_Per_Second":
lifeRegen += parseFloat(values.min);
break;
case "Block_Chance_Item":
case "Block_Chance_Bonus_Item" * 100:
blockChance += parseFloat(values.min);
break;
case "Damage_Percent_Reduction_From_Melee" * 100:
meleeDR += parseFloat(values.min);
break;
case "Damage_Percent_Reduction_From_Elites" * 100:
eliteDR += parseFloat(values.min);
break;
case "Damage_Percent_Reduction_From_Ranged" * 100:
rangedDR += parseFloat(values.min);
break;
case "Block_Amount_Item_Min":
blockMin += parseFloat(values.min);
break;
case "Block_Amount_Item_Delta":
blockMin += parseFloat(values.min);
blockMax += parseFloat(values.min);
break;
case "Health_Globe_Bonus_Health":
globes += parseFloat(values.min);
break;
default:
if (name.match(/^Damage_Weapon_Min#/) || name.match(/^Damage_Weapon_Bonus_Min#/)) {
minBonusDmg += parseFloat(values.min);
} else if (name.match(/^Damage_Weapon_Delta#/ || name.match(/^Damage_Weapon_Bonus_Delta#/))) {
deltaBonusDmg += parseFloat(values.min);
}
}
}
}
minTooltipDmg = (minPhysBonusDmg + minBaseDmg) * (1 + dmgPercent) + minBonusDmg;
maxTooltipDmg = minBaseDmg === 0 && minPhysBonusDmg === 0 ? 0 : (Math.max(minPhysBonusDmg + minBaseDmg, minBaseDmg + deltaBaseDmg - 1) + 1 + deltaPhysBonusDmg) * (1 + dmgPercent) + minBonusDmg + deltaBonusDmg;
return {
armor: armor,
strength: strength,
dexterity: dexterity,
intelligence: intelligence,
vitality: vitality,
allResist: allResist,
resists: resists,
lifePercent: lifePercent,
critDamage: critDamage,
critPercent: critPercent,
ias: ias,
elementalPercent: elementalPercent,
minPhysBonusDmg: minPhysBonusDmg,
deltaPhysBonusDmg: deltaPhysBonusDmg,
minBaseDmg: minBaseDmg,
deltaBaseDmg: deltaBaseDmg,
minBonusDmg: minBonusDmg,
minJewelryDmg: minJewelryDmg,
maxJewelryDmg: maxJewelryDmg,
deltaBonusDmg: deltaBonusDmg,
dmgPercent: dmgPercent,
aps: aps,
minTooltipDmg: minTooltipDmg,
maxTooltipDmg: maxTooltipDmg,
loh: loh,
eliteBonus: eliteBonus,
demonBonus: demonBonus,
ls: ls,
lifeRegen: lifeRegen,
blockChance: blockChance,
meleeDR: meleeDR,
eliteDR: eliteDR,
rangedDR: rangedDR,
blockMin: blockMin,
blockMax: blockMax,
globes: globes
};
};
Item.combineRawValues = function(arr) {
var key, obj, result, val, _i, _len;
result = {};
for (_i = 0, _len = arr.length; _i < _len; _i++) {
obj = arr[_i];
for (key in obj) {
if (!__hasProp.call(obj, key)) continue;
val = obj[key];
result[key] || (result[key] = {
min: 0,
max: 0
});
result[key].min += val.min;
result[key].max += val.max;
}
}
return result;
};
return Item;
})();
Armor = (function(_super) {
__extends(Armor, _super);
function Armor() {
return Armor.__super__.constructor.apply(this, arguments);
}
return Armor;
})(Item);
Jewelry = (function(_super) {
__extends(Jewelry, _super);
function Jewelry() {
return Jewelry.__super__.constructor.apply(this, arguments);
}
Jewelry.prototype.displayableAttributes = function(attrs, opts) {
if (opts == null) {
opts = {
advanced: false
};
}
if (opts.advanced) {
return Jewelry.__super__.displayableAttributes.apply(this, arguments);
} else {
return [[attrs.minJewelryDmg], [attrs.maxJewelryDmg]].concat(Jewelry.__super__.displayableAttributes.apply(this, arguments));
}
};
return Jewelry;
})(Item);
Weapon = (function(_super) {
__extends(Weapon, _super);
function Weapon() {
return Weapon.__super__.constructor.apply(this, arguments);
}
Weapon.prototype.displayableAttributes = function(attrs, opts) {
if (opts == null) {
opts = {
advanced: false
};
}
if (opts.advanced) {
return Weapon.__super__.displayableAttributes.apply(this, arguments);
} else {
return [[this.type], [attrs.minTooltipDmg], [attrs.maxTooltipDmg], [attrs.minBonusDmg], [attrs.minBonusDmg + attrs.deltaBonusDmg]].concat(Weapon.__super__.displayableAttributes.apply(this, arguments)).concat([[attrs.aps]]);
}
};
return Weapon;
})(Item);
Offhand = (function(_super) {
__extends(Offhand, _super);
function Offhand() {
return Offhand.__super__.constructor.apply(this, arguments);
}
Offhand.prototype.displayableAttributes = function(attrs, opts) {
if (opts == null) {
opts = {
advanced: false
};
}
if (opts.advanced) {
return Offhand.__super__.displayableAttributes.apply(this, arguments).concat([[attrs.blockMin], [attrs.blockMax]]);
} else {
return Offhand.__super__.displayableAttributes.apply(this, arguments);
}
};
return Offhand;
})(Weapon);
SetBonuses = (function(_super) {
__extends(SetBonuses, _super);
function SetBonuses(sheets, slot, basicData) {
this.sheets = sheets;
this.slot = slot;
this.basicData = basicData;
}
SetBonuses.prototype.attributes = function() {
var _this = this;
return this._attributes || (this._attributes = (function() {
return Item.parseAttributes(_this.basicData);
})());
};
return SetBonuses;
})(Item);
@nzifnab
Copy link
Author

nzifnab commented Sep 26, 2012

The set bonus stats don't come in an easily-parseable format like gems and affixes do :( Might have to hardcode all the set bonuses in to get those to work. ugh. Maybe it's acceptable not to, not too hard to just add your own set bonuses up.

@nzifnab
Copy link
Author

nzifnab commented Nov 13, 2012

Set bonuses work now but it parses the tooltip string to get the values: aka, localization is still an issue so only english-locale set bonuses are working at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment