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);
}