def group_lines_for_spell(spell_id, lines, spell_powers): total_heals = [] total_overheals = [] total_underheals = [] count_heals = [] nn_underheals = [] nn_overheals = [] nn_full_overheals = [] coefficient = sd.spell_coefficient(spell_id) base_heal = sd.spell_heal(spell_id) for i, sp in enumerate(spell_powers): data = process_lines_for_spell(lines, -sp * coefficient, base_heal) n_h, n_oh, n_f_oh, n_oh_nc, total_h, total_oh, total_h_nc, total_oh_nc = data total_heals.append(total_h / n_h) total_overheals.append(total_oh / n_h) total_underheals.append((total_h - total_oh) / n_h) count_heals.append(n_h) nn_underheals.append((n_h - n_oh) / n_h) nn_overheals.append(n_oh / n_h) nn_full_overheals.append(n_f_oh / n_h) return (total_heals, total_overheals, total_underheals, count_heals, nn_underheals, nn_overheals, nn_full_overheals)
def process_spell(spell_id, spell_lines, spell_power): base_heal = sd.spell_heal(spell_id) coeff = sd.spell_coefficient(spell_id) if base_heal == 0 or coeff == 0: return extra_heal = coeff * spell_power base_heal_fraction = base_heal / (base_heal + extra_heal) n_heal = 0 n_underheal = 0 n_overheal = 0 n_downrank = 0 n_drop_h = 0 for h, oh, crit in spell_lines: h_fraction = 1.0 - oh / h n_heal += 1 if h_fraction >= 1: n_underheal += 1 elif h_fraction <= 0: n_overheal += 1 elif h_fraction < base_heal_fraction: n_downrank += 1 else: n_drop_h += 1 return n_heal, n_underheal, n_overheal, n_downrank, n_drop_h
def process_spell(player_name, spell_id, spell_lines, spell_power=None, show=True, path=None): spell_name = sd.spell_name(spell_id) relative_underheal = [] is_crit = [] for h, oh, crit in spell_lines: relative_underheal.append(1.0 - oh / h) is_crit.append(1 if crit else 0) cast_fraction = np.linspace(0, 1, len(relative_underheal)) relative_underheal = np.array(relative_underheal) relative_underheal = np.sort(relative_underheal) # is_crit = np.array(is_crit) plt.figure(constrained_layout=True) plt.fill_between(cast_fraction, relative_underheal, label="Underheal") plt.fill_between(cast_fraction, 1, relative_underheal, label="Overheal") # add base heal line if spell-power is included base_heal = sd.spell_heal(spell_id) coeff = sd.spell_coefficient(spell_id) if spell_power and base_heal > 0 and coeff > 0: extra_heal = coeff * spell_power base_heal_fraction = base_heal / (base_heal + extra_heal) plt.axhline(base_heal_fraction, linestyle="--", color="k", label="Base heal") plt.title(f"{spell_name}, {len(spell_lines)} casts") plt.xlabel("Fraction of casts") plt.ylabel("Fraction of heal (orange overheal, blue underheal)") plt.xlim((0, 1)) plt.ylim((0, 1)) plt.legend() if path is None: path = "figs/cdf" plt.savefig(f"{path}/{player_name}_cdf_{spell_id}.png") # plt.figure(constrained_layout=True) # plt.fill_between(relative_underheal, 1 - cast_fraction, label="Underheal") # plt.fill_between(relative_underheal, 1, 1 - cast_fraction, label="Overheal") # plt.title(spell_name) # plt.ylabel("Fraction of casts") # plt.xlabel("Fraction of overheals (orange overheal, blue underheal)") # plt.xlim((0, 1)) # plt.ylim((0, 1)) # plt.legend() if show: plt.show()
def aggregate_lines(grouped_lines, spell_power=0.0): """Aggregates and evaluates grouped lines""" # heals, any OH, half OH, full OH, aH, aOH total_data = np.zeros(6) data_list = [] for spell_id, spell_data in grouped_lines.items(): # heals, any OH, half OH, full OH, aH, aOH data = np.zeros(6) data[0] = len(spell_data) # Fail more gracefully if we are missing a coefficient coefficient = sd.spell_coefficient(spell_id) for h, oh, crit in spell_data: dh = coefficient * spell_power if crit: # scale spell power differential by 1.5 if spell was a crit dh *= 1.5 h = h - dh oh = oh - dh if h < 0.0: # could happen for heals on healing reduced players, we just ignore these for now continue if oh < 0.0: oh = 0.0 # if crit: # print(f"* h: {h:.1f} oh: {oh:.1f} *") # else: # print(f" h: {h:.1f} oh: {oh:.1f}") if oh == h: data[3] += 1 data[2] += 1 data[1] += 1 elif oh >= 0.5 * h: data[2] += 1 data[1] += 1 elif oh > 0.0: data[1] += 1 data[4] += h data[5] += oh data_list.append((spell_id, data)) total_data += data return total_data, data_list
def pick_spell(self, deficit, mana, h, **_): # pick spell by deficit choices = { "10917": "Flash Heal (Rank 7)", # "10916": "Flash Heal (Rank 6)", # "10915": "Flash Heal (Rank 5)", "9474": "Flash Heal (Rank 4)", # "9473": "Flash Heal (Rank 3)", # "9472": "Flash Heal (Rank 2)", # "2061": "Flash Heal (Rank 1)", # "2053": "Lesser Heal (Rank 3)", # "10965": "Greater Heal (Rank 4)", # "10964": "Greater Heal (Rank 3)", # "10963": "Greater Heal (Rank 2)", # "2060": "Greater Heal (Rank 1)", # "6064": "Heal (Rank 4)", # "6063": "Heal (Rank 3)", # "2055": "Heal (Rank 2)", # "2054": "Heal (Rank 1)", }.keys() # filter choices by mana available choices = filter( lambda sid: sd.spell_mana(sid, talents=self.talents) < mana, choices) # convert to healing heals = map( lambda sid: (sid, sd.spell_heal(sid) + sd.spell_coefficient(sid) * h), choices) # pick max heal with small amount of overhealing heals = filter(lambda x: 0.80 * x[1] < -deficit, heals) try: spell_id, heal = max(heals, key=lambda x: x[1]) except ValueError: return 0, 0, 0 mana = sd.spell_mana(spell_id) cast_time = 1.5 if "Flash" in sd.spell_name(spell_id) else 2.5 return heal, mana, cast_time
def process_spell(spell_id, spell_lines, heal_increase=0.0): spell_name = sd.spell_name(spell_id) n_heals = 0 n_crits = 0 raw_heals = [] for h, _, crit in spell_lines: n_heals += 1 if crit: n_crits += 1 h /= 1.5 raw_heals.append(h) raw_heals = np.array(raw_heals) raw_heals = filter_out_reduced_healing(raw_heals) sample_size = len(raw_heals) median_heal = np.median(raw_heals) crit_rate = n_crits / n_heals # Include heal increase from Improved Renew or Spiritual Healing spell_heal = sd.spell_heal(spell_id) spell_heal *= 1.0 + heal_increase if spell_heal == 0: # Unknown or 0 base heal, cannot estimate +heal extra_heal = 0.0 else: extra_heal = median_heal - spell_heal coefficient = sd.spell_coefficient(spell_id) if coefficient == 0: # Unknown or 0 coefficient, cannot estimate +heal est_plus_heal = 0 else: est_plus_heal = extra_heal / coefficient print( f" {spell_id:>5s} {spell_name:>26s} {sample_size:3d} {spell_heal:+7.1f} {median_heal:+7.1f}" f" {extra_heal:+6.1f} {est_plus_heal:+7.1f} {crit_rate:5.1%}") return est_plus_heal, n_heals, n_crits
def pick_spell(self, deficit, mana, h, **_): # pick spell by deficit spell_id = self.spell_id base_heal = sd.spell_heal(spell_id) coef = sd.spell_coefficient(spell_id) spell_mana = sd.spell_mana(spell_id, talents=self.talents) if spell_mana > mana: # print(f"{spell_mana} > {mana}") # could not cast return 0, 0, 0 heal = base_heal + h * coef # include random variation and crit maybe? cast_time = 1.5 if "Flash" in sd.spell_name(spell_id) else 2.5 return heal, spell_mana, cast_time
def spell_overheal_probability(player_name, spell_id, lines, spell_power=None): """Plots overheal probability of each spell""" if spell_power is None: sp_neg = 400.0 sp_shift = 0.0 sp_extrap = 200.0 else: sp_neg = spell_power sp_shift = spell_power sp_extrap = 1000.0 - spell_power if sp_extrap < 0: sp_extrap = 1500.0 - spell_power spell_powers = np.linspace(0, -sp_neg, int(sp_neg / 1) + 1) n_heals = [] n_overheals = [] n_overheals_nc = [] # Fail more gracefully if we are missing a coefficient coefficient = sd.spell_coefficient(spell_id) if coefficient == 0: return for sp in spell_powers: n_h = 0 n_oh = 0 n_oh_nc = 0 for h, oh, crit in lines: dh = coefficient * -sp dh_c = dh oh_nc = oh if crit: # scale spell power differential by 1.5 if spell was a crit dh_c *= 1.5 # Scale oh down oh_nc = oh - (h - h / 1.5) # remove spell power contribution h -= dh_c oh -= dh_c oh_nc -= dh if h < 0.0: # could happen for heals on healing reduced players, we just ignore these for now continue n_h += 1 # n_h_nc += not_crit if oh > 0.0: n_oh += 1 if oh_nc > 0.0: n_oh_nc += 1 n_heals.append(n_h) n_overheals.append(n_oh) n_overheals_nc.append(n_oh_nc) # plot probabilities plot_oh_prob( player_name, spell_id, spell_powers, sp_extrap, sp_shift, n_heals, n_overheals, n_overheals_nc, )
def print_results(data): if len(data) == 0: print("No data found.") return data = sorted(data, key=lambda d: sd.spell_name(d[0])) print(f"Crits:") nn_crits = 0 nn_spells = 0 s_crit_fh = 0 s_crit_uh = 0 s_coef = 0 t_hh = 0 t_oh = 0 for spell_id, n_crits, n_spells, crit_fh, crit_uh, hh, ohh in data: spell_name = sd.spell_name(spell_id) coef = sd.spell_coefficient(spell_id) nn_crits += n_crits nn_spells += n_spells s_crit_fh += crit_fh * n_crits s_crit_uh += crit_uh * n_crits s_coef += coef * n_crits t_hh += hh t_oh += ohh crit_pc = n_crits / n_spells message = ( f" {spell_name:<30s}: {n_crits:3d} / {n_spells:3d} crits ({crit_pc:5.1%}); ({ohh / hh:5.1%} OH)" ) if n_crits == 0: print(message) continue crit_oh = crit_fh - crit_uh oh_pc = crit_oh / crit_fh crit_heal = 0.01 * crit_uh eq_h_0c = crit_heal / coef eq_h = eq_h_0c / (1.0 + 0.5 * crit_pc) message += f", Crit H: {crit_fh:4.0f} ({crit_uh:4.0f} + {crit_oh:4.0f} oh) ({oh_pc:5.1%} oh)" message += f", 1% crit gives {0.01 * crit_uh:+4.1f} healing eq to {eq_h:+5.1f} h ({eq_h_0c:+5.1f} at 0% crit)." print(message) print("") crit_pc = nn_crits / nn_spells spell_name = "Overall / Average" coef = s_coef / nn_crits message = ( f" {spell_name:<30s}: {nn_crits:3d} / {nn_spells:3d} crits ({crit_pc:5.1%}); ({t_oh / t_hh:5.1%} OH)" ) if nn_crits == 0: print(message) return crit_fh = s_crit_fh / nn_crits crit_uh = s_crit_uh / nn_crits crit_oh = crit_fh - crit_uh oh_pc = crit_oh / crit_fh crit_heal = 0.01 * crit_uh eq_h_0c = crit_heal / coef eq_h = eq_h_0c / (1.0 + 0.5 * crit_pc) message += f", Crit H: {crit_fh:4.0f} ({crit_uh:4.0f} + {crit_oh:4.0f} oh) ({oh_pc:5.1%} oh)" message += f", 1% crit gives {0.01 * crit_uh:+4.1f} healing eq to {eq_h:+5.1f} h ({eq_h_0c:+5.1f} at 0% crit)." print(message)
def group_spells(spell_casts, spell_heals, spell_periodics, spell_absorbs, reduce_crits=False, neg_sp=0.0): spell_dict = {} for _, spell_id in spell_casts: if spell_id not in spell_dict: spell_dict[spell_id] = dict(casts=0, heals=0, net_heal=0, gross_heal=0, overheal=0, crits=0, gross_crit=0, net_crit=0, can_crit=0) spell_dict[spell_id]["casts"] += 1 for spell_id, th, oh, is_crit, can_crit in spell_heals + spell_periodics + spell_absorbs: if spell_id == "27805": # holy nova pairs with different cast id spell_id = "27801" if spell_id not in spell_dict: # heal but no cast, maybe pre-casted? spell_dict[spell_id] = dict(casts=0, heals=0, net_heal=0, gross_heal=0, overheal=0, crits=0, gross_crit=0, net_crit=0, can_crit=0) spell = spell_dict[spell_id] spell["heals"] += 1 dhp = neg_sp * sd.spell_coefficient(spell_id) if can_crit: spell["can_crit"] = 1 if is_crit: spell["crits"] += 1 if reduce_crits: crit_heal = th / 3 th -= crit_heal oh = max(0.0, oh - crit_heal) else: dhp *= 1.5 th = max(0.0, th - dhp) oh = max(0.0, oh - dhp) if is_crit: g_crit = th / 3 n_crit = max(0.0, g_crit - oh) else: g_crit = th * 0.5 n_crit = 0 spell["gross_heal"] += th spell["overheal"] += oh spell["net_heal"] += th - oh spell["gross_crit"] += g_crit spell["net_crit"] += n_crit return spell_dict
def sum_spells(spells, talents=None): total_data = dict( casts=0, gross_heal=0, overheal=0, net_heal=0, mana=0, coef=0, crit_coef=0, gross_crit=0, net_crit=0, crits=0, can_crit=0, ) spell_data = dict() for spell_id, spell in spells.items(): casts = spell["casts"] net_heal = spell["net_heal"] net_crit = spell["net_crit"] gross_heal = spell["gross_heal"] gross_crit = spell["gross_crit"] overheal = spell["overheal"] crits = spell["crits"] can_crit = spell["can_crit"] if gross_heal == 0: mana = sd.spell_mana(spell_id, talents, warn_on_not_found=False) if mana == 0: continue else: mana = sd.spell_mana(spell_id, talents) coef = sd.spell_coefficient(spell_id, warn_on_not_found=True) if "Renew" in sd.spell_name(spell_id): coef *= 5 average_net_heal = net_heal / casts if casts > 0 else net_heal crit_heal = 0.5 * can_crit * gross_heal / casts if casts > 0 else 0.5 * can_crit * gross_heal spell_data[spell_id] = dict(spell, mana=mana, net_heal=average_net_heal, coef=coef, crit_heal=crit_heal) gross_coef = casts * coef if crits == 0: net_crit = 0 else: # extrapolate crit value for non-crit casts net_crit *= casts / crits total_data["casts"] += casts total_data["net_heal"] += net_heal total_data["net_crit"] += net_crit total_data["gross_heal"] += gross_heal total_data["gross_crit"] += gross_crit total_data["overheal"] += overheal total_data["mana"] += casts * mana total_data["coef"] += gross_coef total_data["crit_coef"] += gross_coef * 0.5 * can_crit total_data["crits"] += crits total_data["can_crit"] += casts * can_crit return total_data, spell_data