wynnbuilder-forked-for-changes/js/damage_calc.js
2022-07-01 01:23:06 -07:00

432 lines
20 KiB
JavaScript

const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["vanish", 0.80], ["warscream", 0.10], ["bash", 0.50] ]);
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) {
// 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 = weapon.get(damage_present_key);
// 2. Conversions.
// 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; //TODO get confirmation that this is how raw works.
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
}
}
// Also theres prop and rainbow!!
const damage_elements = ['n'].concat(skp_elements); // netwfa
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 = 0; i < 6; ++i) {
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 (const skp of skp_order) {
skill_boost.push(skillPointsToPercentage(stats.get(skp)));
}
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 damages) {
let damage_prefix = damage_elements[i] + specific_boost_str;
let damageBoost = 1 + skill_boost[i] + static_boost
+ ((stats.get(damage_prefix+'Pct') + 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?
if (total_elem_min > 0) {
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
if (total_elem_min > 0) {
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 damage_mult = stats.get("damageMultiplier");
for (const damage of damages) {
const res = [
damage[0] * strBoost * damage_mult, // Normal min
damage[1] * strBoost * damage_mult, // Normal max
damage[0] * (strBoost + 1) * damage_mult, // Crit min
damage[1] * (strBoost + 1) * 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] }]
}, {
name: "Heal", // TODO: name for melee attacks? // JUST FOR TESTING...
base_spell: 1,
display: "Total Heal",
parts: [
{ name: "First Pulse", power: 0.12 },
{ name: "Second and Third Pulses", power: 0.06 },
{ name: "Total Heal", hits: { "First Pulse": 1, "Second and Third Pulses": 2 } }
]
}],
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 = {
"wand": [
{ title: "Heal", cost: 6, parts: [
{ subtitle: "First Pulse", type: "heal", strength: 0.12 },
{ subtitle: "Second and Third Pulses", type: "heal", strength: 0.06 },
{ subtitle: "Total Heal", type: "heal", strength: 0.24, summary: true },
{ subtitle: "First Pulse (Ally)", type: "heal", strength: 0.20 },
{ subtitle: "Second and Third Pulses (Ally)", type: "heal", strength: 0.1 },
{ subtitle: "Total Heal (Ally)", type: "heal", strength: 0.4 }
] },
{ title: "Teleport", cost: 4, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 150, conversion: [60, 0, 40, 0, 0, 0], summary: true },
] },
{ title: "Meteor", cost: 8, parts: [
{ subtitle: "Blast Damage", type: "damage", multiplier: 500, conversion: [40, 30, 0, 0, 30, 0], summary: true },
{ subtitle: "Burn Damage", type: "damage", multiplier: 125, conversion: [100, 0, 0, 0, 0, 0] },
] },
{ title: "Ice Snake", cost: 4, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 70, conversion: [50, 0, 0, 50, 0, 0], summary: true },
] },
],
"spear": [
{ title: "Bash", cost: 6, parts: [
{ subtitle: "First Damage", type: "damage", multiplier: 130, conversion: [60, 40, 0, 0, 0, 0]},
{ subtitle: "Explosion Damage", type: "damage", multiplier: 130, conversion: [100, 0, 0, 0, 0, 0]},
{ subtitle: "Total Damage", type: "total", factors: [1, 1], summary: true },
] },
{ title: "Charge", cost: 4, variants: {
DEFAULT: [
{ subtitle: "Total Damage", type: "damage", multiplier: 150, conversion: [60, 0, 0, 0, 40, 0], summary: true }
],
RALLY: [
{ subtitle: "Self Heal", type: "heal", strength: 0.07, summary: true },
{ subtitle: "Ally Heal", type: "heal", strength: 0.15 }
]
} },
{ title: "Uppercut", cost: 9, parts: [
{ subtitle: "First Damage", type: "damage", multiplier: 300, conversion: [70, 20, 10, 0, 0, 0] },
{ subtitle: "Fireworks Damage", type: "damage", multiplier: 50, conversion: [60, 0, 40, 0, 0, 0] },
{ subtitle: "Crash Damage", type: "damage", multiplier: 50, conversion: [80, 0, 20, 0, 0, 0] },
{ subtitle: "Total Damage", type: "total", factors: [1, 1, 1], summary: true },
] },
{ title: "War Scream", cost: 6, parts: [
{ subtitle: "Area Damage", type: "damage", multiplier: 50, conversion: [0, 0, 0, 0, 75, 25], summary: true },
{ subtitle: "Air Shout (Per Hit)", type: "damage", multiplier: 30, conversion: [0, 0, 0, 0, 75, 25] },
] },
],
"bow": [
{ title: "Arrow Storm", cost: 6, variants: {
DEFAULT: [
{ subtitle: "Total Damage", type: "damage", multiplier: 600, conversion: [60, 0, 25, 0, 15, 0], summary: true },
{ subtitle: "Per Arrow (60)", type: "damage", multiplier: 10, conversion: [60, 0, 25, 0, 15, 0]}
],
HAWKEYE: [
{ subtitle: "Total Damage (Hawkeye)", type: "damage", multiplier: 400, conversion: [60, 0, 25, 0, 15, 0], summary: true },
{ subtitle: "Per Arrow (5)", type: "damage", multiplier: 80, conversion: [60, 0, 25, 0, 15, 0]}
],
} },
{ title: "Escape", cost: 3, parts: [
{ subtitle: "Landing Damage", type: "damage", multiplier: 100, conversion: [50, 0, 0, 0, 0, 50], summary: true },
] },
{ title: "Bomb Arrow", cost: 8, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 250, conversion: [60, 25, 0, 0, 15, 0], summary: true },
] },
{ title: "Arrow Shield", cost: 10, parts: [
{ subtitle: "Shield Damage", type: "damage", multiplier: 100, conversion: [70, 0, 0, 0, 0, 30], summary: true },
{ subtitle: "Arrow Rain Damage", type: "damage", multiplier: 200, conversion: [70, 0, 0, 0, 0, 30] },
] },
],
"dagger": [
{ title: "Spin Attack", cost: 6, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 150, conversion: [70, 0, 30, 0, 0, 0], summary: true},
] },
{ title: "Vanish", cost: 2, parts: [
{ subtitle: "No Damage", type: "none", summary: true }
] },
{ title: "Multihit", cost: 8, parts: [
{ subtitle: "1st to 10th Hit", type: "damage", multiplier: 27, conversion: [100, 0, 0, 0, 0, 0] },
{ subtitle: "Fatality", type: "damage", multiplier: 120, conversion: [20, 0, 30, 50, 0, 0] },
{ subtitle: "Total Damage", type: "total", factors: [10, 1], summary: true },
] },
{ title: "Smoke Bomb", cost: 8, variants: {
DEFAULT: [
{ subtitle: "Tick Damage (10 max)", type: "damage", multiplier: 60, conversion: [50, 25, 0, 0, 0, 25] },
{ subtitle: "Total Damage", type: "damage", multiplier: 600, conversion: [50, 25, 0, 0, 0, 25], summary: true },
],
CHERRY_BOMBS: [
{ subtitle: "Total Damage (Cherry Bombs)", type: "damage", multiplier: 330, conversion: [50, 25, 0, 0, 0, 25], summary: true },
{ subtitle: "Per Bomb", type: "damage", multiplier: 110, conversion: [50, 25, 0, 0, 0, 25] }
]
} },
],
"relik": [
{ title: "Totem", cost: 4, parts: [
{ subtitle: "Smash Damage", type: "damage", multiplier: 100, conversion: [80, 0, 0, 0, 20, 0]},
{ subtitle: "Damage Tick", type: "damage", multiplier: 20, conversion: [80, 0, 0, 0, 0, 20]},
{ subtitle: "Heal Tick", type: "heal", strength: 0.03, summary: true },
] },
{ title: "Haul", cost: 1, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 100, conversion: [80, 0, 20, 0, 0, 0], summary: true },
] },
{ title: "Aura", cost: 8, parts: [
{ subtitle: "One Wave", type: "damage", multiplier: 200, conversion: [70, 0, 0, 30, 0, 0], summary: true },
] },
{ title: "Uproot", cost: 6, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 100, conversion: [70, 30, 0, 0, 0, 0], summary: true },
] },
],
"sword": [
{ title: "Successive Strikes", cost: 5, parts: [
{ subtitle: "Damage", type: "damage", multiplier: 65, conversion: [70, 0, 15, 0, 0, 15]},
{ subtitle: "Final Strike", type: "damage", multiplier: 120, conversion: [70, 0, 15, 0, 0, 15]},
{ subtitle: "Total Damage (Normal)", type: "total", factors: [2, 0], summary: true },
] },
{ title: "Dash", cost: 3, parts: [
{ subtitle: "Damage", type: "damage", multiplier: 120, conversion: [60, 0, 0, 0, 0, 40], summary: true },
] },
{ title: "Execute", cost: 8, parts: [
{ subtitle: "Minimum Damage", type: "damage", multiplier: 100, conversion: [60, 0, 20, 0, 20, 0]},
{ subtitle: "Maximum Damage", type: "damage", multiplier: 1200, conversion: [60, 0, 20, 0, 20, 0], summary: true },
] },
{ title: "Blade Echo", cost: 4, parts: [
{ subtitle: "Damage", type: "damage", multiplier: 125, conversion: [60, 0, 0, 20, 0, 20], summary: true },
] },
],
"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]
]
};