diff --git a/builder/index.html b/builder/index.html index d7a802c..d9c3e8f 100644 --- a/builder/index.html +++ b/builder/index.html @@ -313,10 +313,10 @@
- +
- +
@@ -1399,8 +1399,9 @@ - + + diff --git a/js/build.js b/js/build.js index 84ff86a..447162d 100644 --- a/js/build.js +++ b/js/build.js @@ -100,238 +100,12 @@ class Build{ * @param {Number[]} powders : Powder application. List of lists of integers (powder IDs). * In order: boots, Chestplate, Leggings, Boots, Weapon. * @param {Object[]} inputerrors : List of instances of error-like classes. + * + * @param {Object[]} tomes: List of tomes. + * In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome. + * 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building) */ - constructor(level,equipment, powders, externalStats, inputerrors=[]){ - - let errors = inputerrors; - //this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem). - this.craftedItems = []; - this.customItems = []; - // NOTE: powders is just an array of arrays of powder IDs. Not powder objects. - this.powders = powders; - if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") { - const helmet = itemMap.get(equipment[0]); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - } else { - try { - let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined); - if (helmet.statMap.get("type") !== "helmet") { - throw new Error("Not a helmet"); - } - this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots")); - helmet.statMap.set("powders",this.powders[0].slice()); - this.helmet = helmet.statMap; - applyArmorPowders(this.helmet, this.powders[0]); - if (this.helmet.get("custom")) { - this.customItems.push(helmet); - } else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(helmet); - } - - } catch (Error) { - const helmet = itemMap.get("No Helmet"); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - errors.push(new ItemNotFound(equipment[0], "helmet", true)); - } - } - if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") { - const chestplate = itemMap.get(equipment[1]); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - } else { - try { - let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined); - if (chestplate.statMap.get("type") !== "chestplate") { - throw new Error("Not a chestplate"); - } - this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots")); - chestplate.statMap.set("powders",this.powders[1].slice()); - this.chestplate = chestplate.statMap; - applyArmorPowders(this.chestplate, this.powders[1]); - if (this.chestplate.get("custom")) { - this.customItems.push(chestplate); - } else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(chestplate); - } - } catch (Error) { - console.log(Error); - const chestplate = itemMap.get("No Chestplate"); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - errors.push(new ItemNotFound(equipment[1], "chestplate", true)); - } - } - if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") { - const leggings = itemMap.get(equipment[2]); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - } else { - try { - let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined); - if (leggings.statMap.get("type") !== "leggings") { - throw new Error("Not a leggings"); - } - this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots")); - leggings.statMap.set("powders",this.powders[2].slice()); - this.leggings = leggings.statMap; - applyArmorPowders(this.leggings, this.powders[2]); - if (this.leggings.get("custom")) { - this.customItems.push(leggings); - } else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(leggings); - } - } catch (Error) { - const leggings = itemMap.get("No Leggings"); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - errors.push(new ItemNotFound(equipment[2], "leggings", true)); - } - } - if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") { - const boots = itemMap.get(equipment[3]); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - } else { - try { - let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined); - if (boots.statMap.get("type") !== "boots") { - throw new Error("Not a boots"); - } - this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots")); - boots.statMap.set("powders",this.powders[3].slice()); - this.boots = boots.statMap; - applyArmorPowders(this.boots, this.powders[3]); - if (this.boots.get("custom")) { - this.customItems.push(boots); - } else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(boots); - } - } catch (Error) { - const boots = itemMap.get("No Boots"); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - errors.push(new ItemNotFound(equipment[3], "boots", true)); - } - } - if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") { - const ring = itemMap.get(equipment[4]); - this.ring1 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring1 = ring.statMap; - if (this.ring1.get("custom")) { - this.customItems.push(ring); - } else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 1"); - this.ring1 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring")); - } - } - if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") { - const ring = itemMap.get(equipment[5]); - this.ring2 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring2 = ring.statMap; - if (this.ring2.get("custom")) { - this.customItems.push(ring); - } else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 2"); - this.ring2 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring")); - } - } - if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") { - const bracelet = itemMap.get(equipment[6]); - this.bracelet = expandItem(bracelet, []); - }else{ - try { - let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined); - if (bracelet.statMap.get("type") !== "bracelet") { - throw new Error("Not a bracelet"); - } - this.bracelet = bracelet.statMap; - if (this.bracelet.get("custom")) { - this.customItems.push(bracelet); - } else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(bracelet); - } - } catch (Error) { - const bracelet = itemMap.get("No Bracelet"); - this.bracelet = expandItem(bracelet, []); - errors.push(new ItemNotFound(equipment[6], "bracelet", true)); - } - } - if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") { - const necklace = itemMap.get(equipment[7]); - this.necklace = expandItem(necklace, []); - }else{ - try { - let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined); - if (necklace.statMap.get("type") !== "necklace") { - throw new Error("Not a necklace"); - } - this.necklace = necklace.statMap; - if (this.necklace.get("custom")) { - this.customItems.push(necklace); - } else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(necklace); - } - } catch (Error) { - const necklace = itemMap.get("No Necklace"); - this.necklace = expandItem(necklace, []); - errors.push(new ItemNotFound(equipment[7], "necklace", true)); - } - } - if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") { - const weapon = itemMap.get(equipment[8]); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - if (equipment[8] !== "No Weapon") { - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } else { - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - } - }else{ - try { - let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined); - if (weapon.statMap.get("category") !== "weapon") { - throw new Error("Not a weapon"); - } - this.weapon = weapon.statMap; - if (this.weapon.get("custom")) { - this.customItems.push(weapon); - } else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(weapon); - } - this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots")); - this.weapon.set("powders",this.powders[4].slice()); - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } catch (Error) { - const weapon = itemMap.get("No Weapon"); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - errors.push(new ItemNotFound(equipment[8], "weapon", true)); - } - } - //console.log(this.craftedItems) + constructor(level, items, tomes, weapon){ if (level < 1) { //Should these be constants? this.level = 1; @@ -348,10 +122,12 @@ class Build{ document.getElementById("level-choice").value = this.level; this.availableSkillpoints = levelToSkillPoints(this.level); - this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ]; - this.items = this.equipment.concat([this.weapon]); + this.equipment = items; + this.tomes = tomes; + this.weapon = weapon; + this.items = this.equipment.concat([this.weapon]).concat(this.tomes); // return [equip_order, best_skillpoints, final_skillpoints, best_total]; - let result = calculate_skillpoints(this.equipment, this.weapon); + let result = calculate_skillpoints(this.equipment.concat(this.tomes), this.weapon); console.log(result); this.equip_order = result[0]; // How many skillpoints the player had to assign (5 number) @@ -361,28 +137,14 @@ class Build{ // How many skillpoints assigned (1 number, sum of base_skillpoints) this.assigned_skillpoints = result[3]; this.activeSetCounts = result[4]; - - // For strength boosts like warscream, vanish, etc. - this.damageMultiplier = 1.0; - this.defenseMultiplier = 1.0; - - // For other external boosts ;-; - this.externalStats = externalStats; this.initBuildStats(); - - // Remove every error before adding specific ones - for (let i of document.getElementsByClassName("error")) { - i.textContent = ""; - } - this.errors = errors; - if (errors.length > 0) this.errored = true; } /*Returns build in string format */ toString(){ - return [this.equipment,this.weapon].flat(); + return [this.equipment,this.weapon,this.tomes].flat(); } /* Getters */ @@ -392,7 +154,7 @@ class Build{ } getBaseSpellCost(spellIdx, cost) { - cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); + // old intelligence: cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); cost += this.statMap.get("spRaw"+spellIdx); return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)); } diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js new file mode 100644 index 0000000..9b168b1 --- /dev/null +++ b/js/build_encode_decode.js @@ -0,0 +1,201 @@ + +/* + * Populate fields based on url, and calculate build. + */ +function decodeBuild(url_tag) { + if (url_tag) { + //default values + let equipment = [null, null, null, null, null, null, null, null, null]; + let tomes = [null, null, null, null, null, null, null]; + let powdering = ["", "", "", "", ""]; + let info = url_tag.split("_"); + let version = info[0]; + let save_skp = false; + let skillpoints = [0, 0, 0, 0, 0]; + let level = 106; + + let version_number = parseInt(version) + //equipment (items) + // TODO: use filters + if (version_number < 4) { + let equipments = info[1]; + for (let i = 0; i < 9; ++i ) { + let equipment_str = equipments.slice(i*3,i*3+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + } + info[1] = equipments.slice(27); + } + else if (version_number == 4) { + let info_str = info[1]; + let start_idx = 0; + for (let i = 0; i < 9; ++i ) { + if (info_str.charAt(start_idx) === "-") { + equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18); + start_idx += 18; + } + else { + let equipment_str = info_str.slice(start_idx, start_idx+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + start_idx += 3; + } + } + info[1] = info_str.slice(start_idx); + } + else if (version_number <= 6) { + let info_str = info[1]; + let start_idx = 0; + for (let i = 0; i < 9; ++i ) { + if (info_str.slice(start_idx,start_idx+3) === "CR-") { + equipment[i] = info_str.slice(start_idx, start_idx+20); + start_idx += 20; + } else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") { + let len = Base64.toInt(info_str.slice(start_idx,start_idx+3)); + equipment[i] = info_str.slice(start_idx+3,start_idx+3+len); + start_idx += (3+len); + } else { + let equipment_str = info_str.slice(start_idx, start_idx+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + start_idx += 3; + } + } + info[1] = info_str.slice(start_idx); + } + //constant in all versions + for (let i in equipment) { + setValue(equipmentInputs[i], equipment[i]); + } + + //level, skill point assignments, and powdering + if (version_number == 1) { + let powder_info = info[1]; + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number == 2) { + save_skp = true; + let skillpoint_info = info[1].slice(0, 10); + for (let i = 0; i < 5; ++i ) { + skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); + } + + let powder_info = info[1].slice(10); + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number <= 6){ + level = Base64.toInt(info[1].slice(10,12)); + setValue("level-choice",level); + save_skp = true; + let skillpoint_info = info[1].slice(0, 10); + for (let i = 0; i < 5; ++i ) { + skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); + } + + let powder_info = info[1].slice(12); + + let res = parsePowdering(powder_info); + powdering = res[0]; + info[1] = res[1]; + } + // Tomes. + if (version == 6) { + //tome values do not appear in anything before v6. + for (let i = 0; i < 7; ++i) { + let tome_str = info[1].charAt(i); + for (let i in tomes) { + setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); + } + } + info[1] = info[1].slice(7); + } + + for (let i in powderInputs) { + setValue(powderInputs[i], powdering[i]); + } + } +} + +/* Stores the entire build in a string using B64 encoding and adds it to the URL. +*/ +function encodeBuild(build) { + + if (build) { + let build_string; + + //V6 encoding - Tomes + build_version = 4; + build_string = ""; + tome_string = ""; + + let crafted_idx = 0; + let custom_idx = 0; + for (const item of build.items) { + + if (item.get("custom")) { + let custom = "CI-"+encodeCustom(build.customItems[custom_idx],true); + build_string += Base64.fromIntN(custom.length, 3) + custom; + custom_idx += 1; + build_version = Math.max(build_version, 5); + } else if (item.get("crafted")) { + build_string += "CR-"+encodeCraft(build.craftedItems[crafted_idx]); + crafted_idx += 1; + } else if (item.get("category") === "tome") { + let tome_id = item.get("id"); + if (tome_id <= 60) { + // valid normal tome. ID 61-63 is for NONE tomes. + build_version = Math.max(build_version, 6); + } + tome_string += Base64.fromIntN(tome_id, 1); + } else { + build_string += Base64.fromIntN(item.get("id"), 3); + } + } + + for (const skp of skp_order) { + build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 + } + build_string += Base64.fromIntN(build.level, 2); + for (const _powderset of build.powders) { + let n_bits = Math.ceil(_powderset.length / 6); + build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. + // Slice copy. + let powderset = _powderset.slice(); + while (powderset.length != 0) { + let firstSix = powderset.slice(0,6).reverse(); + let powder_hash = 0; + for (const powder of firstSix) { + powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. + } + build_string += Base64.fromIntN(powder_hash, 5); + powderset = powderset.slice(6); + } + } + build_string += tome_string; + + return build_version.toString() + "_" + build_string; + } +} + +function copyBuild(build) { + if (build) { + copyTextToClipboard(url_base+location.hash); + document.getElementById("copy-button").textContent = "Copied!"; + } +} + +function shareBuild(build) { + if (build) { + let text = url_base+location.hash+"\n"+ + "WynnBuilder build:\n"+ + "> "+build.helmet.get("displayName")+"\n"+ + "> "+build.chestplate.get("displayName")+"\n"+ + "> "+build.leggings.get("displayName")+"\n"+ + "> "+build.boots.get("displayName")+"\n"+ + "> "+build.ring1.get("displayName")+"\n"+ + "> "+build.ring2.get("displayName")+"\n"+ + "> "+build.bracelet.get("displayName")+"\n"+ + "> "+build.necklace.get("displayName")+"\n"+ + "> "+build.weapon.get("displayName")+" ["+build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; + copyTextToClipboard(text); + document.getElementById("share-button").textContent = "Copied!"; + } +} + diff --git a/js/builder.js b/js/builder.js index 1e51e31..249bf32 100644 --- a/js/builder.js +++ b/js/builder.js @@ -35,206 +35,6 @@ function parsePowdering(powder_info) { return [powdering, powder_info]; } -/* - * Populate fields based on url, and calculate build. - */ -function decodeBuild(url_tag) { - if (url_tag) { - //default values - let equipment = [null, null, null, null, null, null, null, null, null]; - let tomes = [null, null, null, null, null, null, null]; - let powdering = ["", "", "", "", ""]; - let info = url_tag.split("_"); - let version = info[0]; - let save_skp = false; - let skillpoints = [0, 0, 0, 0, 0]; - let level = 106; - - let version_number = parseInt(version) - //equipment (items) - // TODO: use filters - if (version_number < 4) { - let equipments = info[1]; - for (let i = 0; i < 9; ++i ) { - let equipment_str = equipments.slice(i*3,i*3+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - } - info[1] = equipments.slice(27); - } - else if (version_number == 4) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.charAt(start_idx) === "-") { - equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18); - start_idx += 18; - } - else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - else if (version_number <= 6) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.slice(start_idx,start_idx+3) === "CR-") { - equipment[i] = info_str.slice(start_idx, start_idx+20); - start_idx += 20; - } else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") { - let len = Base64.toInt(info_str.slice(start_idx,start_idx+3)); - equipment[i] = info_str.slice(start_idx+3,start_idx+3+len); - start_idx += (3+len); - } else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - //constant in all versions - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } - - //level, skill point assignments, and powdering - if (version_number == 1) { - let powder_info = info[1]; - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number == 2) { - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(10); - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number <= 6){ - level = Base64.toInt(info[1].slice(10,12)); - setValue("level-choice",level); - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(12); - - let res = parsePowdering(powder_info); - powdering = res[0]; - info[1] = res[1]; - } - // Tomes. - if (version == 6) { - //tome values do not appear in anything before v6. - for (let i = 0; i < 7; ++i) { - let tome_str = info[1].charAt(i); - for (let i in tomes) { - setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); - } - } - info[1] = info[1].slice(7); - } - - for (let i in powderInputs) { - setValue(powderInputs[i], powdering[i]); - } - } -} - -/* Stores the entire build in a string using B64 encoding and adds it to the URL. -*/ -function encodeBuild() { - - if (player_build) { - let build_string; - - //V6 encoding - Tomes - build_version = 4; - build_string = ""; - tome_string = ""; - - let crafted_idx = 0; - let custom_idx = 0; - for (const item of player_build.items) { - - if (item.get("custom")) { - let custom = "CI-"+encodeCustom(player_build.customItems[custom_idx],true); - build_string += Base64.fromIntN(custom.length, 3) + custom; - custom_idx += 1; - build_version = Math.max(build_version, 5); - } else if (item.get("crafted")) { - build_string += "CR-"+encodeCraft(player_build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else if (item.get("category") === "tome") { - let tome_id = item.get("id"); - if (tome_id <= 60) { - // valid normal tome. ID 61-63 is for NONE tomes. - build_version = Math.max(build_version, 6); - } - tome_string += Base64.fromIntN(tome_id, 1); - } else { - build_string += Base64.fromIntN(item.get("id"), 3); - } - } - - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 - } - build_string += Base64.fromIntN(player_build.level, 2); - for (const _powderset of player_build.powders) { - let n_bits = Math.ceil(_powderset.length / 6); - build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. - // Slice copy. - let powderset = _powderset.slice(); - while (powderset.length != 0) { - let firstSix = powderset.slice(0,6).reverse(); - let powder_hash = 0; - for (const powder of firstSix) { - powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. - } - build_string += Base64.fromIntN(powder_hash, 5); - powderset = powderset.slice(6); - } - } - build_string += tome_string; - - return build_version.toString() + "_" + build_string; - } -} - -function copyBuild() { - if (player_build) { - copyTextToClipboard(url_base+location.hash); - document.getElementById("copy-button").textContent = "Copied!"; - } -} - -function shareBuild() { - if (player_build) { - let text = url_base+location.hash+"\n"+ - "WynnBuilder build:\n"+ - "> "+player_build.helmet.get("displayName")+"\n"+ - "> "+player_build.chestplate.get("displayName")+"\n"+ - "> "+player_build.leggings.get("displayName")+"\n"+ - "> "+player_build.boots.get("displayName")+"\n"+ - "> "+player_build.ring1.get("displayName")+"\n"+ - "> "+player_build.ring2.get("displayName")+"\n"+ - "> "+player_build.bracelet.get("displayName")+"\n"+ - "> "+player_build.necklace.get("displayName")+"\n"+ - "> "+player_build.weapon.get("displayName")+" ["+player_build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; - copyTextToClipboard(text); - document.getElementById("share-button").textContent = "Copied!"; - } -} - function populateBuildList() { const buildList = document.getElementById("build-choice"); const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); @@ -250,7 +50,7 @@ function saveBuild() { if (player_build) { const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); const saveName = document.getElementById("build-name").value; - const encodedBuild = encodeBuild(); + const encodedBuild = encodeBuild(player_build); if ((!Object.keys(savedBuilds).includes(saveName) || document.getElementById("saved-error").textContent !== "") && encodedBuild !== "") { savedBuilds[saveName] = encodedBuild.replace("#", ""); @@ -330,6 +130,15 @@ function toggleButton(button_id) { } } +// toggle tab +function toggle_tab(tab) { + if (document.querySelector("#"+tab).style.display == "none") { + document.querySelector("#"+tab).style.display = ""; + } else { + document.querySelector("#"+tab).style.display = "none"; + } +} + // TODO: Learn and use await function init() { console.log("builder.js init"); diff --git a/js/builder_graph.js b/js/builder_graph.js index fd0de13..111cd92 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,5 +1,52 @@ +class BuildEncodeNode extends ComputeNode { + constructor() { + super("builder-encode"); + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "BuildEncodeNode 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 encodeBuild(build); + } +} + +class URLUpdateNode extends ComputeNode { + 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; + } +} + +class BuildAssembleNode extends ComputeNode { + 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') + ]; + let weapon = input_map.get('weapon-input'); + let level = input_map.get('level-input'); + console.log('build node run'); + return new Build(level, equipments, [], weapon); + } +} + let item_nodes = []; document.addEventListener('DOMContentLoaded', function() { @@ -16,6 +63,14 @@ document.addEventListener('DOMContentLoaded', function() { } let weapon_image = document.getElementById("weapon-img"); new WeaponDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); + let level_input = new InputNode('level-input', document.getElementById('level-choice')); + new PrintNode('lvl-debug').link_to(level_input); + + let build_node = new BuildAssembleNode(); + for (const input of item_nodes) { + build_node.link_to(input); + } + build_node.link_to(level_input); console.log("Set up graph"); }); @@ -98,6 +153,7 @@ function init_autocomplete() { if (event.detail.selection.value) { event.target.value = event.detail.selection.value; } + event.target.dispatchEvent(new Event('input')); }, }, } diff --git a/js/computation_graph.js b/js/computation_graph.js index 417c9bf..155d95a 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -6,14 +6,16 @@ class ComputeNode { * @param name : Name of the node (string). Must be unique. Must "fit in" a JS string (terminated by single quotes). */ constructor(name) { - this.inputs = []; + this.inputs = []; // parent nodes this.children = []; this.value = 0; this.name = name; this.update_task = null; this.update_time = Date.now(); this.fail_cb = false; // Set to true to force updates even if parent failed. - this.calc_inputs = new Map(); + this.dirty = false; + this.inputs_dirty = new Map(); + this.inputs_dirty_count = 0; } /** @@ -24,7 +26,19 @@ class ComputeNode { return; } this.update_time = timestamp; - this.set_value(this.compute_func(this.calc_inputs)); + + if (this.inputs_dirty_count != 0) { + return; + } + let calc_inputs = new Map(); + for (const input of this.inputs) { + calc_inputs.set(input.name, input.value); + } + this.value = this.compute_func(calc_inputs); + this.dirty = false; + for (const child of this.children) { + child.mark_input_clean(this.name, this.value, timestamp); + } } /** @@ -40,22 +54,35 @@ class ComputeNode { } /** - * Set an input value. Propagates calculation if all inputs are present. + * Mark parent as not dirty. Propagates calculation if all inputs are present. */ - set_input(input_name, value, timestamp) { - if (value || this.fail_cb) { - this.calc_inputs.set(input_name, value) - if (this.calc_inputs.size === this.inputs.length) { - this.update(timestamp) + mark_input_clean(input_name, value, timestamp) { + if (value !== null || this.fail_cb) { + if (this.inputs_dirty.get(input_name)) { + this.inputs_dirty.set(input_name, false); + this.inputs_dirty_count -= 1; + } + if (this.inputs_dirty_count === 0) { + this.update(timestamp); } } } - /** - * Remove cached input values to this calculation. - */ - clear_cache() { - this.calc_inputs = new Map(); + mark_input_dirty(input_name) { + if (!this.inputs_dirty.get(input_name)) { + this.inputs_dirty.set(input_name, true); + this.inputs_dirty_count += 1; + } + } + + mark_dirty() { + if (!this.dirty) { + this.dirty = true; + for (const child of this.children) { + child.mark_input_dirty(this.name); + child.mark_dirty(); + } + } } /** @@ -74,6 +101,10 @@ class ComputeNode { link_to(parent_node) { this.inputs.push(parent_node) + this.inputs_dirty.set(parent_node.name, parent_node.dirty); + if (parent_node.dirty) { + this.inputs_dirty_count += 1; + } parent_node.children.push(this); } } @@ -87,6 +118,7 @@ function calcSchedule(node) { if (node.update_task !== null) { clearTimeout(node.update_task); } + node.mark_dirty(); node.update_task = setTimeout(function() { const timestamp = Date.now(); node.update(timestamp); @@ -107,10 +139,25 @@ class PrintNode extends ComputeNode { } } +/** + * Node for getting an input from an input field. + */ +class InputNode extends ComputeNode { + constructor(name, input_field) { + super(name); + this.input_field = input_field; + this.input_field.addEventListener("input", () => calcSchedule(this)); + } + + compute_func(input_map) { + return this.input_field.value; + } +} + /** * Node for getting an item's stats from an item input field. */ -class ItemInputNode extends ComputeNode { +class ItemInputNode extends InputNode { /** * Make an item stat pulling compute node. * @@ -119,9 +166,7 @@ class ItemInputNode extends ComputeNode { * @param none_item: Item object to use as the "none" for this field. */ constructor(name, item_input_field, none_item) { - super(name); - this.input_field = item_input_field; - this.input_field.addEventListener("input", () => calcSchedule(this)); + super(name, item_input_field); this.none_item = new Item(none_item); }