diff --git a/js/builder_graph.js b/js/builder_graph.js index 1d7d509..b44964c 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,121 +1,167 @@ let armor_powder_node = new (class extends ComputeNode { - constructor() { super('builder-armor-powder-input'); } + constructor() { + super("builder-armor-powder-input"); + } - compute_func(input_map) { - let damage_boost = 0; - let def_boost = 0; - let statMap = new Map(); - for (const [e, elem] of zip2(skp_elements, skp_order)) { - let val = parseInt(document.getElementById(elem+"_boost_armor").value); - statMap.set(e+'DamPct', val); - } - return statMap; + compute_func(input_map) { + let damage_boost = 0; + let def_boost = 0; + let statMap = new Map(); + for (const [e, elem] of zip2(skp_elements, skp_order)) { + let val = parseInt(document.getElementById(elem + "_boost_armor").value); + statMap.set(e + "DamPct", val); } + return statMap; + } })(); let boosts_node = new (class extends ComputeNode { - constructor() { super('builder-boost-input'); } + constructor() { + super("builder-boost-input"); + } - compute_func(input_map) { - let damage_boost = 0; - let def_boost = 0; - for (const [key, value] of damageMultipliers) { - let elem = document.getElementById(key + "-boost") - if (elem.classList.contains("toggleOn")) { - damage_boost += value; - if (key === "warscream") { def_boost += .10 } - if (key === "vanish") { def_boost += .15 } - } + compute_func(input_map) { + let damage_boost = 0; + let def_boost = 0; + for (const [key, value] of damageMultipliers) { + let elem = document.getElementById(key + "-boost"); + if (elem.classList.contains("toggleOn")) { + damage_boost += value; + if (key === "warscream") { + def_boost += 0.1; } - let res = new Map(); - res.set('damMult.Potion', 100*damage_boost); - res.set('defMult.Potion', 100*def_boost); - return res; + if (key === "vanish") { + def_boost += 0.15; + } + } } + let res = new Map(); + res.set("damMult.Potion", 100 * damage_boost); + res.set("defMult.Potion", 100 * def_boost); + return res; + } })().update(); /* Updates all spell boosts -*/ + */ function update_boosts(buttonId) { - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { - elem.classList.remove("toggleOn"); - } else { - elem.classList.add("toggleOn"); - } - boosts_node.mark_dirty().update(); + let elem = document.getElementById(buttonId); + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } else { + elem.classList.add("toggleOn"); + } + boosts_node.mark_dirty().update(); } -let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"]; +let specialNames = [ + "Quake", + "Chain Lightning", + "Curse", + "Courage", + "Wind Prison", +]; let powder_special_input = new (class extends ComputeNode { - constructor() { super('builder-powder-special-input'); } + constructor() { + super("builder-powder-special-input"); + } - compute_func(input_map) { - let powder_specials = []; // [ [special, power], [special, power]] - for (const sName of specialNames) { - for (let i = 1;i < 6; i++) { - if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { - let powder_special = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; - powder_specials.push([powder_special, i]); - break; - } - } + compute_func(input_map) { + let powder_specials = []; // [ [special, power], [special, power]] + for (const sName of specialNames) { + for (let i = 1; i < 6; i++) { + if ( + document + .getElementById(sName.replace(" ", "_") + "-" + i) + .classList.contains("toggleOn") + ) { + let powder_special = + powderSpecialStats[specialNames.indexOf(sName.replace("_", " "))]; + powder_specials.push([powder_special, i]); + break; } - return powder_specials; + } } + return powder_specials; + } })(); function updatePowderSpecials(buttonId) { - let prefix = (buttonId).split("-")[0].replace(' ', '_') + '-'; - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { elem.classList.remove("toggleOn"); } - else { - for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off - //name is same, power is i - const elem2 = document.getElementById(prefix + i); - if(elem2.classList.contains("toggleOn")) { elem2.classList.remove("toggleOn"); } - } - //toggle the pressed button on - elem.classList.add("toggleOn"); + let prefix = buttonId.split("-")[0].replace(" ", "_") + "-"; + let elem = document.getElementById(buttonId); + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } else { + for (let i = 1; i < 6; i++) { + //toggle all pressed buttons of the same powder special off + //name is same, power is i + const elem2 = document.getElementById(prefix + i); + if (elem2.classList.contains("toggleOn")) { + elem2.classList.remove("toggleOn"); + } } - powder_special_input.mark_dirty().update(); + //toggle the pressed button on + elem.classList.add("toggleOn"); + } + powder_special_input.mark_dirty().update(); } class PowderSpecialCalcNode extends ComputeNode { - constructor() { super('builder-powder-special-apply'); } + constructor() { + super("builder-powder-special-apply"); + } - compute_func(input_map) { - const powder_specials = input_map.get('powder-specials'); - let stats = new Map(); - for (const [special, power] of powder_specials) { - if (special["weaponSpecialEffects"].has("Damage Boost")) { - let name = special["weaponSpecialName"]; - if (name === "Courage" || name === "Curse") { //courage and curse are is universal damage boost - stats.set("sdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); - stats.set("mdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); - stats.set("poisonPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); - } else if (name === "Wind Prison") { - stats.set("aDamPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); - } - } + compute_func(input_map) { + const powder_specials = input_map.get("powder-specials"); + let stats = new Map(); + for (const [special, power] of powder_specials) { + if (special["weaponSpecialEffects"].has("Damage Boost")) { + let name = special["weaponSpecialName"]; + if (name === "Courage" || name === "Curse") { + //courage and curse are is universal damage boost + stats.set( + "sdPct", + special.weaponSpecialEffects.get("Damage Boost")[power - 1] + ); + stats.set( + "mdPct", + special.weaponSpecialEffects.get("Damage Boost")[power - 1] + ); + stats.set( + "poisonPct", + special.weaponSpecialEffects.get("Damage Boost")[power - 1] + ); + } else if (name === "Wind Prison") { + stats.set( + "aDamPct", + special.weaponSpecialEffects.get("Damage Boost")[power - 1] + ); } - return stats; + } } + return stats; + } } class PowderSpecialDisplayNode extends ComputeNode { - // TODO: Refactor this entirely to be adding more spells to the spell list - constructor() { - super('builder-powder-special-display'); - this.fail_cb = true; - } + // TODO: Refactor this entirely to be adding more spells to the spell list + constructor() { + super("builder-powder-special-display"); + this.fail_cb = true; + } - compute_func(input_map) { - const powder_specials = input_map.get('powder-specials'); - const stats = input_map.get('stats'); - const weapon = input_map.get('build').weapon; - displayPowderSpecials(document.getElementById("powder-special-stats"), powder_specials, stats, weapon.statMap, true); - } + compute_func(input_map) { + const powder_specials = input_map.get("powder-specials"); + const stats = input_map.get("stats"); + const weapon = input_map.get("build").weapon; + displayPowderSpecials( + document.getElementById("powder-special-stats"), + powder_specials, + stats, + weapon.statMap, + true + ); + } } /** @@ -124,85 +170,95 @@ class PowderSpecialDisplayNode extends ComputeNode { * Signature: ItemInputNode(powdering: Optional[list[powder]]) => Item | null */ class ItemInputNode extends InputNode { - /** - * Make an item stat pulling compute node. - * - * @param name: Name of this node. - * @param item_input_field: Input field (html element) to listen for item names from. - * @param none_item: Item object to use as the "none" for this field. - */ - constructor(name, item_input_field, none_item) { - super(name, item_input_field); - this.none_item = new Item(none_item); - this.category = this.none_item.statMap.get('category'); - if (this.category == 'armor' || this.category == 'weapon') { - this.none_item.statMap.set('powders', []); - apply_weapon_powders(this.none_item.statMap); // Needed to put in damagecalc zeros - } - this.none_item.statMap.set('NONE', true); + /** + * Make an item stat pulling compute node. + * + * @param name: Name of this node. + * @param item_input_field: Input field (html element) to listen for item names from. + * @param none_item: Item object to use as the "none" for this field. + */ + constructor(name, item_input_field, none_item) { + super(name, item_input_field); + this.none_item = new Item(none_item); + this.category = this.none_item.statMap.get("category"); + if (this.category == "armor" || this.category == "weapon") { + this.none_item.statMap.set("powders", []); + apply_weapon_powders(this.none_item.statMap); // Needed to put in damagecalc zeros + } + this.none_item.statMap.set("NONE", true); + } + + compute_func(input_map) { + const powdering = input_map.get("powdering"); + + // built on the assumption of no one will type in CI/CR letter by letter + let item_text = this.input_field.value; + if (!item_text) { + return this.none_item; } - compute_func(input_map) { - const powdering = input_map.get('powdering'); - - // built on the assumption of no one will type in CI/CR letter by letter - let item_text = this.input_field.value; - if (!item_text) { - return this.none_item; - } - - let item; - if (item_text.slice(0, 3) == "CI-") { item = getCustomFromHash(item_text); } - else if (item_text.slice(0, 3) == "CR-") { item = getCraftFromHash(item_text); } - else if (itemMap.has(item_text)) { item = new Item(itemMap.get(item_text)); } - else if (tomeMap.has(item_text)) { item = new Item(tomeMap.get(item_text)); } - - if (item) { - if (powdering !== undefined) { - const max_slots = item.statMap.get('slots'); - item.statMap.set('powders', powdering.slice(0, max_slots)); - } - let type_match; - if (this.category == 'weapon') { - type_match = item.statMap.get('category') == 'weapon'; - } else { - type_match = item.statMap.get('type') == this.none_item.statMap.get('type'); - } - if (type_match) { - if (item.statMap.get('category') == 'armor') { - applyArmorPowders(item.statMap); - } - else if (item.statMap.get('category') == 'weapon') { - apply_weapon_powders(item.statMap); - } - return item; - } - } - else if (this.none_item.statMap.get('category') === 'weapon' && item_text.startsWith("Morph-")) { - let replace_items = [ "Morph-Stardust", - "Morph-Steel", - "Morph-Iron", - "Morph-Gold", - "Morph-Topaz", - "Morph-Emerald", - "Morph-Amethyst", - "Morph-Ruby", - item_text.substring(6) - ] - - for (const [i, x] of zip2(equipment_inputs, replace_items)) { setValue(i, x); } - - for (const node of item_nodes) { - if (node !== this) { - // save a tiny bit of compute - calcSchedule(node, 10); - } - } - // Needed to push the weapon node's updates forward - return this.compute_func(input_map); - } - return null; + let item; + if (item_text.slice(0, 3) == "CI-") { + item = getCustomFromHash(item_text); + } else if (item_text.slice(0, 3) == "CR-") { + item = getCraftFromHash(item_text); + } else if (itemMap.has(item_text)) { + item = new Item(itemMap.get(item_text)); + } else if (tomeMap.has(item_text)) { + item = new Item(tomeMap.get(item_text)); } + + if (item) { + if (powdering !== undefined) { + const max_slots = item.statMap.get("slots"); + item.statMap.set("powders", powdering.slice(0, max_slots)); + } + let type_match; + if (this.category == "weapon") { + type_match = item.statMap.get("category") == "weapon"; + } else { + type_match = + item.statMap.get("type") == this.none_item.statMap.get("type"); + } + if (type_match) { + if (item.statMap.get("category") == "armor") { + applyArmorPowders(item.statMap); + } else if (item.statMap.get("category") == "weapon") { + apply_weapon_powders(item.statMap); + } + return item; + } + } else if ( + this.none_item.statMap.get("category") === "weapon" && + item_text.startsWith("Morph-") + ) { + let replace_items = [ + "Morph-Stardust", + "Morph-Steel", + "Morph-Iron", + "Morph-Gold", + "Morph-Topaz", + "Morph-Emerald", + "Morph-Amethyst", + "Morph-Ruby", + item_text.substring(6), + ]; + + for (const [i, x] of zip2(equipment_inputs, replace_items)) { + setValue(i, x); + } + + for (const node of item_nodes) { + if (node !== this) { + // save a tiny bit of compute + calcSchedule(node, 10); + } + } + // Needed to push the weapon node's updates forward + return this.compute_func(input_map); + } + return null; + } } /** @@ -211,62 +267,85 @@ class ItemInputNode extends InputNode { * Signature: ItemInputDisplayNode(item: Item) => null */ class ItemInputDisplayNode extends ComputeNode { + constructor(name, eq, item_image) { + super(name); + this.input_field = document.getElementById(eq + "-choice"); + this.health_field = document.getElementById(eq + "-health"); + this.level_field = document.getElementById(eq + "-lv"); + this.powder_field = document.getElementById(eq + "-powder"); // possibly None + this.image = item_image; + this.fail_cb = true; + } - constructor(name, eq, item_image) { - super(name); - this.input_field = document.getElementById(eq+"-choice"); - this.health_field = document.getElementById(eq+"-health"); - this.level_field = document.getElementById(eq+"-lv"); - this.powder_field = document.getElementById(eq+"-powder"); // possibly None - this.image = item_image; - this.fail_cb = true; + compute_func(input_map) { + if (input_map.size !== 1) { + throw "ItemInputDisplayNode accepts exactly one input (item)"; + } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + this.input_field.classList.remove( + "text-light", + "is-invalid", + "Normal", + "Unique", + "Rare", + "Legendary", + "Fabled", + "Mythic", + "Set", + "Crafted", + "Custom" + ); + this.input_field.classList.add("text-light"); + this.image.classList.remove( + "Normal-shadow", + "Unique-shadow", + "Rare-shadow", + "Legendary-shadow", + "Fabled-shadow", + "Mythic-shadow", + "Set-shadow", + "Crafted-shadow", + "Custom-shadow" + ); + + if (this.health_field) { + // Doesn't exist for weapons. + this.health_field.textContent = "0"; + } + if (this.level_field) { + // Doesn't exist for tomes. + this.level_field.textContent = "0"; + } + if (!item) { + this.input_field.classList.add("is-invalid"); + return null; + } + if (this.powder_field && item.statMap.has("powders")) { + this.powder_field.placeholder = "powders"; } - compute_func(input_map) { - if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } - const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - - this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); - this.input_field.classList.add("text-light"); - this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); - - if (this.health_field) { - // Doesn't exist for weapons. - this.health_field.textContent = "0"; - } - if (this.level_field) { - // Doesn't exist for tomes. - this.level_field.textContent = "0"; - } - if (!item) { - this.input_field.classList.add("is-invalid"); - return null; - } - if (this.powder_field && item.statMap.has('powders')) { - this.powder_field.placeholder = "powders"; - } - - if (item.statMap.has('NONE')) { - return null; - } - - if (this.powder_field && item.statMap.has('powders')) { - this.powder_field.placeholder = item.statMap.get('slots') + ' slots'; - } - - const tier = item.statMap.get('tier'); - this.input_field.classList.add(tier); - if (this.health_field) { - // Doesn't exist for weapons. - this.health_field.textContent = item.statMap.get('hp'); - } - if (this.level_field) { - // Doesn't exist for tomes. - this.level_field.textContent = item.statMap.get('lvl'); - } - this.image.classList.add(tier + "-shadow"); - return null; + if (item.statMap.has("NONE")) { + return null; } + + if (this.powder_field && item.statMap.has("powders")) { + this.powder_field.placeholder = item.statMap.get("slots") + " slots"; + } + + const tier = item.statMap.get("tier"); + this.input_field.classList.add(tier); + if (this.health_field) { + // Doesn't exist for weapons. + this.health_field.textContent = item.statMap.get("hp"); + } + if (this.level_field) { + // Doesn't exist for tomes. + this.level_field.textContent = item.statMap.get("lvl"); + } + this.image.classList.add(tier + "-shadow"); + return null; + } } /** @@ -275,18 +354,20 @@ class ItemInputDisplayNode extends ComputeNode { * Signature: ItemDisplayNode(item: Item) => null */ class ItemDisplayNode extends ComputeNode { - constructor(name, target_elem) { - super(name); - this.target_elem = target_elem; - } + constructor(name, target_elem) { + super(name); + this.target_elem = target_elem; + } - compute_func(input_map) { - if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } - const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - - displayExpandedItem(item.statMap, this.target_elem); - collapse_element("#"+this.target_elem); + compute_func(input_map) { + if (input_map.size !== 1) { + throw "ItemInputDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + displayExpandedItem(item.statMap, this.target_elem); + collapse_element("#" + this.target_elem); + } } /** @@ -295,26 +376,30 @@ class ItemDisplayNode extends ComputeNode { * Signature: WeaponInputDisplayNode(item: Item) => null */ class WeaponInputDisplayNode extends ComputeNode { + constructor(name, image_field, dps_field) { + super(name); + this.image = image_field; + this.dps_field = dps_field; + } - constructor(name, image_field, dps_field) { - super(name); - this.image = image_field; - this.dps_field = dps_field; + compute_func(input_map) { + if (input_map.size !== 1) { + throw "WeaponDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - compute_func(input_map) { - if (input_map.size !== 1) { throw "WeaponDisplayNode accepts exactly one input (item)"; } - const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - - const type = item.statMap.get('type'); - this.image.setAttribute('src', '../media/items/new/generic-'+type+'.png'); - let dps = get_base_dps(item.statMap); - if (isNaN(dps)) { - dps = dps[1]; - if (isNaN(dps)) dps = 0; - } - this.dps_field.textContent = Math.round(dps); + const type = item.statMap.get("type"); + this.image.setAttribute( + "src", + "../media/items/new/generic-" + type + ".png" + ); + let dps = get_base_dps(item.statMap); + if (isNaN(dps)) { + dps = dps[1]; + if (isNaN(dps)) dps = 0; } + this.dps_field.textContent = Math.round(dps); + } } /** @@ -328,31 +413,33 @@ class WeaponInputDisplayNode extends ComputeNode { * weapon-powder: List[powder]) => str */ class BuildEncodeNode extends ComputeNode { - constructor() { super("builder-encode"); } + constructor() { + super("builder-encode"); + } - compute_func(input_map) { - const build = input_map.get('build'); - const atree = input_map.get('atree'); - const atree_state = input_map.get('atree-state'); - let powders = [ - input_map.get('helmet-powder'), - input_map.get('chestplate-powder'), - input_map.get('leggings-powder'), - input_map.get('boots-powder'), - input_map.get('weapon-powder') - ]; - const skillpoints = [ - input_map.get('str'), - input_map.get('dex'), - input_map.get('int'), - input_map.get('def'), - input_map.get('agi') - ]; - // TODO: grr global state for copy button.. - player_build = build; - build_powders = powders; - return encodeBuild(build, powders, skillpoints, atree, atree_state); - } + compute_func(input_map) { + const build = input_map.get("build"); + const atree = input_map.get("atree"); + const atree_state = input_map.get("atree-state"); + let powders = [ + input_map.get("helmet-powder"), + input_map.get("chestplate-powder"), + input_map.get("leggings-powder"), + input_map.get("boots-powder"), + input_map.get("weapon-powder"), + ]; + const skillpoints = [ + input_map.get("str"), + input_map.get("dex"), + input_map.get("int"), + input_map.get("def"), + input_map.get("agi"), + ]; + // TODO: grr global state for copy button.. + player_build = build; + build_powders = powders; + return encodeBuild(build, powders, skillpoints, atree, atree_state); + } } /** @@ -361,13 +448,17 @@ class BuildEncodeNode extends ComputeNode { * Signature: URLUpdateNode(build_str: str) => null */ class URLUpdateNode extends ComputeNode { - constructor() { super("builder-url-update"); } + constructor() { + super("builder-url-update"); + } - compute_func(input_map) { - if (input_map.size !== 1) { throw "URLUpdateNode accepts exactly one input (build_str)"; } - const [build_str] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - location.hash = build_str; + compute_func(input_map) { + if (input_map.size !== 1) { + throw "URLUpdateNode accepts exactly one input (build_str)"; } + const [build_str] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + location.hash = build_str; + } } /** @@ -386,51 +477,57 @@ class URLUpdateNode extends ComputeNode { * level-input: int) => Build | null */ class BuildAssembleNode extends ComputeNode { - constructor() { super("builder-make-build"); } + constructor() { + super("builder-make-build"); + } - compute_func(input_map) { - let equipments = [ - input_map.get('helmet-input'), - input_map.get('chestplate-input'), - input_map.get('leggings-input'), - input_map.get('boots-input'), - input_map.get('ring1-input'), - input_map.get('ring2-input'), - input_map.get('bracelet-input'), - input_map.get('necklace-input'), - input_map.get('weaponTome1-input'), - input_map.get('weaponTome2-input'), - input_map.get('armorTome1-input'), - input_map.get('armorTome2-input'), - input_map.get('armorTome3-input'), - input_map.get('armorTome4-input'), - input_map.get('guildTome1-input') - ]; - let weapon = input_map.get('weapon-input'); - let level = parseInt(input_map.get('level-input')); - if (isNaN(level)) { - level = 106; - } - - let all_none = weapon.statMap.has('NONE'); - for (const item of equipments) { - all_none = all_none && item.statMap.has('NONE'); - } - if (all_none && !location.hash) { - return null; - } - return new Build(level, equipments, weapon); + compute_func(input_map) { + let equipments = [ + input_map.get("helmet-input"), + input_map.get("chestplate-input"), + input_map.get("leggings-input"), + input_map.get("boots-input"), + input_map.get("ring1-input"), + input_map.get("ring2-input"), + input_map.get("bracelet-input"), + input_map.get("necklace-input"), + input_map.get("weaponTome1-input"), + input_map.get("weaponTome2-input"), + input_map.get("armorTome1-input"), + input_map.get("armorTome2-input"), + input_map.get("armorTome3-input"), + input_map.get("armorTome4-input"), + input_map.get("guildTome1-input"), + ]; + let weapon = input_map.get("weapon-input"); + let level = parseInt(input_map.get("level-input")); + if (isNaN(level)) { + level = 106; } + + let all_none = weapon.statMap.has("NONE"); + for (const item of equipments) { + all_none = all_none && item.statMap.has("NONE"); + } + if (all_none && !location.hash) { + return null; + } + return new Build(level, equipments, weapon); + } } class PlayerClassNode extends ValueCheckComputeNode { - constructor(name) { super(name); } + constructor(name) { + super(name); + } - compute_func(input_map) { - if (input_map.size !== 1) { throw "PlayerClassNode accepts exactly one input (build)"; } - const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - return wep_to_class.get(build.weapon.statMap.get('type')); + compute_func(input_map) { + if (input_map.size !== 1) { + throw "PlayerClassNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + return wep_to_class.get(build.weapon.statMap.get("type")); + } } /** @@ -440,43 +537,48 @@ class PlayerClassNode extends ValueCheckComputeNode { * Signature: PowderInputNode() => List[powder] | null */ class PowderInputNode extends InputNode { + constructor(name, input_field) { + super(name, input_field); + } - constructor(name, input_field) { super(name, input_field); } - - compute_func(input_map) { - // TODO: haha improve efficiency to O(n) dumb - let input = this.input_field.value.trim(); - let powdering = []; - let errorederrors = []; - while (input) { - let first = input.slice(0, 2); - let powder = powderIDs.get(first); - if (powder === undefined) { - if (first.length > 0) { - errorederrors.push(first); - } else { - break; - } - } else { - powdering.push(powder); - } - input = input.slice(2); - } - - if (this.input_field.getAttribute("placeholder") != null) { - let placeholder = this.input_field.getAttribute("placeholder"); - if (placeholder.includes(" slots") && parseInt(placeholder[0]) < powdering.length) - errorederrors.push("Too many powders: " + powdering.length); - } - - if (errorederrors.length) { - this.input_field.classList.add("is-invalid"); + compute_func(input_map) { + // TODO: haha improve efficiency to O(n) dumb + let input = this.input_field.value.trim(); + let powdering = []; + let errorederrors = []; + while (input) { + let first = input.slice(0, 2); + let powder = powderIDs.get(first); + if (powder === undefined) { + if (first.length > 0) { + errorederrors.push(first); } else { - this.input_field.classList.remove("is-invalid"); + break; } - - return powdering; + } else { + powdering.push(powder); + } + input = input.slice(2); } + + if (this.input_field.getAttribute("placeholder") != null) { + let placeholder = this.input_field.getAttribute("placeholder"); + if ( + placeholder.includes(" slots") && + parseInt(placeholder.substring(0, placeholder.indexOf(" "))) < + powdering.length + ) + errorederrors.push("Too many powders: " + powdering.length); + } + + if (errorederrors.length) { + this.input_field.classList.add("is-invalid"); + } else { + this.input_field.classList.remove("is-invalid"); + } + + return powdering; + } } /** @@ -487,62 +589,68 @@ class PowderInputNode extends InputNode { * Signature: SpellSelectNode(build: Build) => [Spell, SpellParts] */ class SpellSelectNode extends ComputeNode { - constructor(spell) { - super("builder-spell"+spell.base_spell+"-select"); - this.spell = spell; - } + constructor(spell) { + super("builder-spell" + spell.base_spell + "-select"); + this.spell = spell; + } - compute_func(input_map) { - const build = input_map.get('build'); - let stats = build.statMap; - // TODO: apply major ids... DOOM..... + compute_func(input_map) { + const build = input_map.get("build"); + let stats = build.statMap; + // TODO: apply major ids... DOOM..... - return [this.spell, this.spell.parts]; - } + return [this.spell, this.spell.parts]; + } } /* * Get all defensive stats for this build. */ function getDefenseStats(stats) { - let defenseStats = []; - let def_pct = skillPointsToPercentage(stats.get('def')) * skillpoint_final_mult[3]; - let agi_pct = skillPointsToPercentage(stats.get('agi')) * skillpoint_final_mult[4]; - //total hp - let totalHp = stats.get("hp") + stats.get("hpBonus"); - if (totalHp < 5) totalHp = 5; - defenseStats.push(totalHp); - //EHP - let ehp = [totalHp, totalHp]; - let defMult = (2 - stats.get("classDef")); - for (const [k, v] of stats.get("defMult").entries()) { - defMult *= (1 - v/100); - } - // newehp = oldehp / [0.1 * A(x) + (1 - A(x)) * (1 - D(x))] - ehp[0] = ehp[0] / (0.1*agi_pct + (1-agi_pct) * (1-def_pct)); - ehp[0] /= defMult; - // ehp[0] /= (1-def_pct)*(1-agi_pct)*defMult; - ehp[1] /= (1-def_pct)*defMult; - defenseStats.push(ehp); - //HPR - let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); - defenseStats.push(totalHpr); - //EHPR - let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= (1-def_pct)*(1-agi_pct)*defMult; - ehpr[1] /= (1-def_pct)*defMult; - defenseStats.push(ehpr); - //skp stats - defenseStats.push([ def_pct*100, agi_pct*100]); - //eledefs - TODO POWDERS - let eledefs = [0, 0, 0, 0, 0]; - for(const i in skp_elements){ //kinda jank but ok - eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.); - } - defenseStats.push(eledefs); - - //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] - return defenseStats; + let defenseStats = []; + let def_pct = + skillPointsToPercentage(stats.get("def")) * skillpoint_final_mult[3]; + let agi_pct = + skillPointsToPercentage(stats.get("agi")) * skillpoint_final_mult[4]; + //total hp + let totalHp = stats.get("hp") + stats.get("hpBonus"); + if (totalHp < 5) totalHp = 5; + defenseStats.push(totalHp); + //EHP + let ehp = [totalHp, totalHp]; + let defMult = 2 - stats.get("classDef"); + for (const [k, v] of stats.get("defMult").entries()) { + defMult *= 1 - v / 100; + } + // newehp = oldehp / [0.1 * A(x) + (1 - A(x)) * (1 - D(x))] + ehp[0] = ehp[0] / (0.1 * agi_pct + (1 - agi_pct) * (1 - def_pct)); + ehp[0] /= defMult; + // ehp[0] /= (1-def_pct)*(1-agi_pct)*defMult; + ehp[1] /= (1 - def_pct) * defMult; + defenseStats.push(ehp); + //HPR + let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct") / 100); + defenseStats.push(totalHpr); + //EHPR + let ehpr = [totalHpr, totalHpr]; + ehpr[0] /= (1 - def_pct) * (1 - agi_pct) * defMult; + ehpr[1] /= (1 - def_pct) * defMult; + defenseStats.push(ehpr); + //skp stats + defenseStats.push([def_pct * 100, agi_pct * 100]); + //eledefs - TODO POWDERS + let eledefs = [0, 0, 0, 0, 0]; + for (const i in skp_elements) { + //kinda jank but ok + eledefs[i] = rawToPct( + stats.get(skp_elements[i] + "Def"), + stats.get(skp_elements[i] + "DefPct") / 100 + ); + } + defenseStats.push(eledefs); + + //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] + return defenseStats; } /** @@ -554,101 +662,115 @@ function getDefenseStats(stats) { * spell-info: [Spell, SpellParts]) => List[SpellDamage] */ class SpellDamageCalcNode extends ComputeNode { - constructor(spell_num) { - super("builder-spell"+spell_num+"-calc"); - } + constructor(spell_num) { + super("builder-spell" + spell_num + "-calc"); + } - 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 skillpoints = [ - stats.get('str'), - stats.get('dex'), - stats.get('int'), - stats.get('def'), - stats.get('agi') + 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 skillpoints = [ + stats.get("str"), + stats.get("dex"), + stats.get("int"), + stats.get("def"), + 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) { + let spell_result; + if ("multipliers" in part) { + // damage type spell + let results = calculateSpellDamage( + stats, + weapon, + part.multipliers, + use_spell, + !use_speed, + spell.base_spell + "." + part.name + ); + 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], + }; + } else if ("power" in part) { + // TODO: wynn2 formula + let _heal_amount = + part.power * getDefenseStats(stats)[0] * (stats.get("healPct") / 100); + spell_result = { + type: "heal", + heal_amount: _heal_amount, + }; + } else { + continue; + } + spell_result.name = part.name; + spell_results.push(spell_result); + spell_result_map.set(part.name, spell_result); + } + for (const part of spell_parts) { + if ("hits" in part) { + let 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], + heal_amount: 0, + }; + const dam_res_keys = [ + "normal_min", + "normal_max", + "normal_total", + "crit_min", + "crit_max", + "crit_total", ]; - 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) { - let spell_result; - if ('multipliers' in part) { // damage type spell - let results = calculateSpellDamage(stats, weapon, part.multipliers, use_spell, !use_speed, spell.base_spell + '.' + part.name); - 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], - } - } else if ('power' in part) { - // TODO: wynn2 formula - let _heal_amount = (part.power * getDefenseStats(stats)[0] * (stats.get('healPct')/100)); - spell_result = { - type: "heal", - heal_amount: _heal_amount - } + for (const [subpart_name, hits] of Object.entries(part.hits)) { + const subpart = spell_result_map.get(subpart_name); + if (!subpart) { + continue; + } + if (spell_result.type) { + if (subpart.type !== spell_result.type) { + throw "SpellCalc total subpart type mismatch"; } - else { - continue; + } 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; + } } - spell_result.name = part.name; - spell_results.push(spell_result); - spell_result_map.set(part.name, spell_result); + } else { + spell_result.heal_amount += subpart.heal_amount; + } } - for (const part of spell_parts) { - if ('hits' in part) { - let 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], - heal_amount: 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 (!subpart) { continue; } - 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; + spell_result.name = part.name; + spell_results.push(spell_result); + spell_result_map.set(part.name, spell_result); + } } + return spell_results; + } } - /** * Display spell damage from spell parts. * Currently kinda janky / TODO while we rework the internal rep. of spells. @@ -658,22 +780,29 @@ class SpellDamageCalcNode extends ComputeNode { * spell-damage: List[SpellDamage]) => null */ class SpellDisplayNode extends ComputeNode { - constructor(spell_num) { - super("builder-spell"+spell_num+"-display"); - this.spell_idx = spell_num; - } + constructor(spell_num) { + super("builder-spell" + spell_num + "-display"); + this.spell_idx = spell_num; + } - compute_func(input_map) { - const stats = input_map.get('stats'); - const spell_info = input_map.get('spell-info'); - const damages = input_map.get('spell-damage'); - const spell = spell_info[0]; + compute_func(input_map) { + const stats = input_map.get("stats"); + const spell_info = input_map.get("spell-info"); + const damages = input_map.get("spell-damage"); + const spell = spell_info[0]; - 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, damages); - } + 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, + damages + ); + } } /** @@ -682,20 +811,35 @@ class SpellDisplayNode extends ComputeNode { * Signature: BuildDisplayNode(build: Build) => null */ class BuildDisplayNode extends ComputeNode { - constructor() { super("builder-stats-display"); } + constructor() { + super("builder-stats-display"); + } - compute_func(input_map) { - const build = input_map.get('build'); - const stats = input_map.get('stats'); - displayBuildStats('overall-stats', build, build_all_display_commands, stats); - displayBuildStats("offensive-stats", build, build_offensive_display_commands, stats); - displaySetBonuses("set-info", build); - // TODO: move weapon out? - displayDefenseStats(document.getElementById("defensive-stats"), stats); + compute_func(input_map) { + const build = input_map.get("build"); + const stats = input_map.get("stats"); + displayBuildStats( + "overall-stats", + build, + build_all_display_commands, + stats + ); + displayBuildStats( + "offensive-stats", + build, + build_offensive_display_commands, + stats + ); + displaySetBonuses("set-info", build); + // TODO: move weapon out? + displayDefenseStats(document.getElementById("defensive-stats"), stats); - displayPoisonDamage(document.getElementById("build-poison-stats"), build); - displayEquipOrder(document.getElementById("build-order"), build.equip_order); - } + displayPoisonDamage(document.getElementById("build-poison-stats"), build); + displayEquipOrder( + document.getElementById("build-order"), + build.equip_order + ); + } } /** @@ -705,103 +849,150 @@ class BuildDisplayNode extends ComputeNode { * Signature: DisplayBuildWarningNode(build: Build, str: int, dex: int, int: int, def: int, agi: int) => null */ class DisplayBuildWarningsNode extends ComputeNode { - constructor() { super("builder-show-warnings"); } + constructor() { + super("builder-show-warnings"); + } - compute_func(input_map) { - const build = input_map.get('build'); - const min_assigned = build.base_skillpoints; - const base_totals = build.total_skillpoints; - const skillpoints = [ - input_map.get('str'), - input_map.get('dex'), - input_map.get('int'), - input_map.get('def'), - input_map.get('agi') - ]; - let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."]; - let total_assigned = 0; - for (let i in skp_order){ //big bren - const assigned = skillpoints[i] - base_totals[i] + min_assigned[i] - setText(skp_order[i] + "-skp-base", "Original: " + base_totals[i]); - setText(skp_order[i] + "-skp-assign", "Assign: " + assigned); - setValue(skp_order[i] + "-skp", skillpoints[i]); - let linebreak = document.createElement("br"); - linebreak.classList.add("itemp"); - setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100*skillpoint_final_mult[i]).toFixed(1).concat(skp_effects[i])); - document.getElementById(skp_order[i]+"-warnings").textContent = '' - if (assigned > 100) { - let skp_warning = document.createElement("p"); - skp_warning.classList.add("warning", "small-text"); - skp_warning.textContent += "Cannot assign " + assigned + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; - document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning); - } - total_assigned += assigned; - } - - let summarybox = document.getElementById("summary-box"); - summarybox.textContent = ""; - let skpRow = document.createElement("p"); - - let remainingSkp = document.createElement("p"); - remainingSkp.classList.add("scaled-font"); - let remainingSkpTitle = document.createElement("b"); - remainingSkpTitle.textContent = "Assigned " + total_assigned + " skillpoints. Remaining skillpoints: "; - let remainingSkpContent = document.createElement("b"); - remainingSkpContent.textContent = "" + (levelToSkillPoints(build.level) - total_assigned); - remainingSkpContent.classList.add(levelToSkillPoints(build.level) - total_assigned < 0 ? "negative" : "positive"); - - remainingSkp.appendChild(remainingSkpTitle); - remainingSkp.appendChild(remainingSkpContent); - - summarybox.append(skpRow); - summarybox.append(remainingSkp); - if(total_assigned > levelToSkillPoints(build.level)){ - let skpWarning = document.createElement("span"); - //skpWarning.classList.add("itemp"); - skpWarning.classList.add("warning"); - skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!"; - let skpCount = document.createElement("p"); - skpCount.classList.add("warning"); - skpCount.textContent = "For level " + (build.level>101 ? "101+" : build.level) + ", there are only " + levelToSkillPoints(build.level) + " skill points available."; - summarybox.append(skpWarning); - summarybox.append(skpCount); - } - let lvlWarning; - for (const item of build.items) { - let item_lvl; - if (item.statMap.get("crafted")) { - //item_lvl = item.get("lvlLow") + "-" + item.get("lvl"); - item_lvl = item.statMap.get("lvlLow"); - } - else { - item_lvl = item.statMap.get("lvl"); - } - - if (build.level < item_lvl) { - if (!lvlWarning) { - lvlWarning = document.createElement("p"); - lvlWarning.classList.add("itemp"); lvlWarning.classList.add("warning"); - lvlWarning.textContent = "WARNING: A level " + build.level + " player cannot use some piece(s) of this build." - } - let baditem = document.createElement("p"); - baditem.classList.add("nocolor"); baditem.classList.add("itemp"); - baditem.textContent = item.statMap.get("displayName") + " requires level " + item_lvl + " to use."; - lvlWarning.appendChild(baditem); - } - } - if(lvlWarning){ - summarybox.append(lvlWarning); - } - for (const [setName, count] of build.activeSetCounts) { - const bonus = sets.get(setName).bonuses[count-1]; - if (bonus["illegal"]) { - let setWarning = document.createElement("p"); - setWarning.classList.add("itemp"); setWarning.classList.add("warning"); - setWarning.textContent = "WARNING: illegal item combination: " + setName - summarybox.append(setWarning); - } - } + compute_func(input_map) { + const build = input_map.get("build"); + const min_assigned = build.base_skillpoints; + const base_totals = build.total_skillpoints; + const skillpoints = [ + input_map.get("str"), + input_map.get("dex"), + input_map.get("int"), + input_map.get("def"), + input_map.get("agi"), + ]; + let skp_effects = [ + "% more damage dealt.", + "% chance to crit.", + "% spell cost reduction.", + "% less damage taken.", + "% chance to dodge.", + ]; + let total_assigned = 0; + for (let i in skp_order) { + //big bren + const assigned = skillpoints[i] - base_totals[i] + min_assigned[i]; + setText(skp_order[i] + "-skp-base", "Original: " + base_totals[i]); + setText(skp_order[i] + "-skp-assign", "Assign: " + assigned); + setValue(skp_order[i] + "-skp", skillpoints[i]); + let linebreak = document.createElement("br"); + linebreak.classList.add("itemp"); + setText( + skp_order[i] + "-skp-pct", + ( + skillPointsToPercentage(skillpoints[i]) * + 100 * + skillpoint_final_mult[i] + ) + .toFixed(1) + .concat(skp_effects[i]) + ); + document.getElementById(skp_order[i] + "-warnings").textContent = ""; + if (assigned > 100) { + let skp_warning = document.createElement("p"); + skp_warning.classList.add("warning", "small-text"); + skp_warning.textContent += + "Cannot assign " + + assigned + + " skillpoints in " + + ["Strength", "Dexterity", "Intelligence", "Defense", "Agility"][i] + + " manually."; + document + .getElementById(skp_order[i] + "-warnings") + .appendChild(skp_warning); + } + total_assigned += assigned; } + + let summarybox = document.getElementById("summary-box"); + summarybox.textContent = ""; + let skpRow = document.createElement("p"); + + let remainingSkp = document.createElement("p"); + remainingSkp.classList.add("scaled-font"); + let remainingSkpTitle = document.createElement("b"); + remainingSkpTitle.textContent = + "Assigned " + total_assigned + " skillpoints. Remaining skillpoints: "; + let remainingSkpContent = document.createElement("b"); + remainingSkpContent.textContent = + "" + (levelToSkillPoints(build.level) - total_assigned); + remainingSkpContent.classList.add( + levelToSkillPoints(build.level) - total_assigned < 0 + ? "negative" + : "positive" + ); + + remainingSkp.appendChild(remainingSkpTitle); + remainingSkp.appendChild(remainingSkpContent); + + summarybox.append(skpRow); + summarybox.append(remainingSkp); + if (total_assigned > levelToSkillPoints(build.level)) { + let skpWarning = document.createElement("span"); + //skpWarning.classList.add("itemp"); + skpWarning.classList.add("warning"); + skpWarning.textContent = + "WARNING: Too many skillpoints need to be assigned!"; + let skpCount = document.createElement("p"); + skpCount.classList.add("warning"); + skpCount.textContent = + "For level " + + (build.level > 101 ? "101+" : build.level) + + ", there are only " + + levelToSkillPoints(build.level) + + " skill points available."; + summarybox.append(skpWarning); + summarybox.append(skpCount); + } + let lvlWarning; + for (const item of build.items) { + let item_lvl; + if (item.statMap.get("crafted")) { + //item_lvl = item.get("lvlLow") + "-" + item.get("lvl"); + item_lvl = item.statMap.get("lvlLow"); + } else { + item_lvl = item.statMap.get("lvl"); + } + + if (build.level < item_lvl) { + if (!lvlWarning) { + lvlWarning = document.createElement("p"); + lvlWarning.classList.add("itemp"); + lvlWarning.classList.add("warning"); + lvlWarning.textContent = + "WARNING: A level " + + build.level + + " player cannot use some piece(s) of this build."; + } + let baditem = document.createElement("p"); + baditem.classList.add("nocolor"); + baditem.classList.add("itemp"); + baditem.textContent = + item.statMap.get("displayName") + + " requires level " + + item_lvl + + " to use."; + lvlWarning.appendChild(baditem); + } + } + if (lvlWarning) { + summarybox.append(lvlWarning); + } + for (const [setName, count] of build.activeSetCounts) { + const bonus = sets.get(setName).bonuses[count - 1]; + if (bonus["illegal"]) { + let setWarning = document.createElement("p"); + setWarning.classList.add("itemp"); + setWarning.classList.add("warning"); + setWarning.textContent = + "WARNING: illegal item combination: " + setName; + summarybox.append(setWarning); + } + } + } } /** @@ -810,17 +1001,19 @@ class DisplayBuildWarningsNode extends ComputeNode { * Signature: AggregateStatsNode(*args) => StatMap */ class AggregateStatsNode extends ComputeNode { - constructor() { super("builder-aggregate-stats"); } + constructor() { + super("builder-aggregate-stats"); + } - compute_func(input_map) { - const output_stats = new Map(); - for (const [k, v] of input_map.entries()) { - for (const [k2, v2] of v.entries()) { - merge_stat(output_stats, k2, v2); - } - } - return output_stats; + compute_func(input_map) { + const output_stats = new Map(); + for (const [k, v] of input_map.entries()) { + for (const [k2, v2] of v.entries()) { + merge_stat(output_stats, k2, v2); + } } + return output_stats; + } } /** @@ -829,24 +1022,30 @@ class AggregateStatsNode extends ComputeNode { * Signature: AggregateEditableIDNode(build: Build, weapon: Item, *args) => StatMap */ class AggregateEditableIDNode extends ComputeNode { - constructor() { super("builder-aggregate-inputs"); } + constructor() { + super("builder-aggregate-inputs"); + } - compute_func(input_map) { - const build = input_map.get('build'); input_map.delete('build'); + compute_func(input_map) { + const build = input_map.get("build"); + input_map.delete("build"); - const output_stats = new Map(build.statMap); - for (const [k, v] of input_map.entries()) { - output_stats.set(k, v); - } - - output_stats.set('classDef', classDefenseMultipliers.get(build.weapon.statMap.get("type"))); - return output_stats; + const output_stats = new Map(build.statMap); + for (const [k, v] of input_map.entries()) { + output_stats.set(k, v); } + + output_stats.set( + "classDef", + classDefenseMultipliers.get(build.weapon.statMap.get("type")) + ); + return output_stats; + } } let edit_id_output; function resetEditableIDs() { - edit_id_output.notify(); + edit_id_output.notify(); } /** * Set the editble id fields. @@ -854,32 +1053,35 @@ function resetEditableIDs() { * Signature: EditableIDSetterNode(build: Build) => null */ class EditableIDSetterNode extends ComputeNode { - constructor(notify_nodes) { - super("builder-id-setter"); - this.notify_nodes = notify_nodes.slice(); - } + constructor(notify_nodes) { + super("builder-id-setter"); + this.notify_nodes = notify_nodes.slice(); + } - compute_func(input_map) { - if (input_map.size !== 1) { throw "EditableIDSetterNode accepts exactly one input (build)"; } - const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - for (const id of editable_item_fields) { - const val = build.statMap.get(id); - document.getElementById(id).value = val; - document.getElementById(id+'-base').textContent = 'Original Value: ' + val; - } + compute_func(input_map) { + if (input_map.size !== 1) { + throw "EditableIDSetterNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + for (const id of editable_item_fields) { + const val = build.statMap.get(id); + document.getElementById(id).value = val; + document.getElementById(id + "-base").textContent = + "Original Value: " + val; + } + } - notify() { - this.mark_dirty(); - this.update(); - // NOTE: DO NOT merge these loops for performance reasons!!! - for (const node of this.notify_nodes) { - node.mark_dirty(); - } - for (const node of this.notify_nodes) { - node.update(); - } + notify() { + this.mark_dirty(); + this.update(); + // NOTE: DO NOT merge these loops for performance reasons!!! + for (const node of this.notify_nodes) { + node.mark_dirty(); } + for (const node of this.notify_nodes) { + node.update(); + } + } } /** @@ -889,25 +1091,28 @@ class EditableIDSetterNode extends ComputeNode { * Signature: SkillPointSetterNode(build: Build) => null */ class SkillPointSetterNode extends ComputeNode { - constructor(notify_nodes) { - super("builder-skillpoint-setter"); - this.notify_nodes = notify_nodes.slice(); - } + constructor(notify_nodes) { + super("builder-skillpoint-setter"); + this.notify_nodes = notify_nodes.slice(); + } - compute_func(input_map) { - if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; } - const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - for (const [idx, elem] of skp_order.entries()) { - document.getElementById(elem+'-skp').value = build.total_skillpoints[idx]; - } - // NOTE: DO NOT merge these loops for performance reasons!!! - for (const node of this.notify_nodes) { - node.mark_dirty(); - } - for (const node of this.notify_nodes) { - node.update(); - } + compute_func(input_map) { + if (input_map.size !== 1) { + throw "SkillPointSetterNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + for (const [idx, elem] of skp_order.entries()) { + document.getElementById(elem + "-skp").value = + build.total_skillpoints[idx]; + } + // NOTE: DO NOT merge these loops for performance reasons!!! + for (const node of this.notify_nodes) { + node.mark_dirty(); + } + for (const node of this.notify_nodes) { + node.update(); + } + } } /** @@ -916,28 +1121,30 @@ class SkillPointSetterNode extends ComputeNode { * Signature: SumNumberInputNode() => int */ class SumNumberInputNode extends InputNode { - compute_func(input_map) { - let value = this.input_field.value; - if (value === "") { value = "0"; } - - let input_num = 0; - if (value.includes("+")) { - let skp = value.split("+"); - for (const s of skp) { - const val = parseInt(s,10); - if (isNaN(val)) { - return null; - } - input_num += val; - } - } else { - input_num = parseInt(value,10); - if (isNaN(input_num)) { - return null; - } - } - return input_num; + compute_func(input_map) { + let value = this.input_field.value; + if (value === "") { + value = "0"; } + + let input_num = 0; + if (value.includes("+")) { + let skp = value.split("+"); + for (const s of skp) { + const val = parseInt(s, 10); + if (isNaN(val)) { + return null; + } + input_num += val; + } + } else { + input_num = parseInt(value, 10); + if (isNaN(input_num)) { + return null; + } + } + return input_num; + } } let item_nodes = []; @@ -950,166 +1157,198 @@ let edit_agg_node; let atree_graph_creator; function builder_graph_init() { - // Phase 1/3: Set up item input, propagate updates, etc. + // Phase 1/3: Set up item input, propagate updates, etc. - // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). - for (const [eq, display_elem, none_item] of zip3(equipment_fields, build_fields, none_items)) { - let input_field = document.getElementById(eq+"-choice"); - let item_image = document.getElementById(eq+"-img"); + // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). + for (const [eq, display_elem, none_item] of zip3( + equipment_fields, + build_fields, + none_items + )) { + let input_field = document.getElementById(eq + "-choice"); + let item_image = document.getElementById(eq + "-img"); - let item_input = new ItemInputNode(eq+'-input', input_field, none_item); - item_nodes.push(item_input); - new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input); - new ItemDisplayNode(eq+'-item-display', display_elem).link_to(item_input); - //new PrintNode(eq+'-debug').link_to(item_input); - //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); + let item_input = new ItemInputNode(eq + "-input", input_field, none_item); + item_nodes.push(item_input); + new ItemInputDisplayNode(eq + "-input-display", eq, item_image).link_to( + item_input + ); + new ItemDisplayNode(eq + "-item-display", display_elem).link_to(item_input); + //new PrintNode(eq+'-debug').link_to(item_input); + //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); + } + for (const [eq, none_item] of zip2(tome_fields, [ + none_tomes[0], + none_tomes[0], + none_tomes[1], + none_tomes[1], + none_tomes[1], + none_tomes[1], + none_tomes[2], + ])) { + let input_field = document.getElementById(eq + "-choice"); + let item_image = document.getElementById(eq + "-img"); + + let item_input = new ItemInputNode(eq + "-input", input_field, none_item); + item_nodes.push(item_input); + new ItemInputDisplayNode(eq + "-input-display", eq, item_image).link_to( + item_input + ); + } + + // weapon image changer node. + let weapon_image = document.getElementById("weapon-img"); + let weapon_dps = document.getElementById("weapon-dps"); + new WeaponInputDisplayNode("weapon-type", weapon_image, weapon_dps).link_to( + item_nodes[8] + ); + + // Level input node. + let level_input = new InputNode( + "level-input", + document.getElementById("level-choice") + ); + + // linking to atree verification + atree_validate.link_to(level_input, "level"); + + // "Build" now only refers to equipment and level (no powders). Powders are injected before damage calculation / stat display. + build_node = new BuildAssembleNode(); + for (const input of item_nodes) { + build_node.link_to(input); + } + build_node.link_to(level_input); + + let build_encode_node = new BuildEncodeNode(); + build_encode_node.link_to(build_node, "build"); + + let url_update_node = new URLUpdateNode(); + url_update_node.link_to(build_encode_node, "build-str"); + + for (const input of powder_inputs) { + let powder_node = new PowderInputNode( + input, + document.getElementById(input) + ); + powder_nodes.push(powder_node); + build_encode_node.link_to(powder_node, input); + } + + item_nodes[0].link_to(powder_nodes[0], "powdering"); + item_nodes[1].link_to(powder_nodes[1], "powdering"); + item_nodes[2].link_to(powder_nodes[2], "powdering"); + item_nodes[3].link_to(powder_nodes[3], "powdering"); + item_nodes[8].link_to(powder_nodes[4], "powdering"); + + // Phase 2/3: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage + + // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) + stat_agg_node = new AggregateStatsNode(); + edit_agg_node = new AggregateEditableIDNode(); + edit_agg_node.link_to(build_node, "build"); + for (const field of editable_item_fields) { + // Create nodes that listens to each editable id input, the node name should match the "id" + const elem = document.getElementById(field); + const node = new SumNumberInputNode("builder-" + field + "-input", elem); + + edit_agg_node.link_to(node, field); + edit_input_nodes.push(node); + } + // Edit IDs setter declared up here to set ids so they will be populated by default. + edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list. + edit_id_output.link_to(build_node); + + for (const skp of skp_order) { + const elem = document.getElementById(skp + "-skp"); + const node = new SumNumberInputNode("builder-" + skp + "-input", elem); + + edit_agg_node.link_to(node, skp); + build_encode_node.link_to(node, skp); + edit_input_nodes.push(node); + skp_inputs.push(node); + } + stat_agg_node.link_to(edit_agg_node); + + // Phase 3/3: Set up atree stuff. + + let class_node = new PlayerClassNode("builder-class").link_to(build_node); + // These two are defined in `atree.js` + atree_node.link_to(class_node, "player-class"); + atree_merge.link_to(class_node, "player-class"); + atree_stats.link_to(build_node, "build"); + stat_agg_node.link_to(atree_stats, "atree-stats"); + + build_encode_node + .link_to(atree_node, "atree") + .link_to(atree_state_node, "atree-state"); + + // --------------------------------------------------------------- + // Trigger the update cascade for build! + // --------------------------------------------------------------- + for (const input_node of item_nodes.concat(powder_nodes)) { + input_node.update(); + } + armor_powder_node.update(); + level_input.update(); + + atree_graph_creator = new AbilityTreeEnsureNodesNode( + build_node, + stat_agg_node + ).link_to(atree_collect_spells, "spells"); + + // kinda janky, manually set atree and update. Some wasted compute here + if (atree_data !== null && atree_node.value !== null) { + // janky check if atree is valid + const atree_state = atree_state_node.value; + if (atree_data.length > 0) { + const active_nodes = decode_atree(atree_node.value, atree_data); + for (const node of active_nodes) { + atree_set_state(atree_state.get(node.ability.id), true); + } + atree_state_node.mark_dirty().update(); } - for (const [eq, none_item] of zip2(tome_fields, [none_tomes[0], none_tomes[0], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[2]])) { - let input_field = document.getElementById(eq+"-choice"); - let item_image = document.getElementById(eq+"-img"); + } - let item_input = new ItemInputNode(eq+'-input', input_field, none_item); - item_nodes.push(item_input); - new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input); - } + // Powder specials. + let powder_special_calc = new PowderSpecialCalcNode().link_to( + powder_special_input, + "powder-specials" + ); + new PowderSpecialDisplayNode() + .link_to(powder_special_input, "powder-specials") + .link_to(stat_agg_node, "stats") + .link_to(build_node, "build"); + stat_agg_node.link_to(powder_special_calc, "powder-boost"); + stat_agg_node.link_to(armor_powder_node, "armor-powder"); + powder_special_input.update(); - // weapon image changer node. - let weapon_image = document.getElementById("weapon-img"); - let weapon_dps = document.getElementById("weapon-dps"); - new WeaponInputDisplayNode('weapon-type', weapon_image, weapon_dps).link_to(item_nodes[8]); + // Potion boost. + stat_agg_node.link_to(boosts_node, "potion-boost"); - // Level input node. - let level_input = new InputNode('level-input', document.getElementById('level-choice')); - - // linking to atree verification - atree_validate.link_to(level_input, 'level'); + // Also do something similar for skill points - // "Build" now only refers to equipment and level (no powders). Powders are injected before damage calculation / stat display. - build_node = new BuildAssembleNode(); - for (const input of item_nodes) { - build_node.link_to(input); - } - build_node.link_to(level_input); + let build_disp_node = new BuildDisplayNode(); + build_disp_node.link_to(build_node, "build"); + build_disp_node.link_to(stat_agg_node, "stats"); - let build_encode_node = new BuildEncodeNode(); - build_encode_node.link_to(build_node, 'build'); + for (const node of edit_input_nodes) { + node.update(); + } - let url_update_node = new URLUpdateNode(); - url_update_node.link_to(build_encode_node, 'build-str'); + let skp_output = new SkillPointSetterNode(edit_input_nodes); + skp_output.link_to(build_node); + let build_warnings_node = new DisplayBuildWarningsNode(); + build_warnings_node.link_to(build_node, "build"); + for (const [skp_input, skp] of zip2(skp_inputs, skp_order)) { + build_warnings_node.link_to(skp_input, skp); + } + build_warnings_node.update(); - for (const input of powder_inputs) { - let powder_node = new PowderInputNode(input, document.getElementById(input)); - powder_nodes.push(powder_node); - build_encode_node.link_to(powder_node, input); - } + // call node.update() for each skillpoint node and stat edit listener node manually + // NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them + // this will propagate the update to the `stat_agg_node`, and then to damage calc - item_nodes[0].link_to(powder_nodes[0], 'powdering'); - item_nodes[1].link_to(powder_nodes[1], 'powdering'); - item_nodes[2].link_to(powder_nodes[2], 'powdering'); - item_nodes[3].link_to(powder_nodes[3], 'powdering'); - item_nodes[8].link_to(powder_nodes[4], 'powdering'); - - // Phase 2/3: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage - - // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) - stat_agg_node = new AggregateStatsNode(); - edit_agg_node = new AggregateEditableIDNode(); - edit_agg_node.link_to(build_node, 'build'); - for (const field of editable_item_fields) { - // Create nodes that listens to each editable id input, the node name should match the "id" - const elem = document.getElementById(field); - const node = new SumNumberInputNode('builder-'+field+'-input', elem); - - edit_agg_node.link_to(node, field); - edit_input_nodes.push(node); - } - // Edit IDs setter declared up here to set ids so they will be populated by default. - edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list. - edit_id_output.link_to(build_node); - - for (const skp of skp_order) { - const elem = document.getElementById(skp+'-skp'); - const node = new SumNumberInputNode('builder-'+skp+'-input', elem); - - edit_agg_node.link_to(node, skp); - build_encode_node.link_to(node, skp); - edit_input_nodes.push(node); - skp_inputs.push(node); - } - stat_agg_node.link_to(edit_agg_node); - - // Phase 3/3: Set up atree stuff. - - let class_node = new PlayerClassNode('builder-class').link_to(build_node); - // These two are defined in `atree.js` - atree_node.link_to(class_node, 'player-class'); - atree_merge.link_to(class_node, 'player-class'); - atree_stats.link_to(build_node, 'build'); - stat_agg_node.link_to(atree_stats, 'atree-stats'); - - build_encode_node.link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state'); - - // --------------------------------------------------------------- - // Trigger the update cascade for build! - // --------------------------------------------------------------- - for (const input_node of item_nodes.concat(powder_nodes)) { - input_node.update(); - } - armor_powder_node.update(); - level_input.update(); - - atree_graph_creator = new AbilityTreeEnsureNodesNode(build_node, stat_agg_node) - .link_to(atree_collect_spells, 'spells'); - - // kinda janky, manually set atree and update. Some wasted compute here - if (atree_data !== null && atree_node.value !== null) { // janky check if atree is valid - const atree_state = atree_state_node.value; - if (atree_data.length > 0) { - const active_nodes = decode_atree(atree_node.value, atree_data); - for (const node of active_nodes) { - atree_set_state(atree_state.get(node.ability.id), true); - } - atree_state_node.mark_dirty().update(); - } - } - - // Powder specials. - let powder_special_calc = new PowderSpecialCalcNode().link_to(powder_special_input, 'powder-specials'); - new PowderSpecialDisplayNode().link_to(powder_special_input, 'powder-specials') - .link_to(stat_agg_node, 'stats').link_to(build_node, 'build'); - stat_agg_node.link_to(powder_special_calc, 'powder-boost'); - stat_agg_node.link_to(armor_powder_node, 'armor-powder'); - powder_special_input.update(); - - // Potion boost. - stat_agg_node.link_to(boosts_node, 'potion-boost'); - - // Also do something similar for skill points - - let build_disp_node = new BuildDisplayNode() - build_disp_node.link_to(build_node, 'build'); - build_disp_node.link_to(stat_agg_node, 'stats'); - - for (const node of edit_input_nodes) { - node.update(); - } - - let skp_output = new SkillPointSetterNode(edit_input_nodes); - skp_output.link_to(build_node); - - let build_warnings_node = new DisplayBuildWarningsNode(); - build_warnings_node.link_to(build_node, 'build'); - for (const [skp_input, skp] of zip2(skp_inputs, skp_order)) { - build_warnings_node.link_to(skp_input, skp); - } - build_warnings_node.update(); - - // call node.update() for each skillpoint node and stat edit listener node manually - // NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them - // this will propagate the update to the `stat_agg_node`, and then to damage calc - - console.log("Set up graph"); - graph_live_update = true; + console.log("Set up graph"); + graph_live_update = true; } -