Compute graph cleanup, prepping for full build calc (currently broke)

This commit is contained in:
hppeng 2022-06-19 00:42:49 -07:00
parent 4f7f0f9cfc
commit 62a9a4f0c2
6 changed files with 346 additions and 472 deletions

View file

@ -313,10 +313,10 @@
</div>
<div class="row align-items-center">
<div class="col-auto px-1 text-nowrap scaled-font">
<button class="border-dark text-light dark-5 scaled-font rounded" id=copy-button onclick="copyBuild()">Copy short</button>
<button class="border-dark text-light dark-5 scaled-font rounded" id=copy-button onclick="copyBuild(player_build)">Copy short</button>
</div>
<div class="col-auto px-1 text-nowrap scaled-font">
<button class="border-dark text-light dark-5 scaled-font rounded" id=share-button onclick="shareBuild()">Copy for sharing</button>
<button class="border-dark text-light dark-5 scaled-font rounded" id=share-button onclick="shareBuild(player_build)">Copy for sharing</button>
</div>
</div>
</div>
@ -1399,8 +1399,9 @@
<script type="text/javascript" src="../js/load_tome.js"></script>
<script type="text/javascript" src="../js/custom.js"></script>
<script type="text/javascript" src="../js/craft.js"></script>
<script type="text/javascript" src="../js/sq2build.js"></script>
<script type="text/javascript" src="../js/build.js"></script>
<script type="text/javascript" src="../js/build_constants.js"></script>
<script type="text/javascript" src="../js/build_encode_decode.js"></script>
<script type="text/javascript" src="../js/builder.js"></script>
<script type="text/javascript" src="../js/builder_graph.js"></script>
<script type="text/javascript" src="../js/expr_parser.js"></script>

View file

@ -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)
@ -362,27 +138,13 @@ class Build{
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));
}

201
js/build_encode_decode.js Normal file
View file

@ -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!";
}
}

View file

@ -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");

View file

@ -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'));
},
},
}

View file

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