Created
June 5, 2013 10:34
-
-
Save AlexanderFabisch/5713016 to your computer and use it in GitHub Desktop.
Compare PVP scores of characters in Diablo 3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rubygems' | |
require 'diablo3-api-client' | |
# TODO | |
# fury / barb attributes | |
# block | |
# edp only for dh | |
# efp for barbs? | |
# monks | |
# wizards | |
# witch doctors | |
class Set | |
def initialize set | |
@name = set["name"] | |
@bonuses = {} | |
set["ranks"].each do |rank| | |
@bonuses[rank["required"]] = rank["attributes"] | |
end | |
@items = {} | |
set["items"].each do |item| | |
@items[item["name"]] = false | |
end | |
end | |
def add_item name | |
@items[name] = true | |
end | |
def attributes | |
attr = [] | |
items = @items.values.count true | |
@bonuses.each do |n, a| | |
attr += a if n <= items | |
end | |
attr | |
end | |
end | |
class Weapon | |
attr_accessor :wtype, :dps, :min, :max, :aps, :ed, :elemin, :elemax | |
def initialize wtype, dps, min, max, aps, ed, elemin, elemax | |
@wtype = wtype | |
@dps = dps || 0.0 | |
@min = min || 0.0 | |
@max = max || 0.0 | |
@aps = aps || 0.0 | |
@ed = ed || 0.0 | |
@elemin = elemin || 0.0 | |
@elemax = elemax || 0.0 | |
end | |
end | |
def parse_items hero | |
mainhand = nil | |
offhand = nil | |
sets = {} | |
items = [] | |
for item in hero.items | |
attr = {} | |
weapon = true | |
is_offhand = item.class.scope == "offHand" | |
unless item.set.nil? | |
sets[item.set["name"]] = Set.new item.set if sets[item.set["name"]].nil? | |
sets[item.set["name"]].add_item item.name | |
end | |
wtype = "" | |
case item.type["id"] | |
when /(Bow|Crossbow|HandXbow|Sword|MightyWeapon1H|Mace|Axe|Axe2H|Dagger|Sword|Polearm|Spear|FistWeapon|Wand)/ | |
wtype = item.type["id"] | |
when /(Shield|Helm|ChestArmor|Boots|Gloves|Shoulders|Legs|Bracers|GenericBelt|Ring|Amulet|Belt|Belt_Barbarian|Cloak|Quiver|SpiritStone_Monk|Orb|WizardHat|VoodooMask|Mojo)/ | |
weapon = false | |
else | |
puts "WARNING: unknown wtype " + item.type["id"] | |
end | |
item.attributes_raw.each do |name, attribute| | |
parse_raw_attribute name, attribute, attr, weapon, is_offhand | |
end | |
item.attributes.each do |attribute| | |
parse_attribute attribute, attr, weapon, is_offhand | |
end | |
unless attr["delta"].nil? | |
init_attr attr, "max" | |
attr["max"] += attr["delta"] | |
attr["delta"] = nil | |
end | |
#puts item.name | |
#puts item.type["id"] | |
#puts item.attributes_raw # Damage_Bonus_Min#Physical, Damage_Min#Physical, Damage_Delta#Physical | |
if weapon | |
baps = if attr["baps"].nil? then 0.0 else attr["baps"] end | |
unless is_offhand | |
mainhand = Weapon.new wtype, item.dps["min"], item.min_damage["min"], item.max_damage["min"], item.attacks_per_second["min"]-baps, attr["ed"], attr["elemin"], attr["elemax"] | |
else | |
offhand = Weapon.new wtype, item.dps["min"], item.min_damage["min"], item.max_damage["min"], item.attacks_per_second["min"]-baps, attr["ed"], attr["elemin"], attr["elemax"] | |
end | |
end | |
for gem in item.gems | |
gem["attributesRaw"].each do |name, attribute| | |
parse_raw_attribute name, attribute, attr, weapon, is_offhand | |
end | |
gem["attributes"].each do |attribute| | |
parse_attribute attribute, attr | |
end | |
end | |
items << attr | |
end | |
for name, set in sets | |
set.attributes.each do |attribute| | |
parse_attribute attribute, attr, false, false, true | |
end | |
end | |
[items, mainhand, offhand] | |
end | |
def parse_raw_attribute name, attribute, attr, weapon=false, offhand=false | |
case name | |
when "Movement_Scalar" | |
init_attr attr, "ms" | |
attr["ms"] += attribute["min"].to_f * 100.0 | |
when /(Dexterity|Intelligence|Strength)_Item/ | |
stat = $1.downcase[0...3] | |
init_attr attr, stat | |
attr[stat] += attribute["min"].to_f | |
when "Vitality_Item" | |
init_attr attr, "vita" | |
attr["vita"] += attribute["min"].to_f | |
when "Hitpoints_Max_Percent_Bonus_Item" | |
init_attr attr, "life" | |
attr["life"] += attribute["min"].to_f | |
when "Resistance_All" | |
init_attr attr, "allres" | |
attr["allres"] += attribute["min"].to_f | |
when /Resistance#(Physical|Poison|Fire|Cold|Arcane|Lightning)/ | |
res = $1.downcase[0...2] | |
init_attr attr, res | |
attr[res + "res"] = attribute["min"].to_f | |
when "Resource_Max_Bonus#Discipline" | |
init_attr attr, "disc" | |
attr["disc"] += attribute["min"].to_f | |
when "Damage_Bonus_Min#Physical" | |
init_attr attr, "min" | |
attr["min"] += attribute["min"].to_f | |
when "Damage_Min#Physical" | |
init_attr attr, "min2" | |
attr["min2"] += attribute["min"].to_f | |
when "Damage_Delta#Physical" | |
init_attr attr, "delta" | |
attr["delta"] += attribute["min"].to_f | |
when "Attacks_Per_Second_Item_Percent" | |
init_attr attr, "wias" | |
attr["wias"] += attribute["min"].to_f | |
when "Attacks_Per_Second_Percent" | |
init_attr attr, "ias" | |
attr["ias"] += attribute["min"].to_f | |
when "Damage_Weapon_Percent_Bonus#Physical" | |
init_attr attr, "ed" | |
attr["ed"] += attribute["min"].to_f | |
when "Crit_Damage_Percent" | |
init_attr attr, "cd" | |
attr["cd"] += attribute["min"].to_f | |
when "Crit_Percent_Bonus_Capped" | |
init_attr attr, "cc" | |
attr["cc"] += attribute["min"].to_f | |
when "Armor_Item", "Armor_Bonus_Item" | |
init_attr attr, "armor" | |
attr["armor"] += attribute["min"].to_f | |
when "Gold_PickUp_Radius" | |
init_attr attr, "pickup" | |
attr["pickup"] += attribute["min"].to_f | |
when "Experience_Bonus_Percent" | |
init_attr attr, "exp" | |
attr["exp"] += attribute["min"].to_f | |
when "Damage_Percent_Reduction_From_Melee" | |
init_attr attr, "meeleedr" | |
attr["meeleedr"] += attribute["min"].to_f | |
when "Power_Damage_Percent_Bonus#DemonHunter_HungeringArrow" | |
init_attr attr, "hungering_arrow" | |
attr["hungering_arrow"] += attribute["min"].to_f | |
when "Damage_Percent_Bonus_Vs_Elites" | |
init_attr attr, "dmg_vs_elites" | |
attr["dmg_vs_elites"] += attribute["min"].to_f | |
when "Sockets" | |
when "Item_Indestructible" | |
when "Durability_Cur" | |
when "Durability_Max" | |
when "Requirement_When_Equipped#BowAny" | |
else | |
#puts "#{name} => #{attribute}" | |
end | |
if not attr["min2"].nil? and not attr["delta"].nil? | |
init_attr attr, "min" | |
init_attr attr, "max" | |
attr["min"] += attr["min2"] | |
attr["max"] += attr["min2"] + attr["delta"] | |
attr["min2"] = nil | |
attr["delta"] = nil | |
end | |
end | |
def parse_attribute attribute, attr, weapon=false, offhand=false, set=false | |
case attribute | |
when /\+(\d+)% Movement Speed/ | |
init_attr attr, "ms" | |
attr["ms"] += $1.to_f if set | |
when /\+(\d+) Dexterity/ | |
init_attr attr, "dex" | |
attr["dex"] += $1.to_f if set | |
when /\+(\d+) Strength/ | |
init_attr attr, "str" | |
attr["str"] += $1.to_f if set | |
when /\+(\d+) Intelligence/ | |
init_attr attr, "int" | |
attr["int"] += $1.to_f if set | |
when /\+(\d+) Vitality/ | |
init_attr attr, "vita" | |
attr["vita"] += $1.to_f if set | |
when /\+(\d+)% Life/ | |
init_attr attr, "life" | |
attr["life"] += $1.to_f/100.0 if set | |
when /\+(\d+) Armor/ | |
when /\+(\d+) Resistance to All Elements/ | |
init_attr attr, "allres" | |
attr["allres"] += $1.to_f if set | |
when /\+(\d+) (Physical|Poison|Fire|Cold|Arcane|Lightning) Resistance/ | |
attr[$2.downcase[0...2] + "res"] = $1 if set | |
when /\+(\d+)% Chance to Block/ | |
attr["block"] = $1.to_f/100.0 | |
when /Attack Speed Increased by (\d+)%/ | |
attr["ias"] = $1.to_f/100.0 if set | |
when /\+(\d+).+?(\d+) (Fire|Holy|Poison|Lightning|Arcane|Cold) Damage/ | |
attr["elemin"] = $1.to_f | |
attr["elemax"] = $2.to_f | |
when /\+(\d+)% Damage/ | |
attr["ed"] = $1.to_f/100.0 if set | |
when /Critical Hit Damage Increased by (\d+)%/ | |
init_attr attr, "cd" | |
attr["cd"] += $1.to_f/100.0 if set | |
when /Critical Hit Chance Increased by (\d+\.*\d*)%/ | |
init_attr attr, "cc" | |
attr["cc"] += $1.to_f/100.0 if set | |
when /Adds (\d+)% to (Fire|Holy|Poison|Lightning|Arcane|Cold) Damage/ | |
attr["elemental_ed"] = $1.to_f/100.0 | |
when /\+(\d+) Maximum Discipline.*/ | |
attr["disc"] = $1.to_f if set | |
when /Increases Discipline Regeneration by (\d+) per Second.*/ | |
attr["discreg"] = $1.to_f | |
when /Regenerates (\d+) Life per Second/ | |
attr["lifereg"] = $1.to_f | |
when /Each Hit Adds \+(\d+) Life/ | |
init_attr attr, "loh" | |
attr["loh"] += $1.to_f | |
when /(\d+\.\d*)% of Damage Dealt Is Converted to Life/ | |
attr["ll"] = $1.to_f/100.0 | |
when /Increases Gold and Health Pickup by (\d+) Yards\./ | |
attr["pickup"] = $1 if set | |
when /Reduces damage from ranged attacks by (\d+)%\./ | |
attr["rangedr"] = $1.to_f/100.0 | |
when /Health Globes and Potions Grant \+(\d+) Life\./ | |
attr["potions"] = $1.to_f | |
when /Increases Hatred Regeneration by (\d+\.*\d*) per Second.*/ | |
attr["hatredreg"] = $1.to_f | |
when /Reduces duration of control impairing effects by (\d+)%/ | |
when /\+(\d+.+?\d+) Attacks per Second/ | |
attr["baps"] = $1.to_f | |
when /Increases Damage Against Elites by (\d+)%/ | |
when /Reduces damage from elites by (\d+)%\./ | |
when /Reduces damage from Cold attacks by (\d+)%\./ | |
when /Increases (Bash) Damage by (\d+)%.*/ | |
when /Increases (Spirit Barrage) Damage by (\d+)%.*/ | |
when /Level Requirement Reduced by (\d+)/ | |
when /\+(\d+)% Extra Gold from Monsters/ | |
when /(\d+)% Better Chance of Finding Magical Items/ | |
when /Increases Bonus Experience by (\d+)%/ | |
else | |
end | |
end | |
def init_attr attr, key | |
attr[key] = 0.0 if attr[key].nil? | |
end | |
def merge_items items | |
attributes = {} | |
items.each do |item| | |
item.each do |a, v| | |
if attributes[a].nil? | |
attributes[a] = v | |
elsif [Fixnum, Float].include? attributes[a].class | |
attributes[a] += v | |
end | |
end | |
end | |
attributes | |
end | |
def effective_life stats, items, char, buffs, mlvl=63, verbose=false | |
result = {} | |
life = stats["life"] | |
result["life"] = life | |
dr_char = if ["barbarian", "monk"].include? char and buffs.include? "character" then 0.3 else 0.0 end | |
result["dr_char"] = dr_char | |
res = [items["phres"], items["fires"], items["cores"], items["lires"], items["pores"], items["arres"]] | |
avg_res = if char == "monk" and buffs.include? "one-with-everything" | |
res.delete_if do |r| r.nil? end.max | |
else | |
res.inject(0.0) do |sum, el| sum + el.to_f end.to_f / 6.0 | |
end | |
allres = if items["allres"].nil? then 0.0 else items["allres"] end | |
result["allres"] = allres | |
avg_res += allres + stats["intelligence"]/10.0 | |
dodge_chance = get_dodge_chance(stats["dexterity"]) | |
result["dodge_chance"] = dodge_chance | |
armor = items["armor"].to_f + stats["strength"].to_f | |
case char | |
when "wizard" | |
if buffs.include? "glas-cannon" | |
avg_res *= 0.9 | |
armor *= 0.9 | |
end | |
end | |
result["avg_res"] = avg_res | |
dr_res = avg_res/(avg_res+mlvl*5) | |
result["dr_res"] = dr_res | |
result["armor"] = armor | |
dr_armor = armor/(armor+mlvl*50) | |
result["dr_armor"] = dr_armor | |
dr_range = if items["rangedr"].nil? then 0.0 else items["rangedr"] end | |
dr_meelee = if items["meeleedr"].nil? then 0.0 else items["meeleedr"] end | |
dr_rm = (dr_range + dr_meelee) / 2.0 | |
result["dr_rm"] = dr_rm | |
total_dr = 1.0 - (1.0-dodge_chance)*(1.0-dr_armor)*(1.0-dr_res)*(1.0-dr_rm)*(1.0-dr_char) | |
result["total_dr"] = total_dr | |
block_chance = stats["blockChance"] | |
block_amount_min = stats["blockAmountMin"] | |
block_amount_max = stats["blockAmountMax"] | |
# TODO block | |
ehp = life / (1.0-total_dr) | |
result["ehp"] = ehp | |
pretty_print result if verbose | |
result | |
end | |
def get_dodge_chance dex | |
return (([([dex-1000, 7000]).min,0]).max*0.01 + | |
([([dex-500, 500]).min, 0]).max*0.02 + | |
([([dex-100, 400]).min, 0]).max*0.025 + | |
([dex, 100]).min*0.1) / 100.0 | |
end | |
def effective_disc stats, items, verbose=false | |
result = {} | |
disc = 30.0 + if items["disc"].nil? then 0.0 else items["disc"] end | |
result["disc"] = disc | |
discreg = 1.0 + if items["discreg"].nil? then 0.0 else items["discreg"] end | |
result["discreg"] = discreg | |
erp = disc / (14-2*discreg) * 14 | |
result["erp"] = erp | |
pretty_print result if verbose | |
result | |
end | |
def effective_fury stats, items, verbose=false | |
result = {} | |
fury = 100.0 + if items["fury"].nil? then 0.0 else items["fury"] end | |
result["fury"] = fury | |
furyreg = -3.0 + if items["furyreg"].nil? then 0.0 else items["furyreg"] end | |
result["furyreg"] = furyreg | |
erp = fury / (14-2*furyreg) * 14 | |
result["erp"] = erp | |
pretty_print result if verbose | |
result | |
end | |
def unbuffed_dps stats, items, mainhand, offhand, char, buffs, verbose=false | |
result = {} | |
weapons = [mainhand, offhand].delete_if do |w| w.nil? end | |
dual_wield = weapons.size == 2 | |
damage = [] | |
aps = [] | |
case char | |
when "barbarian" | |
mainstat = stats["strength"].to_f | |
when "demon-hunter", "monk" | |
mainstat = stats["dexterity"].to_f | |
when "wizard", "witch-doctor" | |
mainstat = stats["intelligence"].to_f | |
else | |
puts "unknown character: " + char | |
end | |
result["mainstat"] = mainstat | |
cc = 0.05 + if items["cc"].nil? then 0.0 else items["cc"] end | |
cd = 0.5 + if items["cd"].nil? then 0.0 else items["cd"] end | |
ias = if items["ias"].nil? then 0.0 else items["ias"] end | |
ed = 0.0 | |
pvped = if items["dmg_vs_elites"].nil? then 0.0 else items["dmg_vs_elites"] end | |
additional_dmg = if items["min"].nil? then 0.0 else items["min"] end | |
additional_dmg += if items["max"].nil? then 0.0 else items["max"] end | |
additional_dmg /= 2.0 | |
result["additional_dmg"] = additional_dmg | |
weapons.each do |weapon| | |
weapon_dmg = (weapon.min + weapon.max)/2.0 | |
result["weapon_dmg"] = weapon_dmg | |
base_dmg = weapon_dmg + additional_dmg | |
result["base_dmg"] = base_dmg | |
elemental_dmg = (weapon.elemin+weapon.elemax)/2.0 | |
calculated_wdps = (weapon_dmg + elemental_dmg)*weapon.aps | |
if calculated_wdps != weapon.dps then puts "WARNING: calculated_wdps #{calculated_wdps} != weapon.dps #{weapon.dps}" end | |
unless items["elemental_ed"].nil? | |
elemental_dmg += base_dmg * items["elemental_ed"] | |
end | |
result["elemental_dmg"] = elemental_dmg | |
damage << (base_dmg + elemental_dmg) | |
aps << weapon.aps | |
end | |
if buffs.include? "focused-mind" | |
ias += 0.03 | |
end | |
case char | |
when "barbarian" | |
if buffs.include? "battle-rage" | |
if buffs.include? "maraudar's-rage" | |
ed += 0.3 | |
else | |
ed += 0.15 | |
end | |
cc += 0.03 | |
end | |
if buffs.include? "wrath-of-the-berserker" | |
cc += 0.1 | |
ias += 0.25 | |
end | |
if buffs.include? "ruthless" | |
cc += 0.05 | |
cd += 0.5 | |
end | |
if buffs.include? "weapon-master" | |
case mainhand.wtype | |
when "MightyWeapon1H" | |
# TODO 3 fury each hit?! | |
when "Mace", "Axe", "Axe2H" | |
cc += 0.1 | |
when "Sword", "Dagger" | |
ed += 0.15 | |
when "Polearm", "Spear" | |
ias += 0.1 | |
else | |
puts "WARNING: unknown wtype " + mainhand.wtype | |
end | |
end | |
when "demon-hunter" | |
if buffs.include? "archery" | |
case mainhand.wtype | |
when "Bow" | |
ed += 0.15 | |
when "HandXbow" | |
cc += 0.1 | |
when "Crossbow" | |
cd += 0.5 | |
else | |
puts "WARNING: unknown wtype " + mainhand.wtype | |
end | |
end | |
when "wizard" | |
if buffs.include? "glas-cannon" | |
ed += 0.15 | |
end | |
end | |
result["cc"] = cc | |
result["cd"] = cd | |
result["ias"] = ias | |
result["ed"] = ed | |
result["pvped"] = pvped | |
baps = if items["baps"].nil? then 0.0 else items["baps"] end | |
aps.map! do |a| baps+a end | |
damage.map! do |dmg| dmg * (1.0 + ed) * (1.0 + pvped) end | |
result["damage"] = damage | |
result["aps"] = aps | |
aps_total = if dual_wield then 2 * aps[0] * aps[1] * (1.15+ias) / (aps[0] + aps[1]) else aps[0] * (1.0+ias) end | |
result["aps_total"] = aps_total | |
unbuffed_dps = (1.0+mainstat/100.0) * (1.0+cc*cd) * aps_total * if dual_wield then (damage[0] + damage[1])/2 else damage[0] end | |
result["dps"] = unbuffed_dps | |
pretty_print result if verbose | |
result | |
end | |
def pretty_print scores, w=20 | |
puts "".ljust(50, ".") | |
scores.each do |k, v| | |
res = if [Float, Fixnum].include? v.class | |
sprintf("%.3f", v) | |
elsif v.class == Array | |
v.map do |i| sprintf("%.3f", i) end.join ", " | |
else | |
v.to_s + v.class.to_s | |
end | |
puts "\t" + (k+":").ljust(w, ".") + res.rjust(w, ".") | |
end | |
end | |
verbose = false | |
# char specific passive skill buffs | |
buffs = ["character", | |
# barbarian | |
"weapon-master", | |
"ruthless", | |
#"battle-rage", | |
#"maraudar's-rage", | |
#"wrath-of-the-berserker", | |
# demon hunter | |
"archery", | |
# monk | |
"one-with-everything", | |
# wizard | |
"glas-cannon", | |
# followers | |
#"focused-mind", | |
] | |
players = [ | |
#{ "name" => "Darai", "tag" => "Rorschach", "number" => 2926, "exclude" => nil }, | |
#{ "name" => "Yan", "tag" => "Scorpion", "number" => 2883, "chars" => "demon-hunter", "exclude" => nil }, | |
#{ "name" => "Slovenka", "tag" => "Bitsurugi", "number" => 2597, "exclude" => nil }, | |
#{ "name" => "Alizee", "tag" => "Sawyer", "number" => 2666, "exclude" => nil }, | |
#{ "name" => "Karl", "tag" => "Karl", "number" => 2673, "exclude" => nil }, | |
#{ "name" => "Clemenza", "tag" => "Kamhul", "number" => 2452, "exclude" => nil }, | |
#{ "name" => "Poisonivy", "tag" => "Antili", "number" => 2847, "exclude" => nil }, | |
#{ "name" => "Paradogz", "tag" => "Paradogz", "number" => 2133, "exclude" => nil }, | |
#{ "name" => "Anakin", "tag" => "Anakin", "number" => 2978, "exclude" => nil }, | |
#{ "name" => "Hillarius", "tag" => "Hillarius", "number" => 1372, "exclude" => nil }, | |
# Barbarians | |
#{ "name" => "Pulsedraft", "tag" => "Pulsedraft", "number" => 2965, "chars" => ["barbarian"], "exclude" => ["Pooliemuli", "Cuzlure", "BlasiHasi", "TheHoff"] }, | |
#{ "name" => "Braveheart", "tag" => "Braveheart", "number" => 2324, "chars" => ["barbarian"], "exclude" => ["Astus", "Cuirass", "Auktionshaus", "Bullseye"] }, | |
#{ "name" => "Quickness", "tag" => "Quickness", "number" => 1919, "chars" => ["barbarian"], "exclude" => nil }, | |
#{ "name" => "Johni", "tag" => "JOHNi", "number" => 2140, "chars" => ["barbarian"], "exclude" => ["RINGEDiamant", "R\u00FCstungen"] }, | |
# Demon Hunters | |
{ "name" => "Freaky", "tag" => "Alexander", "number" => 2557, "chars" => ["demon-hunter"], "exclude" => ["Alexander"] }, | |
{ "name" => "Onkelfeix", "tag" => "Onkelfeix", "number" => 1125, "chars" => ["demon-hunter"], "exclude" => ["Tradeheini", "Ingeborg"] }, | |
{ "name" => "Xerxes", "tag" => "Xerxes", "number" => 2516, "chars" => ["demon-hunter"], "exclude" => ["Kenny", "Horatio", "hurine"] }, | |
# Monks | |
#{ "name" => "Raky", "tag" => "raky", "number" => 2522, "chars" => ["monk"], "exclude" => nil }, | |
#{ "name" => "Psi", "tag" => "Psi", "number" => 2724, "chars" => ["monk"], "exclude" => nil }, | |
#{ "name" => "Lokk", "tag" => "lokk", "number" => 2851, "chars" => ["monk"], "exclude" => nil }, | |
# Witch Doctors | |
#{ "name" => "Sinured", "tag" => "Sinured", "number" => 2182, "chars" => ["witch-doctor"], "exclude" => ["Azula"] }, | |
#{ "name" => "Apocalypto", "tag" => "Apocalypto", "number" => 2474, "chars" => ["witch-doctor"], "exclude" => nil }, | |
#{ "name" => "Anocsa", "tag" => "Anocsa", "number" => 2133, "chars" => ["witch-doctor"], "exclude" => nil }, | |
# Wizards | |
#{ "name" => "Hamster", "tag" => "pawhi", "number" => 2897, "chars" => ["wizard"], "exclude" => nil }, | |
#{ "name" => "Xenter", "tag" => "Xenter", "number" => 2860, "chars" => ["wizard"], "exclude" => ["Xentercore"] }, | |
#{ "name" => "SirBacke", "tag" => "Alexander", "number" => 21697, "chars" => ["wizard"], "exclude" => nil }, | |
#{ "name" => "Koenig", "tag" => "Koenig", "number" => 2201, "chars" => ["wizard"], "exclude" => ["WonderWoman"] }, | |
#{ "name" => "Silencer", "tag" => "Silencer", "number" => 2339, "chars" => ["wizard"], "exclude" => ["Seran"] }, | |
] | |
players.each do |player| | |
profile = Diablo3::Api::Client::Profile.new(player["tag"], player["number"]) | |
puts player["name"] | |
profile.heroes.each do |hero| | |
if player["chars"].include? hero.class_name | |
if not player["exclude"].nil? and player["exclude"].include? hero.name | |
puts "Excluded" if verbose | |
next | |
end | |
items, mainhand, offhand = parse_items hero | |
attributes = merge_items items | |
if mainhand.nil? | |
puts "Skipping mule" if verbose | |
next | |
end | |
dual_wield = !(mainhand.nil? or offhand.nil?) | |
puts hero.stats if verbose | |
puts "Char: #{hero.name} (#{hero.class_name})" | |
ehp = effective_life hero.stats, attributes, char=hero.class_name, buffs, mlvl=63, verbose=verbose | |
dps = unbuffed_dps hero.stats, attributes, mainhand, offhand, hero.class_name, buffs, verbose=verbose | |
aps = dps["aps_total"] | |
ms = if attributes["ms"].nil? then 0.0 else attributes["ms"] end | |
ms = [ms.to_f, 25.0].min | |
erp = 0.0 | |
pvp = 0.0 | |
case hero.class_name | |
when "barbarian" # TODO parse fury + regen | |
erp = effective_fury hero.stats, attributes, verbose=verbose | |
pvp = (ehp["ehp"]/141421.4) * (dps["dps"]/141421.4) * (1.0+erp["erp"])**0.1 * (1.0+aps)**0.1 * (1.0+ms)**0.1 | |
when "demon-hunter" | |
erp = effective_disc hero.stats, attributes, verbose=verbose | |
pvp = (ehp["ehp"]/141421.4) * (dps["dps"]/141421.4) * (1.0+erp["erp"])**0.1 * (1.0+aps)**0.1 * (1.0+ms)**0.1 | |
else | |
puts "WARNING: unknown character" | |
next | |
end | |
result = {"EHP" => ehp["ehp"], "ERP" => erp["erp"], "DPS" => dps["dps"], "APS" => aps, "MS" => ms, "PVP" => pvp} | |
pretty_print result, w=10 if pvp > 1.0 | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://github.com/murphyslaw/diablo3-api-client is required for this script.
Explanation (German)
Legende:
EHP: Effective Health Points. Was das ist sollte ja mittlerweile jedem klar sein.
EDP: Effective Discipline Points. Hier wird die Disziplinregeneration reingerechnet. Als Grundlage wird dabei der Skill Smoke Screen (SS) genommen. Dieser hat einen Cooldown von 2 Sekunden, kostet 14 Disziplin und es wird angenommen, dass er immer neu gecastet wird. Mit den EDP kann man direkt ausrechnen wie oft man hintereinander SS casten kann. Bei 84 EDP kann man z. B. 84/14 = 6 mal SS casten. Die Formel dafür ist:
EDP = 14_Disc / (14-2_Discreg)
DPS: Damage Per Second.
APS: Attacks Per Second.
MS: Movement Speed.
Score: Ein Punktwert, der die Eignung eines Charakter für PvP abschätzt. Je mehr, desto besser. Im Moment wird er durch folgende Formel berechnet:
Score = (EHP_DPS_(1+APS)^0.1_(1+EDP)^0.1_(1+MS)^0.1)/20.000.000.000
Das ganze hat folgenden Hintergrund: angenommen Spieler 1 hat EHP_1 Effective Health Points und DPS_1 Damage Per Second und Spieler 2 EHP_2 und DPS_2. Dann killt Spieler 1 Spieler 2 theoretisch in EHP_2/DPS_1 Sekunden und umgekehrt. Damit Spieler 1 Spieler 2 schlägt, muss Spieler 1 länger leben, d. h.
EHP_1/DPS_2 > EHP_2/DPS_1
Das lässt sich umstellen zu
EHP_1 * DPS_1 > EHP_2 * DPS_2
Man kann alle Charaktere direkt mit dem Wert EHP*DPS für PvP ranken. So wird das bei diabloprogress.com auch gemacht. Das setzt aber voraus, dass sich die beiden Spieler nur gegenüber stehen und stumpfsinnig aufeinander einballern.
Tatsächlich gibt es allerdings noch weitere Faktoren, die zwar nicht direkt den Killspeed, sehr wohl aber die Überlebensfähigkeit beeinflussen. Das sind der MS, der das Ausweichen und Abhauen vereinfacht, die APS, die die Beweglichkeit ebenfalls erhöhen und die EDP, die einen öfter gegen Schaden immun macht, je höher sie ist. Da utopisch hohe Werte allerdings nicht so viel besser als mittelgute Werte sind, sollte der Faktor mit dem sie den PvP-Score beeinflussen allerdings nicht so stark sein, wie z. B. bei den DPS. Stattdessen nimmt der Einfluss mit steigendem Wert immer weiter ab. Deshalb ist hier der Exponent 0.1.