Base DPS visualizer initial commit (kinda useless
This commit is contained in:
parent
d9aab5c1dd
commit
e2a2ae02db
4 changed files with 347 additions and 0 deletions
1
dps_data_compress.json
Normal file
1
dps_data_compress.json
Normal file
File diff suppressed because one or more lines are too long
33
dps_vis.html
Normal file
33
dps_vis.html
Normal 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
124
dps_vis.js
Normal 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
189
py_script/plot_dps.py
Normal 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()
|
Loading…
Reference in a new issue