diff --git a/js/builder_graph.js b/js/builder_graph.js index d1825e2..b8b4833 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -468,6 +468,7 @@ class PowderInputNode extends InputNode { * Signature: SpellSelectNode(build: Build) => [Spell, SpellParts] */ class SpellSelectNode extends ComputeNode { + // TODO: rewrite me entirely... constructor(spell_num) { super("builder-spell"+spell_num+"-select"); this.spell_idx = spell_num; @@ -475,10 +476,18 @@ class SpellSelectNode extends ComputeNode { compute_func(input_map) { const build = input_map.get('build'); + let stats = build.statMap; const i = this.spell_idx; - let spell = spell_table[build.weapon.statMap.get("type")][i]; - let stats = build.statMap; + const spells = default_spells[build.weapon.statMap.get("type")]; + let spell; + for (const _spell of spells) { + if (_spell.base_spell === i) { + spell = _spell; + break; + } + } + if (spell === undefined) { return null; } let spell_parts; if (spell.parts) { @@ -551,6 +560,7 @@ class SpellDamageCalcNode extends ComputeNode { compute_func(input_map) { const weapon = input_map.get('build').weapon.statMap; const spell_info = input_map.get('spell-info'); + const spell = spell_info[0]; const spell_parts = spell_info[1]; const stats = input_map.get('stats'); const damage_mult = stats.get('damageMultiplier'); @@ -562,23 +572,66 @@ class SpellDamageCalcNode extends ComputeNode { stats.get('agi') ]; let spell_results = [] + let spell_result_map = new Map(); + const use_speed = (('use_atkspd' in spell) ? spell.use_atkspd : true); + const use_spell = (('scaling' in spell) ? spell.scaling === 'spell' : true); + // TODO: move preprocessing to separate node/node chain for (const part of spell_parts) { - if (part.type === "damage") { - let tmp_conv = []; - for (let i in part.conversion) { - tmp_conv.push(part.conversion[i] * part.multiplier/100); + let spell_result; + if ('multipliers' in part) { // damage type spell + let results = calculateSpellDamage(stats, weapon, part.multipliers, use_spell, !use_speed); + spell_result = { + type: "damage", + normal_min: results[2].map(x => x[0]), + normal_max: results[2].map(x => x[1]), + normal_total: results[0], + crit_min: results[2].map(x => x[2]), + crit_max: results[2].map(x => x[3]), + crit_total: results[1], } - let results = calculateSpellDamage(stats, weapon, tmp_conv, true); - spell_results.push(results); - } else if (part.type === "heal") { + } else if ('power' in part) { // TODO: wynn2 formula - let heal_amount = (part.strength * getDefenseStats(stats)[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); - spell_results.push(heal_amount); - } else if (part.type === "total") { - // TODO: remove "total" type - spell_results.push(null); + let _heal_amount = (part.strength * getDefenseStats(stats)[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); + spell_result = { + type: "heal", + heal_amount: _heal_amount + } + } else if ('hits' in part) { + spell_result = { + normal_min: [0, 0, 0, 0, 0, 0], + normal_max: [0, 0, 0, 0, 0, 0], + normal_total: [0, 0], + crit_min: [0, 0, 0, 0, 0, 0], + crit_max: [0, 0, 0, 0, 0, 0], + crit_total: [0, 0] + } + const dam_res_keys = ['normal_min', 'normal_max', 'normal_total', 'crit_min', 'crit_max', 'crit_total']; + for (const [subpart_name, hits] of Object.entries(part.hits)) { + const subpart = spell_result_map.get(subpart_name); + if (spell_result.type) { + if (subpart.type !== spell_result.type) { + throw "SpellCalc total subpart type mismatch"; + } + } + else { + spell_result.type = subpart.type; + } + if (spell_result.type === 'damage') { + for (const key of dam_res_keys) { + for (let i in spell_result.normal_min) { + spell_result[key][i] += subpart[key][i] * hits; + } + } + } + else { + spell_result.heal_amount += subpart.heal_amount; + } + } } + spell_result.name = part.name; + spell_results.push(spell_result); + spell_result_map.set(part.name, spell_result); } return spell_results; } @@ -604,12 +657,11 @@ class SpellDisplayNode extends ComputeNode { const spell_info = input_map.get('spell-info'); const damages = input_map.get('spell-damage'); const spell = spell_info[0]; - const spell_parts = spell_info[1]; const i = this.spell_idx; let parent_elem = document.getElementById("spell"+i+"-info"); let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaySpellDamage(parent_elem, overallparent_elem, stats, spell, i+1, spell_parts, damages); + displaySpellDamage(parent_elem, overallparent_elem, stats, spell, i+1, damages); } } @@ -1059,7 +1111,8 @@ function builder_graph_init() { // Also do something similar for skill points - for (let i = 0; i < 4; ++i) { + //for (let i = 0; i < 4; ++i) { + for (let i = 0; i < 1; ++i) { let spell_node = new SpellSelectNode(i); spell_node.link_to(build_node, 'build'); // TODO: link and rewrite spell_node to the stat agg node diff --git a/js/computation_graph.js b/js/computation_graph.js index 76b1c2d..a3903eb 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -89,6 +89,9 @@ class ComputeNode { throw "no compute func specified"; } + /** + * Add link to a parent compute node, optionally with an alias. + */ link_to(parent_node, link_name) { this.inputs.push(parent_node) link_name = (link_name !== undefined) ? link_name : parent_node.name; @@ -100,6 +103,26 @@ class ComputeNode { parent_node.children.push(this); return this; } + + /** + * Delete a link to a parent node. + * TODO: time complexity of list deletion (not super relevant but it hurts my soul) + */ + remove_link(parent_node) { + const idx = this.inputs.indexOf(parent_node); // Get idx + this.inputs.splice(idx, 1); // remove element + + this.input_translations.delete(parent_node.name); + const was_dirty = this.inputs_dirty.get(parent_node.name); + this.inputs_dirty.delete(parent_node.name); + if (was_dirty) { + this.inputs_dirty_count -= 1; + } + + const idx2 = parent_node.children.indexOf(this); + parent_node.children.splice(idx2, 1); + return this; + } } /** diff --git a/js/damage_calc.js b/js/damage_calc.js index 2195926..2710ec4 100644 --- a/js/damage_calc.js +++ b/js/damage_calc.js @@ -56,11 +56,14 @@ function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, igno // 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) { - damages[i][0] += conversions[i]/100 * weapon_min; - damages[i][1] += conversions[i]/100 * weapon_max; + const conv_frac = conversions[i]/100; + damages[i][0] += conv_frac * weapon_min; + damages[i][1] += conf_frac * weapon_max; present[i] = true; + total_convert += conv_frac } } @@ -125,22 +128,22 @@ function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, igno raw_boost += stats.get(damage_prefix+'Raw') + stats.get(damage_elements[i]+'DamRaw'); } // Next, rainraw and propRaw - let new_min = damages_obj[0] + raw_boost; - let new_max = damages_obj[1] + raw_boost; + 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) { - new_min += (damages_obj[0] / total_min) * prop_raw; + min_boost += (damages_obj[0] / total_min) * prop_raw; } - new_max += (damages_obj[1] / total_max) * 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) { - new_min += (damages_obj[0] / total_elem_min) * rainbow_raw; + min_boost += (damages_obj[0] / total_elem_min) * rainbow_raw; } - new_max += (damages_obj[1] / total_elem_max) * rainbow_raw; + max_boost += (damages_obj[1] / total_elem_max) * rainbow_raw; } - damages_obj[0] = new_min; - damages_obj[1] = new_max; + damages_obj[0] += min_boost * total_convert; + damages_obj[1] += max_boost * total_convert; } // 6. Strength boosters @@ -182,8 +185,9 @@ spell: { display_text: str short description of the spell, ex. Bash, Meteor, Arrow Shield 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: str "melee" or "spell" - display: str "total" to sum all parts. Or, the name of a spell part + 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) } @@ -212,37 +216,74 @@ spell_total: { 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: [{ - name: "Melee", // TODO: name for melee attacks? + name: "Magic Strike", // TODO: name for melee attacks? display_text: "Mage basic attack", - base_spell: 0, // Spell 0 is special cased to be handled with melee logic. - spell_type: "damage", - scaling: "melee", - display: "total", - parts: { name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] } + base_spell: 0, + scaling: "melee", use_atkspd: false, + display: "Melee", + parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] }], spear: [{ name: "Melee", // TODO: name for melee attacks? display_text: "Warrior basic attack", - base_spell: 0, // Spell 0 is special cased to be handled with melee logic. - spell_type: "damage", - scaling: "melee", - display: "total", - parts: { name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] } + base_spell: 0, + scaling: "melee", use_atkspd: false, + display: "Melee", + parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] }], - bow: [ - - ], - dagger: [ - - ], - relik: [ - - ], -} + bow: [{ + name: "Bow Shot", // TODO: name for melee attacks? + display_text: "Archer basic attack", + base_spell: 0, + scaling: "melee", use_atkspd: false, + display: "Melee", + parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] + }], + dagger: [{ + name: "Melee", // TODO: name for melee attacks? + display_text: "Assassin basic attack", + base_spell: 0, + scaling: "melee", use_atkspd: false, + display: "Melee", + parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] + }], + relik: [{ + name: "Spread Beam", // TODO: name for melee attacks? + display_text: "Shaman basic attack", + 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": [ diff --git a/js/display.js b/js/display.js index a47667d..e4114ed 100644 --- a/js/display.js +++ b/js/display.js @@ -1579,7 +1579,7 @@ function getBaseSpellCost(stats, spellIdx, cost) { } -function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spellIdx, spell_parts, damages) { +function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spellIdx, spell_results) { // TODO: remove spellIdx (just used to flag melee and cost) // TODO: move cost calc out parent_elem.textContent = ""; @@ -1589,7 +1589,7 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell overallparent_elem.textContent = ""; let title_elemavg = document.createElement("b"); - if (spellIdx != 0) { + if ('cost' in spell) { let first = document.createElement("span"); first.textContent = spell.title + " ("; title_elem.appendChild(first.cloneNode(true)); //cloneNode is needed here. @@ -1611,44 +1611,36 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell title_elemavg.appendChild(third_summary); } else { - title_elem.textContent = spell.title; - title_elemavg.textContent = spell.title; + title_elem.textContent = spell.name; + title_elemavg.textContent = spell.name; } parent_elem.append(title_elem); overallparent_elem.append(title_elemavg); - overallparent_elem.append(displayNextCosts(stats, spell, spellIdx)); + if ('cost' in spell) { + overallparent_elem.append(displayNextCosts(stats, spell, spellIdx)); + } let critChance = skillPointsToPercentage(stats.get('dex')); - let save_damages = []; - let part_divavg = document.createElement("p"); overallparent_elem.append(part_divavg); - for (let i = 0; i < spell_parts.length; ++i) { - const part = spell_parts[i]; - const damage = damages[i]; + for (let i = 0; i < spell_results.length; ++i) { + const damage_info = spell_results[i]; let part_div = document.createElement("p"); parent_elem.append(part_div); let subtitle_elem = document.createElement("p"); - subtitle_elem.textContent = part.subtitle; + subtitle_elem.textContent = damage_info.name part_div.append(subtitle_elem); - if (part.type === "damage") { - let _results = damage; - let totalDamNormal = _results[0]; - let totalDamCrit = _results[1]; - let results = _results[2]; - - for (let i = 0; i < 6; ++i) { - for (let j in results[i]) { - results[i][j] = results[i][j].toFixed(2); - } - } + if (damage_info.type === "damage") { + let totalDamNormal = damage_info.normal_total; + let totalDamCrit = damage_info.crit_total; + let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; @@ -1659,11 +1651,11 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell part_div.append(averageLabel); - if (part.summary == true) { + if (damage_info.name === spell.display) { let overallaverageLabel = document.createElement("p"); let first = document.createElement("span"); let second = document.createElement("span"); - first.textContent = part.subtitle + " Average: "; + first.textContent = damage_info.name+ " Average: "; second.textContent = averageDamage.toFixed(2); overallaverageLabel.appendChild(first); overallaverageLabel.appendChild(second); @@ -1671,71 +1663,65 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell part_divavg.append(overallaverageLabel); } - function _damage_display(label_text, average, result_idx) { + function _damage_display(label_text, average, dmg_min, dmg_max) { let label = document.createElement("p"); label.textContent = label_text+average.toFixed(2); part_div.append(label); - let arrmin = []; - let arrmax = []; for (let i = 0; i < 6; i++){ - if (results[i][1] != 0){ + if (dmg_max[i] != 0){ let p = document.createElement("p"); p.classList.add(damageClasses[i]); - p.textContent = results[i][result_idx] + " \u2013 " + results[i][result_idx + 1]; - arrmin.push(results[i][result_idx]); - arrmax.push(results[i][result_idx + 1]); + p.textContent = dmg_min[i].toFixed(2)+" \u2013 "+dmg_max[i].toFixed(2); part_div.append(p); } } } - _damage_display("Non-Crit Average: ", nonCritAverage, 0); - _damage_display("Crit Average: ", critAverage, 2); - - save_damages.push(averageDamage); - } else if (part.type === "heal") { - let heal_amount = damage; + _damage_display("Non-Crit Average: ", nonCritAverage, damage_info.normal_min, damage_info.normal_max); + _damage_display("Crit Average: ", critAverage, damage_info.crit_min, damage_info.crit_max); + } else if (damage_info.type === "heal") { + let heal_amount = damage_info.heal_amount; let healLabel = document.createElement("p"); healLabel.textContent = heal_amount; // healLabel.classList.add("damagep"); part_div.append(healLabel); - if (part.summary == true) { + if (damage_info.name === spell.display) { let overallhealLabel = document.createElement("p"); let first = document.createElement("span"); let second = document.createElement("span"); - first.textContent = part.subtitle + ": "; + first.textContent = damage_info.name+ ": "; second.textContent = heal_amount; overallhealLabel.appendChild(first); second.classList.add("Set"); overallhealLabel.appendChild(second); part_divavg.append(overallhealLabel); } - } else if (part.type === "total") { - let total_damage = 0; - for (let i in part.factors) { - total_damage += save_damages[i] * part.factors[i]; - } - - let dmgarr = part.factors.slice(); - dmgarr = dmgarr.map(x => "(" + x + " * " + save_damages[dmgarr.indexOf(x)].toFixed(2) + ")"); - - - let averageLabel = document.createElement("p"); - averageLabel.textContent = "Average: "+total_damage.toFixed(2); - averageLabel.classList.add("damageSubtitle"); - part_div.append(averageLabel); - - let overallaverageLabel = document.createElement("p"); - let overallaverageLabelFirst = document.createElement("span"); - let overallaverageLabelSecond = document.createElement("span"); - overallaverageLabelFirst.textContent = "Average: "; - overallaverageLabelSecond.textContent = total_damage.toFixed(2); - overallaverageLabelSecond.classList.add("Damage"); - - - overallaverageLabel.appendChild(overallaverageLabelFirst); - overallaverageLabel.appendChild(overallaverageLabelSecond); - part_divavg.append(overallaverageLabel); +// } else if (part.type === "total") { +// let total_damage = 0; +// for (let i in part.factors) { +// total_damage += save_damages[i] * part.factors[i]; +// } +// +// let dmgarr = part.factors.slice(); +// dmgarr = dmgarr.map(x => "(" + x + " * " + save_damages[dmgarr.indexOf(x)].toFixed(2) + ")"); +// +// +// let averageLabel = document.createElement("p"); +// averageLabel.textContent = "Average: "+total_damage.toFixed(2); +// averageLabel.classList.add("damageSubtitle"); +// part_div.append(averageLabel); +// +// let overallaverageLabel = document.createElement("p"); +// let overallaverageLabelFirst = document.createElement("span"); +// let overallaverageLabelSecond = document.createElement("span"); +// overallaverageLabelFirst.textContent = "Average: "; +// overallaverageLabelSecond.textContent = total_damage.toFixed(2); +// overallaverageLabelSecond.classList.add("Damage"); +// +// +// overallaverageLabel.appendChild(overallaverageLabelFirst); +// overallaverageLabel.appendChild(overallaverageLabelSecond); +// part_divavg.append(overallaverageLabel); } }