344 lines
14 KiB
JavaScript
344 lines
14 KiB
JavaScript
/**
|
|
* File implementing core damage calculation logic.
|
|
*/
|
|
|
|
const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["warscream", 0.00], ["ragnarokkr", 0.30], ["fortitude", 0.60] ]);
|
|
|
|
function get_base_dps(item) {
|
|
const attack_speed_mult = baseDamageMultiplier[attackSpeeds.indexOf(item.get("atkSpd"))];
|
|
//SUPER JANK @HPP PLS FIX
|
|
if (item.get("tier") !== "Crafted") {
|
|
let total_damage = 0;
|
|
for (const damage_k of damage_keys) {
|
|
damages = item.get(damage_k);
|
|
total_damage += damages[0] + damages[1];
|
|
}
|
|
return total_damage * attack_speed_mult / 2;
|
|
}
|
|
else {
|
|
let total_damage_min = 0;
|
|
let total_damage_max = 0;
|
|
for (const damage_k of damage_keys) {
|
|
damages = item.get(damage_k);
|
|
total_damage_min += damages[0][0] + damages[0][1];
|
|
total_damage_max += damages[1][0] + damages[1][1];
|
|
}
|
|
total_damage_min = attack_speed_mult * total_damage_min / 2;
|
|
total_damage_max = attack_speed_mult * total_damage_max / 2;
|
|
return [total_damage_min, total_damage_max];
|
|
}
|
|
}
|
|
|
|
|
|
function calculateSpellDamage(stats, weapon, _conversions, use_spell_damage, ignore_speed=false, part_filter=undefined) {
|
|
// TODO: Roll all the loops together maybe
|
|
|
|
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
|
|
// 1. Get weapon damage (with powders).
|
|
let weapon_damages;
|
|
if (weapon.get('tier') === 'Crafted') {
|
|
weapon_damages = damage_keys.map(x => weapon.get(x)[1]);
|
|
}
|
|
else {
|
|
weapon_damages = damage_keys.map(x => weapon.get(x));
|
|
}
|
|
let present = deepcopy(weapon.get(damage_present_key));
|
|
|
|
// Also theres prop and rainbow!!
|
|
const damage_elements = ['n'].concat(skp_elements); // netwfa
|
|
|
|
// 2. Conversions.
|
|
// 2.0: First, modify conversions.
|
|
let conversions = deepcopy(_conversions);
|
|
if (part_filter !== undefined) {
|
|
const conv_postfix = ':'+part_filter;
|
|
for (let i in damage_elements) {
|
|
const stat_name = damage_elements[i]+'ConvBase'+conv_postfix;
|
|
if (stats.has(stat_name)) {
|
|
conversions[i] += stats.get(stat_name);
|
|
}
|
|
}
|
|
}
|
|
for (let i in damage_elements) {
|
|
const stat_name = damage_elements[i]+'ConvBase';
|
|
if (stats.has(stat_name)) {
|
|
conversions[i] += stats.get(stat_name);
|
|
}
|
|
}
|
|
|
|
// 2.1. First, apply neutral conversion (scale weapon damage). Keep track of total weapon damage here.
|
|
let damages = [];
|
|
const neutral_convert = conversions[0] / 100;
|
|
let weapon_min = 0;
|
|
let weapon_max = 0;
|
|
for (const damage of weapon_damages) {
|
|
let min_dmg = damage[0] * neutral_convert;
|
|
let max_dmg = damage[1] * neutral_convert;
|
|
damages.push([min_dmg, max_dmg]);
|
|
weapon_min += damage[0];
|
|
weapon_max += damage[1];
|
|
}
|
|
|
|
// 2.2. Next, apply elemental conversions using damage computed in step 1.1.
|
|
// Also, track which elements are present. (Add onto those present in the weapon itself.)
|
|
let total_convert = 0;
|
|
for (let i = 1; i <= 5; ++i) {
|
|
if (conversions[i] > 0) {
|
|
const conv_frac = conversions[i]/100;
|
|
damages[i][0] += conv_frac * weapon_min;
|
|
damages[i][1] += conv_frac * weapon_max;
|
|
present[i] = true;
|
|
total_convert += conv_frac
|
|
}
|
|
}
|
|
total_convert += conversions[0]/100;
|
|
|
|
if (!ignore_speed) {
|
|
// 3. Apply attack speed multiplier. Ignored for melee single hit
|
|
const attack_speed_mult = baseDamageMultiplier[attackSpeeds.indexOf(weapon.get("atkSpd"))];
|
|
for (let i = 0; i < 6; ++i) {
|
|
damages[i][0] *= attack_speed_mult;
|
|
damages[i][1] *= attack_speed_mult;
|
|
}
|
|
}
|
|
|
|
// 4. Add additive damage. TODO: Is there separate additive damage?
|
|
for (let i in damage_elements) {
|
|
if (present[i]) {
|
|
damages[i][0] += stats.get(damage_elements[i]+'DamAddMin');
|
|
damages[i][1] += stats.get(damage_elements[i]+'DamAddMax');
|
|
}
|
|
}
|
|
|
|
// 5. ID bonus.
|
|
let specific_boost_str = 'Md';
|
|
if (use_spell_damage) {
|
|
specific_boost_str = 'Sd';
|
|
}
|
|
// 5.1: %boost application
|
|
let skill_boost = [0]; // no neutral skillpoint booster
|
|
for (let i in skp_order) {
|
|
const skp = skp_order[i];
|
|
skill_boost.push(skillPointsToPercentage(stats.get(skp)) * skillpoint_damage_mult[i]);
|
|
}
|
|
let static_boost = (stats.get(specific_boost_str.toLowerCase()+'Pct') + stats.get('damPct')) / 100;
|
|
|
|
// These do not count raw damage. I think. Easy enough to change
|
|
let total_min = 0;
|
|
let total_max = 0;
|
|
for (let i in damage_elements) {
|
|
let damage_specific = damage_elements[i] + specific_boost_str + 'Pct';
|
|
let damageBoost = 1 + skill_boost[i] + static_boost
|
|
+ ((stats.get(damage_specific) + stats.get(damage_elements[i]+'DamPct')) /100);
|
|
damages[i][0] *= Math.max(damageBoost, 0);
|
|
damages[i][1] *= Math.max(damageBoost, 0);
|
|
// Collect total damage post %boost
|
|
total_min += damages[i][0];
|
|
total_max += damages[i][1];
|
|
}
|
|
|
|
let total_elem_min = total_min - damages[0][0];
|
|
let total_elem_max = total_max - damages[0][1];
|
|
|
|
// 5.2: Raw application.
|
|
let prop_raw = stats.get(specific_boost_str.toLowerCase()+'Raw') + stats.get('damRaw');
|
|
let rainbow_raw = stats.get('r'+specific_boost_str+'Raw') + stats.get('rDamRaw');
|
|
for (let i in damages) {
|
|
let damages_obj = damages[i];
|
|
let damage_prefix = damage_elements[i] + specific_boost_str;
|
|
// Normie raw
|
|
let raw_boost = 0;
|
|
if (present[i]) {
|
|
raw_boost += stats.get(damage_prefix+'Raw') + stats.get(damage_elements[i]+'DamRaw');
|
|
}
|
|
// Next, rainraw and propRaw
|
|
let min_boost = raw_boost;
|
|
let max_boost = raw_boost;
|
|
if (total_max > 0) { // TODO: what about total negative all raw?
|
|
// TODO: compute actual chance of 0 damage. For now we just copy max ratio
|
|
if (total_min === 0) {
|
|
min_boost += (damages_obj[1] / total_max) * prop_raw;
|
|
}
|
|
else {
|
|
min_boost += (damages_obj[0] / total_min) * prop_raw;
|
|
}
|
|
max_boost += (damages_obj[1] / total_max) * prop_raw;
|
|
}
|
|
if (i != 0 && total_elem_max > 0) { // rainraw TODO above
|
|
// TODO: compute actual chance of 0 damage. For now we just copy max ratio
|
|
if (total_elem_min === 0) {
|
|
min_boost += (damages_obj[1] / total_elem_max) * rainbow_raw;
|
|
}
|
|
else {
|
|
min_boost += (damages_obj[0] / total_elem_min) * rainbow_raw;
|
|
}
|
|
max_boost += (damages_obj[1] / total_elem_max) * rainbow_raw;
|
|
}
|
|
damages_obj[0] += min_boost * total_convert;
|
|
damages_obj[1] += max_boost * total_convert;
|
|
}
|
|
|
|
// 6. Strength boosters
|
|
// str/dex, as well as any other mutually multiplicative effects
|
|
let strBoost = 1 + skill_boost[1];
|
|
let total_dam_norm = [0, 0];
|
|
let total_dam_crit = [0, 0];
|
|
let damages_results = [];
|
|
const mult_map = stats.get("damMult");
|
|
let damage_mult = 1;
|
|
for (const [k, v] of mult_map.entries()) {
|
|
if (k.includes(':')) {
|
|
// TODO: fragile... checking for specific part multipliers.
|
|
const spell_match = k.split(':')[1];
|
|
if (spell_match !== part_filter) {
|
|
continue;
|
|
}
|
|
}
|
|
damage_mult *= (1 + v/100);
|
|
}
|
|
|
|
const crit_mult = stats.get("critDamPct")/100;
|
|
|
|
for (const damage of damages) {
|
|
const res = [
|
|
damage[0] * strBoost * damage_mult, // Normal min
|
|
damage[1] * strBoost * damage_mult, // Normal max
|
|
damage[0] * (strBoost + crit_mult) * damage_mult, // Crit min
|
|
damage[1] * (strBoost + crit_mult) * damage_mult, // Crit max
|
|
];
|
|
damages_results.push(res);
|
|
total_dam_norm[0] += res[0];
|
|
total_dam_norm[1] += res[1];
|
|
total_dam_crit[0] += res[2];
|
|
total_dam_crit[1] += res[3];
|
|
}
|
|
|
|
if (total_dam_norm[0] < 0) total_dam_norm[0] = 0;
|
|
if (total_dam_norm[1] < 0) total_dam_norm[1] = 0;
|
|
if (total_dam_crit[0] < 0) total_dam_crit[0] = 0;
|
|
if (total_dam_crit[1] < 0) total_dam_crit[1] = 0;
|
|
|
|
return [total_dam_norm, total_dam_crit, damages_results];
|
|
}
|
|
|
|
/*
|
|
Spell schema:
|
|
|
|
spell: {
|
|
name: str internal string name for the spell. Unique identifier, also display
|
|
cost: Optional[int] ignored for spells that are not id 1-4
|
|
base_spell: int spell index. 0-4 are reserved (0 is melee, 1-4 is common 4 spells)
|
|
spell_type: str [TODO: DEPRECATED/REMOVE] "healing" or "damage"
|
|
scaling: Optional[str] [DEFAULT: "spell"] "melee" or "spell"
|
|
use_atkspd: Optional[bool] [DEFAULT: true] true to factor attack speed, false otherwise.
|
|
display: Optional[str] [DEFAULT: "total"] "total" to sum all parts. Or, the name of a spell part
|
|
parts: List[part] Parts of this spell (different stuff the spell does basically)
|
|
}
|
|
|
|
NOTE: when using `replace_spell` on an existing spell, all fields become optional.
|
|
Specified fields overwrite existing fields; unspecified fields are left unchanged.
|
|
|
|
|
|
There are three possible spell "part" types: damage, heal, and total.
|
|
|
|
part: spell_damage | spell_heal | spell_total
|
|
|
|
spell_damage: {
|
|
name: str != "total" Name of the part.
|
|
type: "damage" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
|
|
multipliers: array[num, 6] floating point spellmults (though supposedly wynn only supports integer mults)
|
|
}
|
|
spell_heal: {
|
|
name: str != "total" Name of the part.
|
|
type: "heal" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
|
|
power: num floating point healing power (1 is 100% of max hp).
|
|
}
|
|
spell_total: {
|
|
name: str != "total" Name of the part.
|
|
type: "total" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
|
|
hits: Map[str, num] Keys are other part names, numbers are the multipliers. Undefined behavior if subparts
|
|
are not the same type of spell. Can only pull from spells defined before it.
|
|
}
|
|
|
|
|
|
Before passing to display, use the following structs.
|
|
NOTE: total is collapsed into damage or healing.
|
|
|
|
spell_damage: {
|
|
type: "damage" Internal use
|
|
name: str Display name of part. Should be human readable
|
|
normal_min: array[num, 6] floating point damages (no crit, min), can be less than zero. Order: NETWFA
|
|
normal_max: array[num, 6] floating point damages (no crit, max)
|
|
normal_total: array[num, 2] (min, max) noncrit total damage (not negative)
|
|
crit_min: array[num, 6] floating point damages (crit, min), can be less than zero. Order: NETWFA
|
|
crit_max: array[num, 6] floating point damages (crit, max)
|
|
crit_total: array[num, 2] (min, max) crit total damage (not negative)
|
|
}
|
|
spell_heal: {
|
|
type: "heal" Internal use
|
|
name: str Display name of part. Should be human readable
|
|
heal_amount: num floating point HP healed (self)
|
|
}
|
|
|
|
*/
|
|
|
|
const default_spells = {
|
|
wand: [{
|
|
type: "replace_spell", // not needed but makes this usable as an "abil part"
|
|
name: "Wand Melee", // TODO: name for melee attacks?
|
|
base_spell: 0,
|
|
scaling: "melee", use_atkspd: false,
|
|
display: "Melee",
|
|
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
|
|
}],
|
|
spear: [{
|
|
type: "replace_spell", // not needed but makes this usable as an "abil part"
|
|
name: "Melee", // TODO: name for melee attacks?
|
|
base_spell: 0,
|
|
scaling: "melee", use_atkspd: false,
|
|
display: "Melee",
|
|
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
|
|
}],
|
|
bow: [{
|
|
type: "replace_spell", // not needed but makes this usable as an "abil part"
|
|
name: "Bow Shot", // TODO: name for melee attacks?
|
|
base_spell: 0,
|
|
scaling: "melee", use_atkspd: false,
|
|
display: "Single Shot",
|
|
parts: [{ name: "Single Shot", multipliers: [100, 0, 0, 0, 0, 0] }]
|
|
}],
|
|
dagger: [{
|
|
type: "replace_spell", // not needed but makes this usable as an "abil part"
|
|
name: "Melee", // TODO: name for melee attacks?
|
|
base_spell: 0,
|
|
scaling: "melee", use_atkspd: false,
|
|
display: "Melee",
|
|
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
|
|
}],
|
|
relik: [{
|
|
type: "replace_spell", // not needed but makes this usable as an "abil part"
|
|
name: "Relik Melee", // TODO: name for melee attacks?
|
|
base_spell: 0,
|
|
spell_type: "damage",
|
|
scaling: "melee", use_atkspd: false,
|
|
display: "Total",
|
|
parts: [
|
|
{ name: "Single Beam", multipliers: [33, 0, 0, 0, 0, 0] },
|
|
{ name: "Total", hits: { "Single Beam": 3 } }
|
|
]
|
|
}]
|
|
};
|
|
|
|
const spell_table = {
|
|
"powder": [ //This is how instant-damage powder specials are implemented.
|
|
{ title: "Quake", cost: 0, parts:[
|
|
{ subtitle: "Total Damage", type: "damage", multiplier: [155, 220, 285, 350, 415], conversion: [0,100,0,0,0,0], summary: true},
|
|
] },
|
|
{ title: "Chain Lightning", cost: 0, parts: [
|
|
{ subtitle: "Total Damage", type: "damage", multiplier: [200, 225, 250, 275, 300], conversion: [0,0,100,0,0,0], summary: true},
|
|
]},
|
|
{ title: "Courage", cost: 0, parts: [
|
|
{ subtitle: "Total Damage", type: "damage", multiplier: [75, 87.5, 100, 112.5, 125], conversion: [0,0,0,0,100,0], summary: true},
|
|
]}, //[75, 87.5, 100, 112.5, 125]
|
|
]
|
|
};
|