Base DPS visualizer initial commit (kinda useless

This commit is contained in:
b 2021-06-20 06:49:08 -07:00
parent d9aab5c1dd
commit e2a2ae02db
4 changed files with 347 additions and 0 deletions

1
dps_data_compress.json Normal file

File diff suppressed because one or more lines are too long

33
dps_vis.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html scroll-behavior="smooth" style="height: 100%">
<head>
<!-- nunito font, copying wynndata -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="article.css">
<link rel="icon" href="./media/memes/agony.png">
<link rel="manifest" href="manifest.json">
<title>Base DPS Visualizer</title>
</head>
<body class="all" style="height: 95%; overflow: auto; display: flex; flex-direction: column">
<header class="header nomarginp">
<div class="headerleft" id = "headerleft">
</div>
<div class="headercenter" id = "headercenter">
<div>
<p class="itemp" id="header">Base DPS Visualization</p>
</div>
</div>
<div class="headerright" id = "headerright">
</div>
</header>
<script src="https://d3js.org/d3.v7.js"></script>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="loadheader.js"></script>
<script type="text/javascript" src="icons.js"></script>
<script type="text/javascript" src="dps_vis.js"></script>
</body>
</html>

124
dps_vis.js Normal file
View file

@ -0,0 +1,124 @@
d3.select("body")
.append("div")
.attr("style", "width: 100%; min-height: 0px; flex-grow: 1")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true);
let graph = d3.select("svg");
console.log(graph);
let margin = {top: 20, right: 20, bottom: 35, left: 40};
function bbox() {
return graph.node().parentNode.getBoundingClientRect();
}
function xAxis(g, x) {
let _bbox = bbox();
g.attr("transform", `translate(0,${_bbox.height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(_bbox.width / 80, ","))
.call(g => g.select(".domain").remove())
.call(g => g.select("text")
.attr("x", _bbox.width)
.attr("y", margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text("Combat Level"));
}
function yAxis(g, y) {
g.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.select("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Base DPS"));
}
function grid(g1, g2, x, y) {
let _bbox = bbox();
g1.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.selectAll("line")
.data(x.ticks())
.join("line")
.attr("x1", d => 0.5 + x(d))
.attr("x2", d => 0.5 + x(d))
.attr("y1", margin.top)
.attr("y2", _bbox.height - margin.bottom)
.exit(g => g.remove());
g2.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.selectAll("line")
.data(y.ticks())
.join("line")
.attr("y1", d => 0.5 + y(d))
.attr("y2", d => 0.5 + y(d))
.attr("x1", margin.left)
.attr("x2", _bbox.width - margin.right)
.exit(g => g.remove());
}
let _xAxis = graph.append("g");
_xAxis.append("text");
let _yAxis = graph.append("g");
_yAxis.append("text");
let _grid1 = graph.append("g");
let _grid2 = graph.append("g");
let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1];
(async function() {
let dps_data = await (await fetch(baseUrl + "/dps_data_compress.json")).json()
console.log(dps_data)
let colorMap = new Map(
[
["Normal", "#fff"],
["Unique", "#ff5"],
["Rare","#f5f"],
["Legendary","#5ff"],
["Fabled","#f55"],
["Mythic","#a0a"],
["Crafted","#0aa"],
["Custom","#0aa"],
["Set","#5f5"]
]
);
const item_points = graph.append("g")
.attr("stroke", "black")
.selectAll("circle")
.data(dps_data.wand, d => d[2])
.join("circle")
.attr("fill", d => colorMap.get(d[3]))
.attr("r", d => 5)
.call(circle => circle.append("title")
.text(d => [d[0], d[2]].join("\n")));
function redraw(data) {
let max_dps_base = 0;
for (let x of data) {
if (x[4] > max_dps_base) {
max_dps_base = x[4];
}
}
let x = d3.scaleLinear([70, 105], [margin.left, bbox().width - margin.right]);
let y = d3.scaleLinear([0, max_dps_base * 1.1], [bbox().height - margin.bottom, margin.top]);
let _bbox = bbox();
graph.attr("viewBox", [0, 0, _bbox.width, _bbox.height]);
xAxis(_xAxis, x);
yAxis(_yAxis, y);
grid(_grid1, _grid2, x, y);
item_points.data(data, d => d[2])
.attr("cx", d => x(d[1]))
.attr("cy", d => y(d[4]));
}
d3.select(window)
.on("resize", function() {
redraw(dps_data.wand);
});
redraw(dps_data.wand);
}) ();

189
py_script/plot_dps.py Normal file
View file

@ -0,0 +1,189 @@
import matplotlib.pyplot as plt
import json
import numpy as np
#baselines_known_x = np.array([ 70, 72, 74, 75, 76, 78, 80, 82, 84, 85, 86, 89, 90, 91, 93, 95, 97, 98, 100 ])
#baselines_known_y = np.array([ 341.53, 358.29, 374.68, 383.35, 394.06, 410.75, 432.14, 453.45, 469.94, 480.93, 491.21, 524.26, 536.69, 546.26, 569.78, 592.45, 615.65, 626.75, 648.21 ]) / 2.05
item_type = "bow"
items_file = "../clean.json"
items_new_file = "../clean3.json"
min_level = 70
baselines_known_x = np.array([ 70, 75, 80, 85, 90, 95, 100 ])
baselines_known_y = np.array([ 341.53, 383.35, 432.14, 480.93, 536.69, 592.45, 648.21 ]) / 2.05
baselines_known_y_new = np.zeros(len(baselines_known_x))
for i, (x, y) in enumerate(zip(baselines_known_x, baselines_known_y)):
baselines_known_y_new[i] = y * (1 - 0.01 * (x - 70))
def interpolate_baseline(level):
i = 0
while baselines_known_x[i] <= level:
if baselines_known_x[i] == level:
return baselines_known_y[i]
i += 1
if i == len(baselines_known_x):
return baselines_known_y[-1]
start = i - 1
slope = ((baselines_known_y[i] - baselines_known_y[start])
/ (baselines_known_x[i] - baselines_known_x[start]))
dx = level - baselines_known_x[start]
return baselines_known_y[start] + slope * dx
reqs = ["dexReq", "strReq", "defReq", "agiReq", "intReq"]
# tefaw
powders_new = [ [4.5, 6.5, 8.5, 9, 10.5, 12.5, "T"],
[4.5, 6.5, 8, 8.5, 10, 12, "E"],
[3.5, 6, 7, 7.5, 9, 11, "F"],
[4, 6.5, 7.5, 8, 9.5, 11, "A"],
[3.5, 5, 6.5, 7, 8.5, 10, "W"]]
powders_old = [ [4.5, 6.5, 8.5, 9, 10.5, 22.5, "T"], # LOL only updating the 6th col
[4.5, 6.5, 8, 8.5, 10, 20, "E"],
[3.5, 6, 7, 7.5, 9, 17, "F"],
[4, 6.5, 7.5, 8, 9.5, 17, "A"],
[3.5, 5, 6.5, 7, 8.5, 15, "W"]]
def get_appropriate_powder_idx(item):
for i, req in enumerate(reqs):
if req in item and item[req] > 0:
return i
return 0
def get_display_name(item):
if "displayName" in item:
return item["displayName"]
return item["name"]
item_data = json.load(open(items_file))["items"]
item_map = {get_display_name(item): item for item in item_data}
item_new_data = json.load(open(items_new_file))["items"]
item_new_map = {get_display_name(item): item for item in item_new_data}
attack_speed_mods = {"SUPER_SLOW": 0.51, "VERY_SLOW": 0.83, "SLOW": 1.5, "NORMAL": 2.05, "FAST": 2.5, "VERY_FAST": 3.1, "SUPER_FAST": 4.3}
attack_speed_target_mult = {"SUPER_SLOW": 4, "VERY_SLOW": 2.5, "SLOW": 1.4, "NORMAL": 1, "FAST": 0.8, "VERY_FAST": 0.66, "SUPER_FAST": 0.48}
dps_to_baseline = dict()
min_mult = 10
max_mult = 0
for k in attack_speed_mods:
mult = attack_speed_mods[k] * attack_speed_target_mult[k]
if mult < min_mult:
min_mult = mult
if mult > max_mult:
max_mult = mult
dps_to_baseline[k] = 1/mult
weapon_type_mods = {"wand": 0.6, "spear": 0.8, "dagger": 1.0, "bow": 1.2, "relik": 1.2}
min_mult
max_mult
tiers_mod = {"Normal": 0.8, "Unique": 1.0, "Rare": 1.1, "Legendary": 1.3, "Fabled": 1.5, "Mythic": 1.7, "Set": 1.05}
tiers_colors = {"Normal": (0.9, 0.9, 0.9), "Unique": (1, 1, 1/3), "Rare": (1, 1/3, 1), "Legendary": (1/3, 1, 1), "Fabled": (1, 1/3, 1/3), "Mythic": (2/3, 0, 2/3), "Set": (1/3, 1, 1/3)}
damage_types = ["nDam", "eDam", "tDam", "wDam", "fDam", "aDam"]
# tefaw
damage_baseline_modifiers = [0.05, 0.05, -0.05, 0, -0.05]
def guess_design_modifier(item, base_dps):
level = item["lvl"]
tier = item["tier"]
nominal_baseline = interpolate_baseline(level) * weapon_type_mods[item["type"]] * tiers_mod[tier]
explanation = []
num_reqs = 0
total_modifier = 0
for i, req in enumerate(reqs):
if req in item and item[req] > 0:
num_reqs += 1
total_modifier += damage_baseline_modifiers[i]
explanation.append((req, damage_baseline_modifiers[i]))
num_damage_types = 0
for damage_type in damage_types[1:]:
if damage_type in item:
damages = item[damage_type].split("-")
if int(damages[1]) != 0:
num_damage_types += 1
is_rainbow = num_damage_types == 5 or num_reqs == 5
if is_rainbow:
total_modifier = 0.15
explanation = [("rainbow", 0.15)]
elif num_reqs > 1 or num_damage_types > 1:
total_modifier += 0.05
explanation.append(("multi_element", 0.05))
#total_modifier = 0.05
#explanation = [("multi_element", 0.05)]
nslots = 0
if "slots" in item:
nslots = item["slots"]
if nslots == 0:
total_modifier += 0.1
explanation.append(("zero_slot", 0.1))
elif nslots == 1:
total_modifier += 0.05
explanation.append(("one_slot", 0.05))
item_baseline = dps_to_baseline[item["atkSpd"]] * base_dps
actual_modifier = (item_baseline - nominal_baseline) / nominal_baseline
explained_baseline = nominal_baseline * (1 + total_modifier)
delta = (item_baseline - explained_baseline) / nominal_baseline
if delta >= 0.04 and delta <= 0.05:
total_modifier += 0.05
explanation.append(("offensive", 0.05))
elif delta >= 0.08:
total_modifier += 0.1
explanation.append(("hyper_offensive", 0.1))
elif delta <= -0.04 and delta >= -0.05:
total_modifier -= 0.05
explanation.append(("defensive", -0.05))
elif delta <= -0.08:
total_modifier -= 0.1
explanation.append(("hyper_defensive", -0.1))
if abs(delta) > 0.2:
print("LARGE BASELINE ERROR FOR ITEM " + get_display_name(item))
return total_modifier, actual_modifier, explanation
def get_data(item, powders):
total_damage = 0
for damage_type in damage_types:
if damage_type in item:
damages = item[damage_type].split("-")
total_damage += int(damages[0]) + int(damages[1])
total_damage /= 2
attack_speed_mod = attack_speed_mods[item["atkSpd"]]
dps = total_damage * attack_speed_mod
postpowder_damage = total_damage
if "slots" in item:
powder = powders[get_appropriate_powder_idx(item)][5] # Assume tier6 always.
postpowder_damage += powder * item["slots"]
return (dps, postpowder_damage * attack_speed_mod)
item_dat = {cat: [] for cat in weapon_type_mods}
for name, item in item_map.items():
if "lvl" not in item:
continue
if item["lvl"] >= min_level and item["category"] == "weapon":
dps, postpowder_dps = get_data(item, powders_old)
new_item = item_new_map[name]
new_dps, new_postpowder_dps = get_data(new_item, powders_new)
total, actual, explain = guess_design_modifier(item, dps)
item_dat[item["type"]].append((get_display_name(item), item["lvl"], item["id"], item["tier"],
dps, postpowder_dps,
new_dps, new_postpowder_dps,
total, actual, explain))
item_dat["baseline_xs"] = baselines_known_x.tolist()
item_dat["baseline_ys"] = baselines_known_y.tolist()
item_dat["baseline_ys_new"] = baselines_known_y_new.tolist()
json.dump(item_dat, open("dps_data.json", "w"), indent=2)
json.dump(item_dat, open("dps_data_compress.json", "w"))
# item_lvl, item_dps, item_tiercolor = zip(*item_dat)
# plt.scatter(item_lvl, item_dps, color=item_tiercolor)
#
# for tier, mod in tiers_mod.items():
# plt.plot(baselines_known_x, baselines_known_y * max_mult * mod, '--', color=tiers_colors[tier])
# plt.plot(baselines_known_x, baselines_known_y * min_mult * mod, '--', color=tiers_colors[tier])
# plt.xticks([70, 75, 80, 85, 90, 95, 100])
# plt.xlabel("Combat level")
# plt.ylabel("Base dps")
# plt.title(f"{item_type} base dps vs level")
# plt.grid()
# plt.show()