diff --git a/.gitignore b/.gitignore
index bc37c3b..ce62501 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
*.swp
*.bat
sets/
+
+.idea/
+*.iml
diff --git a/build.js b/build.js
index 29c9c63..b6b0e54 100644
--- a/build.js
+++ b/build.js
@@ -113,9 +113,12 @@ class Build{
const helmet = itemMap.get(equipment[0]);
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
- }else{
+ } else {
try {
let helmet = getCraftFromHash(equipment[0]);
+ if (helmet.statMap.get("type") !== "helmet") {
+ throw new Error("Not a helmet");
+ }
this.powders[0] = this.powders[0].slice(0,helmet.statMap.slots);
helmet.statMap.set("powders",this.powders[0].slice());
helmet.applyPowders();
@@ -133,9 +136,12 @@ class Build{
const chestplate = itemMap.get(equipment[1]);
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
- }else{
+ } else {
try {
let chestplate = getCraftFromHash(equipment[1]);
+ if (chestplate.statMap.get("type") !== "chestplate") {
+ throw new Error("Not a chestplate");
+ }
this.powders[1] = this.powders[1].slice(0,chestplate.statMap.slots);
chestplate.statMap.set("powders",this.powders[1].slice());
chestplate.applyPowders();
@@ -148,13 +154,16 @@ class Build{
errors.push(new ItemNotFound(equipment[1], "chestplate", true));
}
}
- if(itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") {
+ 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{
+ } else {
try {
let leggings = getCraftFromHash(equipment[2]);
+ if (leggings.statMap.get("type") !== "leggings") {
+ throw new Error("Not a leggings");
+ }
this.powders[2] = this.powders[2].slice(0,leggings.statMap.slots);
leggings.statMap.set("powders",this.powders[2].slice());
leggings.applyPowders();
@@ -167,13 +176,16 @@ class Build{
errors.push(new ItemNotFound(equipment[2], "leggings", true));
}
}
- if(itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") {
+ 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{
+ } else {
try {
let boots = getCraftFromHash(equipment[3]);
+ if (boots.statMap.get("type") !== "boots") {
+ throw new Error("Not a boots");
+ }
this.powders[3] = this.powders[3].slice(0,boots.statMap.slots);
boots.statMap.set("powders",this.powders[3].slice());
boots.applyPowders();
@@ -192,6 +204,9 @@ class Build{
}else{
try {
let ring = getCraftFromHash(equipment[4]);
+ if (ring.statMap.get("type") !== "ring") {
+ throw new Error("Not a ring");
+ }
this.ring1 = ring.statMap;
this.craftedItems.push(ring);
} catch (Error) {
@@ -206,6 +221,9 @@ class Build{
}else{
try {
let ring = getCraftFromHash(equipment[5]);
+ if (ring.statMap.get("type") !== "ring") {
+ throw new Error("Not a ring");
+ }
this.ring2 = ring.statMap;
this.craftedItems.push(ring);
} catch (Error) {
@@ -220,6 +238,9 @@ class Build{
}else{
try {
let bracelet = getCraftFromHash(equipment[6]);
+ if (bracelet.statMap.get("type") !== "bracelet") {
+ throw new Error("Not a bracelet");
+ }
this.bracelet = bracelet.statMap;
this.craftedItems.push(bracelet);
} catch (Error) {
@@ -234,6 +255,9 @@ class Build{
}else{
try {
let necklace = getCraftFromHash(equipment[7]);
+ if (necklace.statMap.get("type") !== "necklace") {
+ throw new Error("Not a necklace");
+ }
this.necklace = necklace.statMap;
this.craftedItems.push(necklace);
} catch (Error) {
@@ -254,6 +278,9 @@ class Build{
}else{
try {
let weapon = getCraftFromHash(equipment[8]);
+ if (weapon.statMap.get("category") !== "weapon") {
+ throw new Error("Not a weapon");
+ }
this.weapon = weapon.statMap;
this.craftedItems.push(weapon);
this.powders[4] = this.powders[4].slice(0,this.weapon.slots);
@@ -417,6 +444,7 @@ class Build{
}
statMap.set("hp", levelToHPBase(this.level));
+ let major_ids = new Set();
for (const item of this.items){
for (let [id, value] of item.get("maxRolls")) {
statMap.set(id,(statMap.get(id) || 0)+value);
@@ -426,7 +454,13 @@ class Build{
statMap.set(staticID, statMap.get(staticID) + item.get(staticID));
}
}
+ if (item.get("majorIds")) {
+ for (const majorID of item.get("majorIds")) {
+ major_ids.add(majorID);
+ }
+ }
}
+ statMap.set("activeMajorIDs", major_ids);
for (const [setName, count] of this.activeSetCounts) {
const bonus = sets[setName].bonuses[count-1];
for (const id in bonus) {
diff --git a/builder.js b/builder.js
index cd1a887..9bc6862 100644
--- a/builder.js
+++ b/builder.js
@@ -3,7 +3,7 @@ const url_tag = location.hash.slice(1);
console.log(url_base);
console.log(url_tag);
-const BUILD_VERSION = "6.9.20";
+const BUILD_VERSION = "6.9.22";
function setTitle() {
let text;
@@ -220,6 +220,29 @@ function getItemNameFromID(id) {
return idMap.get(id);
}
+function parsePowdering(powder_info) {
+ // TODO: Make this run in linear instead of quadratic time... ew
+ let powdering = [];
+ for (let i = 0; i < 5; ++i) {
+ let powders = "";
+ let n_blocks = Base64.toInt(powder_info.charAt(0));
+ console.log(n_blocks + " blocks");
+ powder_info = powder_info.slice(1);
+ for (let j = 0; j < n_blocks; ++j) {
+ let block = powder_info.slice(0,5);
+ console.log(block);
+ let six_powders = Base64.toInt(block);
+ for (let k = 0; k < 6 && six_powders != 0; ++k) {
+ powders += powderNames.get((six_powders & 0x1f) - 1);
+ six_powders >>>= 5;
+ }
+ powder_info = powder_info.slice(5);
+ }
+ powdering[i] = powders;
+ }
+ return powdering;
+}
+
/*
* Populate fields based on url, and calculate build.
*/
@@ -235,90 +258,54 @@ function decodeBuild(url_tag) {
if (version === "0" || version === "1" || version === "2" || version === "3") {
let equipments = info[1];
for (let i = 0; i < 9; ++i ) {
- equipment[i] = getItemNameFromID(Base64.toInt(equipments.slice(i*3,i*3+3)));
+ let equipment_str = equipments.slice(i*3,i*3+3);
+ equipment[i] = getItemNameFromID(Base64.toInt(equipment_str));
}
+ info[1] = equipments.slice(27);
}
- if (version === "1") {
- let powder_info = info[1].slice(27);
- console.log(powder_info);
- // TODO: Make this run in linear instead of quadratic time... ew
- for (let i = 0; i < 5; ++i) {
- let powders = "";
- let n_blocks = Base64.toInt(powder_info.charAt(0));
- console.log(n_blocks + " blocks");
- powder_info = powder_info.slice(1);
- for (let j = 0; j < n_blocks; ++j) {
- let block = powder_info.slice(0,5);
- console.log(block);
- let six_powders = Base64.toInt(block);
- for (let k = 0; k < 6 && six_powders != 0; ++k) {
- powders += powderNames.get((six_powders & 0x1f) - 1);
- six_powders >>>= 5;
- }
- powder_info = powder_info.slice(5);
+ if (version === "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;
}
- powdering[i] = powders;
}
+ info[1] = info_str.slice(start_idx);
}
- if (version === "2") {
+
+ if (version === "1") {
+ let powder_info = info[1];
+ powdering = parsePowdering(powder_info);
+ } else if (version === "2") {
save_skp = true;
- let skillpoint_info = info[1].slice(27, 37);
+ 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(37);
- console.log(powder_info);
- // TODO: Make this run in linear instead of quadratic time...
- for (let i = 0; i < 5; ++i) {
- let powders = "";
- let n_blocks = Base64.toInt(powder_info.charAt(0));
- console.log(n_blocks + " blocks");
- powder_info = powder_info.slice(1);
- for (let j = 0; j < n_blocks; ++j) {
- let block = powder_info.slice(0,5);
- console.log(block);
- let six_powders = Base64.toInt(block);
- for (let k = 0; k < 6 && six_powders != 0; ++k) {
- powders += powderNames.get((six_powders & 0x1f) - 1);
- six_powders >>>= 5;
- }
- powder_info = powder_info.slice(5);
- }
- powdering[i] = powders;
- }
- }
- if (version === "3"){
- level = Base64.toInt(info[1].slice(37,39));
+ let powder_info = info[1].slice(10);
+ powdering = parsePowdering(powder_info);
+ } else if (version === "3" || version === "4"){
+ level = Base64.toInt(info[1].slice(10,12));
setValue("level-choice",level);
save_skp = true;
- let skillpoint_info = info[1].slice(27, 37);
+ 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(39);
- // TODO: Make this run in linear instead of quadratic time...
- for (let i = 0; i < 5; ++i) {
- let powders = "";
- let n_blocks = Base64.toInt(powder_info.charAt(0));
- powder_info = powder_info.slice(1);
- for (let j = 0; j < n_blocks; ++j) {
- let block = powder_info.slice(0,5);
- console.log(block);
- let six_powders = Base64.toInt(block);
- for (let k = 0; k < 6 && six_powders != 0; ++k) {
- powders += powderNames.get((six_powders & 0x1f) - 1);
- six_powders >>>= 5;
- }
- powder_info = powder_info.slice(5);
- }
- powdering[i] = powders;
- }
- }
- if (version === "4") { //crafted support
- //@hpp
+ let powder_info = info[1].slice(12);
+
+ powdering = parsePowdering(powder_info);
}
+
for (let i in powderInputs) {
setValue(powderInputs[i], powdering[i]);
}
@@ -334,15 +321,27 @@ function decodeBuild(url_tag) {
function encodeBuild() {
if (player_build) {
//@hpp update for 4_
- let build_string = "3_" + Base64.fromIntN(player_build.helmet.get("id"), 3) +
- Base64.fromIntN(player_build.chestplate.get("id"), 3) +
- Base64.fromIntN(player_build.leggings.get("id"), 3) +
- Base64.fromIntN(player_build.boots.get("id"), 3) +
- Base64.fromIntN(player_build.ring1.get("id"), 3) +
- Base64.fromIntN(player_build.ring2.get("id"), 3) +
- Base64.fromIntN(player_build.bracelet.get("id"), 3) +
- Base64.fromIntN(player_build.necklace.get("id"), 3) +
- Base64.fromIntN(player_build.weapon.get("id"), 3);
+ let build_string = "4_";
+ let crafted_idx = 0;
+ for (const item of player_build.items) {
+ if (item.get("crafted")) {
+ build_string += "-"+encodeCraft(player_build.craftedItems[crafted_idx])
+ crafted_idx += 1
+ }
+ else {
+ build_string += Base64.fromIntN(item.get("id"), 3);
+ }
+ }
+// this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ];
+// let build_string = "3_" + Base64.fromIntN(player_build.helmet.get("id"), 3) +
+// Base64.fromIntN(player_build.chestplate.get("id"), 3) +
+// Base64.fromIntN(player_build.leggings.get("id"), 3) +
+// Base64.fromIntN(player_build.boots.get("id"), 3) +
+// Base64.fromIntN(player_build.ring1.get("id"), 3) +
+// Base64.fromIntN(player_build.ring2.get("id"), 3) +
+// Base64.fromIntN(player_build.bracelet.get("id"), 3) +
+// Base64.fromIntN(player_build.necklace.get("id"), 3) +
+// Base64.fromIntN(player_build.weapon.get("id"), 3);
for (const skp of skp_order) {
build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048
@@ -797,7 +796,7 @@ function calculateBuildStats() {
let baditem = document.createElement("p");
baditem.classList.add("nocolor");
baditem.classList.add("itemp");
- baditem.textContent = item.get("name") + " requires level " + item.get("lvl") + " to use.";
+ baditem.textContent = item.get("displayName") + " requires level " + item.get("lvl") + " to use.";
lvlWarning.appendChild(baditem);
}
}
diff --git a/craft.js b/craft.js
index 1c6bcab..6eca41e 100644
--- a/craft.js
+++ b/craft.js
@@ -369,6 +369,8 @@ class Craft{
statMap.get("maxRolls").set(id,0);
}
}
+
+ statMap.set("crafted", true);
this.statMap = statMap;
}
-}
\ No newline at end of file
+}
diff --git a/crafter.js b/crafter.js
index 6856d5d..472d1fb 100644
--- a/crafter.js
+++ b/crafter.js
@@ -202,8 +202,9 @@ function calculateCraft() {
//create the craft
player_craft = new Craft(recipe,mat_tiers,ingreds,atkSpd,"");
- location.hash = encodeCraft();
- player_craft.setHash(encodeCraft());
+ let craft_str = encodeCraft(player_craft);
+ location.hash = craft_str;
+ player_craft.setHash(craft_str);
console.log(player_craft);
/*console.log(recipe)
console.log(levelrange)
@@ -239,19 +240,19 @@ function calculateCraft() {
}
-function encodeCraft() {
- if (player_craft) {
+function encodeCraft(craft) {
+ if (craft) {
let atkSpds = ["SLOW","NORMAL","FAST"];
let craft_string = "1" +
- Base64.fromIntN(player_craft.ingreds[0].get("id"), 2) +
- Base64.fromIntN(player_craft.ingreds[1].get("id"), 2) +
- Base64.fromIntN(player_craft.ingreds[2].get("id"), 2) +
- Base64.fromIntN(player_craft.ingreds[3].get("id"), 2) +
- Base64.fromIntN(player_craft.ingreds[4].get("id"), 2) +
- Base64.fromIntN(player_craft.ingreds[5].get("id"), 2) +
- Base64.fromIntN(player_craft.recipe.get("id"),2) +
- Base64.fromIntN(player_craft.mat_tiers[0] + (player_craft.mat_tiers[1]-1)*3, 1) + //this maps tiers [a,b] to a+3b.
- Base64.fromIntN(atkSpds.indexOf(player_craft["atkSpd"]),1);
+ Base64.fromIntN(craft.ingreds[0].get("id"), 2) +
+ Base64.fromIntN(craft.ingreds[1].get("id"), 2) +
+ Base64.fromIntN(craft.ingreds[2].get("id"), 2) +
+ Base64.fromIntN(craft.ingreds[3].get("id"), 2) +
+ Base64.fromIntN(craft.ingreds[4].get("id"), 2) +
+ Base64.fromIntN(craft.ingreds[5].get("id"), 2) +
+ Base64.fromIntN(craft.recipe.get("id"),2) +
+ Base64.fromIntN(craft.mat_tiers[0] + (craft.mat_tiers[1]-1)*3, 1) + //this maps tiers [a,b] to a+3b.
+ Base64.fromIntN(atkSpds.indexOf(craft["atkSpd"]),1);
return craft_string;
}
return "";
diff --git a/credits.txt b/credits.txt
index 29ba52c..f32f2a1 100644
--- a/credits.txt
+++ b/credits.txt
@@ -5,6 +5,7 @@ The game, of course
- wynncraft.com
Additional Contributors:
+ - Phanta (WynnAtlas custom expression parser / item search)
- QuantumNep (Layout code/layout ideas)
- dr_carlos (Hiding UI elements properly, fade animations, proper error handling)
- Atlas Inc discord (feedback, ideas, etc)
diff --git a/damage_calc.js b/damage_calc.js
index c131ef7..bd225ee 100644
--- a/damage_calc.js
+++ b/damage_calc.js
@@ -6,13 +6,15 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier,
let buildStats = new Map(stats);
if(externalStats) { //if nothing is passed in, then this hopefully won't trigger
- for (const [key,value] of externalStats) {
+ for (let i = 0; i < externalStats.length; i++) {
+ const key = externalStats[i][0];
+ const value = externalStats[i][1];
if (typeof value === "number") {
buildStats.set(key, buildStats.get(key) + value);
} else if (Array.isArray(value)) {
arr = [];
- for (let i = 0; i < value.length; i++) {
- arr[i] = buildStats.get(key)[i] + value[i];
+ for (let j = 0; j < value.length; j++) {
+ arr[j] = buildStats.get(key)[j] + value[j];
}
buildStats.set(key, arr);
}
@@ -21,8 +23,9 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier,
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
let damages = [];
- for (const damage_string of buildStats.get("damageRaw")) {
- const damage_vals = damage_string.split("-").map(Number);
+ const rawDamages = buildStats.get("damageRaw");
+ for (let i = 0; i < rawDamages.length; i++) {
+ const damage_vals = rawDamages[i].split("-").map(Number);
damages.push(damage_vals);
}
@@ -131,7 +134,10 @@ const spell_table = {
{ title: "Heal", cost: 6, parts: [
{ subtitle: "First Pulse", type: "heal", strength: 0.12 },
{ subtitle: "Second and Third Pulses", type: "heal", strength: 0.06 },
- { subtitle: "Total Heal", type: "heal", strength: 0.24, summary: true }
+ { subtitle: "Total Heal", type: "heal", strength: 0.24, summary: true },
+ { subtitle: "First Pulse (Ally)", type: "heal", strength: 0.20 },
+ { subtitle: "Second and Third Pulses (Ally)", type: "heal", strength: 0.1 },
+ { subtitle: "Total Heal (Ally)", type: "heal", strength: 0.4 }
] },
{ title: "Teleport", cost: 4, parts: [
{ subtitle: "Total Damage", type: "damage", multiplier: 100, conversion: [60, 0, 40, 0, 0, 0], summary: true },
@@ -150,9 +156,15 @@ const spell_table = {
{ subtitle: "Explosion Damage", type: "damage", multiplier: 130, conversion: [100, 0, 0, 0, 0, 0]},
{ subtitle: "Total Damage", type: "total", factors: [1, 1], summary: true },
] },
- { title: "Charge", cost: 4, parts: [
- { subtitle: "Total Damage", type: "damage", multiplier: 150, conversion: [60, 0, 0, 0, 40, 0], summary: true },
- ] },
+ { title: "Charge", cost: 4, variants: {
+ DEFAULT: [
+ { subtitle: "Total Damage", type: "damage", multiplier: 150, conversion: [60, 0, 0, 0, 40, 0], summary: true }
+ ],
+ RALLY: [
+ { subtitle: "Self Heal", type: "heal", strength: 0.1, summary: true },
+ { subtitle: "Ally Heal", type: "heal", strength: 0.15 }
+ ]
+ } },
{ title: "Uppercut", cost: 9, parts: [
{ subtitle: "First Damage", type: "damage", multiplier: 300, conversion: [70, 20, 10, 0, 0, 0] },
{ subtitle: "Fireworks Damage", type: "damage", multiplier: 50, conversion: [60, 0, 40, 0, 0, 0] },
@@ -165,10 +177,16 @@ const spell_table = {
] },
],
"bow": [
- { title: "Arrow Storm", cost: 6, parts: [
+ { title: "Arrow Storm", cost: 6, variants: {
+ DEFAULT: [
{ subtitle: "Total Damage", type: "damage", multiplier: 600, conversion: [60, 0, 25, 0, 15, 0], summary: true },
- { subtitle: "Per Arrow", type: "damage", multiplier: 10, conversion: [60, 0, 25, 0, 15, 0]},
- ] },
+ { subtitle: "Per Arrow (60)", type: "damage", multiplier: 10, conversion: [60, 0, 25, 0, 15, 0]}
+ ],
+ HAWKEYE: [
+ { subtitle: "Total Damage (Hawkeye)", type: "damage", multiplier: 400, conversion: [60, 0, 25, 0, 15, 0], summary: true },
+ { subtitle: "Per Arrow (5)", type: "damage", multiplier: 80, conversion: [60, 0, 25, 0, 15, 0]}
+ ],
+ } },
{ title: "Escape", cost: 3, parts: [
{ subtitle: "Landing Damage", type: "damage", multiplier: 100, conversion: [50, 0, 0, 0, 0, 50], summary: true },
] },
@@ -192,10 +210,16 @@ const spell_table = {
{ subtitle: "Fatality", type: "damage", multiplier: 120, conversion: [20, 0, 30, 50, 0, 0] },
{ subtitle: "Total Damage", type: "total", factors: [10, 1], summary: true },
] },
- { title: "Smoke Bomb", cost: 8, parts: [
- { subtitle: "Tick Damage", type: "damage", multiplier: 60, conversion: [45, 25, 0, 0, 0, 30] },
+ { title: "Smoke Bomb", cost: 8, variants: {
+ DEFAULT: [
+ { subtitle: "Tick Damage (10 max)", type: "damage", multiplier: 60, conversion: [45, 25, 0, 0, 0, 30] },
{ subtitle: "Total Damage", type: "damage", multiplier: 600, conversion: [45, 25, 0, 0, 0, 30], summary: true },
- ] },
+ ],
+ CHERRY_BOMBS: [
+ { subtitle: "Total Damage (Cherry Bombs)", type: "damage", multiplier: 330, conversion: [45, 25, 0, 0, 0, 30], summary: true },
+ { subtitle: "Per Bomb", type: "damage", multiplier: 110, conversion: [45, 25, 0, 0, 0, 30] }
+ ]
+ } },
],
"relik": [
{ title: "Totem", cost: 4, parts: [
diff --git a/display.js b/display.js
index ac8f547..c69205e 100644
--- a/display.js
+++ b/display.js
@@ -1,4 +1,4 @@
-let nonRolledIDs = ["name", "displayName", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "defReq", "agiReq","str", "dex", "int", "agi", "def", "fixID", "category", "id", "skillpoints", "reqs", "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_"];
+let nonRolledIDs = ["name", "displayName", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "defReq", "agiReq","str", "dex", "int", "agi", "def", "fixID", "category", "id", "skillpoints", "reqs", "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", "majorIds"];
let rolledIDs = ["hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd"];
let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ];
let colorMap = new Map(
@@ -497,12 +497,12 @@ function displayExpandedItem(item, parent_id){
"eSteal",
"gXp", "gSpd",
"#ldiv",
- "!elemental",
+ "majorIds",
"slots",
- "!elemental",
"set",
"quest",
- "restrict"];
+ "restrict"
+ ];
// Clear the parent div.
setHTML(parent_id, "");
@@ -511,7 +511,8 @@ function displayExpandedItem(item, parent_id){
let active_elem;
let fix_id = item.has("fixID") && item.get("fixID");
let elemental_format = false;
- for (const command of display_commands) {
+ for (let i = 0; i < display_commands.length; i++) {
+ const command = display_commands[i];
if (command.charAt(0) === "#") {
if (command === "#cdiv") {
active_elem = document.createElement('div');
@@ -561,6 +562,11 @@ function displayExpandedItem(item, parent_id){
powderSuffix.textContent = "]";
p_elem.appendChild(powderSuffix);
active_elem.appendChild(p_elem);
+ } else if (id === "majorIds") {
+ let p_elem = document.createElement("p");
+ p_elem.classList.add("itemp");
+ p_elem.textContent = "Major IDs: " + item.get(id).toString();
+ active_elem.appendChild(p_elem);
} else {
let p_elem;
if ( !(item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") && (!skp_order.includes(id)) || (skp_order.includes(id) && item.get("tier") !== "Crafted" && active_elem.nodeName === "DIV") ) { //skp warp
@@ -720,12 +726,12 @@ function displayExpandedItem(item, parent_id){
effects = powderSpecial["armorSpecialEffects"];
specialTitle.textContent += powderSpecial["armorSpecialName"] + ": ";
}
- for (const [key,value] of effects) {
- if (key !== "Description") {
+ for (let i = 0; i < effects.length; i++) {
+ if (effects[i][0] !== "Description") {
let effect = document.createElement("p");
effect.classList.add("itemp");
- effect.textContent += key + ": " + value[power] + specialSuffixes.get(key);
- if(key === "Damage"){
+ effect.textContent += effects[i][0] + ": " + effects[i][1][power] + specialSuffixes.get(effects[i][0]);
+ if(effects[i][0] === "Damage"){
effect.textContent += elementIcons[skp_elements.indexOf(element)];
}
if (element === "w") {
@@ -1824,7 +1830,21 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell
part_divavg.classList.add("nomargin");
overallparent_elem.append(part_divavg);
- for (const part of spell.parts) {
+ let spell_parts;
+ if (spell.parts) {
+ spell_parts = spell.parts;
+ }
+ else {
+ spell_parts = spell.variants.DEFAULT;
+ for (const majorID of stats.get("activeMajorIDs")) {
+ if (majorID in spell.variants) {
+ spell_parts = spell.variants[majorID];
+ break;
+ }
+ }
+ }
+
+ for (const part of spell_parts) {
parent_elem.append(document.createElement("br"));
let part_div = document.createElement("p");
parent_elem.append(part_div);
diff --git a/items.html b/items.html
index e0ebc5f..79fe83e 100644
--- a/items.html
+++ b/items.html
@@ -44,6 +44,9 @@
+
diff --git a/items_2.html b/items_2.html
new file mode 100644
index 0000000..981901c
--- /dev/null
+++ b/items_2.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+ WynnAtlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/items_2.js b/items_2.js
new file mode 100644
index 0000000..bbe96f2
--- /dev/null
+++ b/items_2.js
@@ -0,0 +1,193 @@
+// represents a field containing a query expression string
+class ExprField {
+ constructor(fieldId, errorTextId, compiler) {
+ this.field = document.getElementById(fieldId);
+ this.errorText = document.getElementById(errorTextId);
+ this.compiler = compiler;
+ this.output = null;
+ this.text = null;
+ }
+
+ get value() {
+ return this.field.value;
+ }
+
+ compile() {
+ if (this.value === this.text) return false;
+ this.text = this.value;
+ this.errorText.innerText = '';
+ try {
+ this.output = this.compiler(this.text);
+ } catch (e) {
+ this.errorText.innerText = e.message;
+ this.output = null;
+ }
+ return true;
+ }
+}
+
+function compareLexico(ia, keysA, ib, keysB) {
+ for (let i = 0; i < keysA.length; i++) { // assuming keysA and keysB are the same length
+ let aKey = keysA[i], bKey = keysB[i];
+ if (typeof aKey !== typeof bKey) throw new Error(`Incomparable types ${typeof aKey} and ${typeof bKey}`); // can this even happen?
+ switch (typeof aKey) {
+ case 'string':
+ aKey = aKey.toLowerCase();
+ bKey = bKey.toLowerCase();
+ if (aKey < bKey) return -1;
+ if (aKey > bKey) return 1;
+ break;
+ case 'number': // sort numeric stuff in reverse order
+ if (aKey < bKey) return 1;
+ if (aKey > bKey) return -1;
+ break;
+ default:
+ throw new Error(`Incomparable type ${typeof aKey}`);
+ }
+ }
+ return ib.lvl - ia.lvl;
+}
+
+function stringify(v) {
+ return typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : v;
+}
+
+function init() {
+ const itemList = document.getElementById('item-list');
+ const itemListFooter = document.getElementById('item-list-footer');
+
+ // compile the search db from the item db
+ const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
+
+ // init item list elements
+ const ITEM_LIST_SIZE = 64;
+ const itemEntries = [];
+ for (let i = 0; i < ITEM_LIST_SIZE; i++) {
+ const itemElem = document.createElement('div');
+ itemElem.classList.add('box');
+ itemElem.setAttribute('id', `item-entry-${i}`);
+ itemElem.style.display = 'none';
+ itemElem.style.width = '20vw';
+ itemElem.style.margin = '1vw';
+ itemElem.style.verticalAlign = 'top';
+ itemList.append(itemElem);
+ itemEntries.push(itemElem);
+ }
+
+ // the two search query input boxes
+ const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) {
+ const expr = compileQueryExpr(exprStr);
+ return expr !== null ? expr : (i, ie) => true;
+ });
+ const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) {
+ const subExprs = exprStr.split(';').map(compileQueryExpr).filter(f => f != null);
+ return function(i, ie) {
+ const sortKeys = [];
+ for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k](i, ie));
+ return sortKeys;
+ };
+ });
+
+ // updates the current search state from the search query input boxes
+ function updateSearch() {
+ // compile query expressions, aborting if nothing has changed or either fails to compile
+ const changed = searchFilterField.compile() | searchSortField.compile();
+ if (!changed || searchFilterField.output === null || searchSortField.output === null) return;
+
+ // update url query string
+ const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`
+ + `?f=${encodeURIComponent(searchFilterField.value)}&s=${encodeURIComponent(searchSortField.value)}`;
+ window.history.pushState({ path: newUrl }, '', newUrl);
+
+ // hide old search results
+ itemListFooter.innerText = '';
+ for (const itemEntry of itemEntries) itemEntry.style.display = 'none';
+
+ // index and sort search results
+ const searchResults = [];
+ try {
+ for (let i = 0; i < searchDb.length; i++) {
+ const item = searchDb[i][0], itemExp = searchDb[i][1];
+ if (checkBool(searchFilterField.output(item, itemExp))) {
+ searchResults.push({ item, itemExp, sortKeys: searchSortField.output(item, itemExp) });
+ }
+ }
+ } catch (e) {
+ searchFilterField.errorText.innerText = e.message;
+ return;
+ }
+ if (searchResults.length === 0) {
+ itemListFooter.innerText = 'No results!';
+ return;
+ }
+ try {
+ searchResults.sort((a, b) => compareLexico(a.item, a.sortKeys, b.item, b.sortKeys));
+ } catch (e) {
+ searchSortField.errorText.innerText = e.message;
+ return;
+ }
+
+ // display search results
+ const searchMax = Math.min(searchResults.length, ITEM_LIST_SIZE);
+ for (let i = 0; i < searchMax; i++) {
+ const result = searchResults[i];
+ itemEntries[i].style.display = 'inline-block';
+ displayExpandedItem(result.itemExp, `item-entry-${i}`);
+ if (result.sortKeys.length > 0) {
+ const sortKeyListContainer = document.createElement('div');
+ sortKeyListContainer.classList.add('itemleft');
+ const sortKeyList = document.createElement('ul');
+ sortKeyList.classList.add('itemp', 'T0');
+ sortKeyList.style.marginLeft = '1.75em';
+ sortKeyListContainer.append(sortKeyList);
+ for (let j = 0; j < result.sortKeys.length; j++) {
+ const sortKeyElem = document.createElement('li');
+ sortKeyElem.innerText = stringify(result.sortKeys[j]);
+ sortKeyList.append(sortKeyElem);
+ }
+ itemEntries[i].append(sortKeyListContainer);
+ }
+ }
+ if (searchMax < searchResults.length) {
+ itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
+ }
+ }
+
+ // updates the search state from the input boxes after a brief delay, to prevent excessive DOM updates
+ let updateSearchTask = null;
+ function scheduleSearchUpdate() {
+ if (updateSearchTask !== null) {
+ clearTimeout(updateSearchTask);
+ }
+ updateSearchTask = setTimeout(() => {
+ updateSearchTask = null;
+ updateSearch();
+ }, 500);
+ }
+ searchFilterField.field.addEventListener('input', e => scheduleSearchUpdate());
+ searchSortField.field.addEventListener('input', e => scheduleSearchUpdate());
+
+ // parse query string, display initial search results
+ if (window.location.search.startsWith('?')) {
+ for (const entryStr of window.location.search.substring(1).split('&')) {
+ const ndx = entryStr.indexOf('=');
+ if (ndx !== -1) {
+ switch (entryStr.substring(0, ndx)) {
+ case 'f':
+ searchFilterField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
+ break;
+ case 's':
+ searchSortField.field.value = decodeURIComponent(entryStr.substring(ndx + 1));
+ break;
+ }
+ }
+ }
+ }
+ updateSearch();
+
+ // focus the query filter text box
+ searchFilterField.field.focus();
+ searchFilterField.field.select();
+}
+
+load_init(init);
diff --git a/load.js b/load.js
index ae7b62a..1a195fb 100644
--- a/load.js
+++ b/load.js
@@ -1,4 +1,4 @@
-const DB_VERSION = 31;
+const DB_VERSION = 32;
// @See https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/video-store/index.js
let db;
@@ -55,6 +55,9 @@ function clean_item(item) {
item.skillpoints = [item.str, item.dex, item.int, item.def, item.agi];
item.has_negstat = item.str < 0 || item.dex < 0 || item.int < 0 || item.def < 0 || item.agi < 0;
item.reqs = [item.strReq, item.dexReq, item.intReq, item.defReq, item.agiReq];
+ if (item.slots === undefined) {
+ item.slots = 0
+ }
}
}
diff --git a/options.txt b/options.txt
new file mode 100644
index 0000000..ec2836e
--- /dev/null
+++ b/options.txt
@@ -0,0 +1,152 @@
+Parser specification:
+
+/*
+ * disj := conj "|" disj
+ * | conj
+ *
+ * conj := cmp "&" conj
+ * | cmpEq
+ *
+ * cmpEq := cmpRel "=" cmpEq
+ * | cmpRel "?=" prim
+ * | cmpRel "!=" cmpEq
+ *
+ * cmpRel := sum "<=" cmpRel
+ * | sum "<" cmpRel
+ * | sum ">" cmpRel
+ * | sum ">=" cmpRel
+ * | sum
+ *
+ * sum := prod "+" sum
+ * | prod "-" sum
+ * | prod
+ *
+ * prod := exp "*" prod
+ * | exp "/" prod
+ * | exp
+ *
+ * exp := unary "^" exp
+ * | unary
+ *
+ * unary := "-" unary
+ * | "!" unary
+ * | prim
+ *
+ * prim := nLit
+ * | bLit
+ * | sLit
+ * | ident "(" [disj ["," disj...]] ")"
+ * | ident
+ * | "(" disj ")"
+ */
+
+Basically just type math. You can use "-" to negate things (to sort by ascending order for example), use & (and) and | (or) to combine search filters, or use ! (not) to invert filters.
+
+Use spaces between arguments I guess, sometimes its picky
+
+
+
+Special operator: "?=" is used to find a "includes" relation -- for example:
+
+ name ?= "blue"
+
+will find items whose name includes the strong "blue" (not case sensitive).
+
+
+
+Below is a list of all the options.
+Left of colon is what you type into the search bar (sometimes multiple things can alias to the same values), right side is what it represents.
+
+'name': item name
+'type': item type (helmet, chestplate, leggings, boots, ring, bracelet, necklace, wand, bow, dagger, spear, relik)
+['cat', 'category']: item category (armor, accessory, weapon)
+['rarityname', 'raritystr', 'tiername', 'tierstr']: item tier string (normal, unique, set, rare, legendary, fabled, mythic)
+['rarity', 'tier']: item tier number (0 = normal, 6 = mythic
+
+['level', 'lvl', 'combatlevel', 'combatlvl']: item level req
+['strmin', 'strreq']: Item str req
+['dexmin', 'dexreq']: Item dex req
+['intmin', 'intreq']: Item int req
+['defmin', 'defreq']: Item def req
+['agimin', 'agireq']: Item agi req
+['summin', 'sumreq', 'totalmin', 'totalreq']: Item total req
+
+'str': Item str bonus
+'dex': Item dex bonus
+'int': Item int bonus
+'def': Item def bonus
+'agi': Item agi bonus
+['skillpoints', 'skillpts', 'attributes', 'attrs']: Sum(item skill points bonus)
+
+['neutraldmg', 'neutraldam', 'ndmg', 'ndam']: Item Neutral Damage, Average
+['earthdmg', 'earthdam', 'edmg', 'edam']: Item Earth Damage, Average
+['thunderdmg', 'thunderdam', 'tdmg', 'tdam']: Item Thunder Damage, Average
+['waterdmg', 'waterdam', 'wdmg', 'wdam']: Item Water Damage, Average
+['firedmg', 'firedam', 'fdmg', 'fdam']: Item Fire Damage, Average
+['airdmg', 'airdam', 'admg', 'adam']: Item Air Damage, Average
+['sumdmg', 'sumdam', 'totaldmg', 'totaldam']: Item Total Damage, Average
+
+['earthdmg%', 'earthdam%', 'edmg%', 'edam%', 'edampct']: Earth Damage Bonus
+['thunderdmg%', 'thunderdam%', 'tdmg%', 'tdam%', 'tdampct']: Thunder Damage Bonus
+['waterdmg%', 'waterdam%', 'wdmg%', 'wdam%', 'wdampct']: Water Damage Bonus
+['firedmg%', 'firedam%', 'fdmg%', 'fdam%', 'fdampct']: Fire Damage Bonus
+['airdmg%', 'airdam%', 'admg%', 'adam%', 'adampct']: Air Damage Bonus
+['sumdmg%', 'sumdam%', 'totaldmg%', 'totaldam%', 'sumdampct', 'totaldampct']: Sum damages %
+
+['mainatkdmg', 'mainatkdam', 'mainatkdmg%', 'mainatkdam%', 'meleedmg', 'meleedam', 'meleedmg%', 'meleedam%', 'mdpct']: Melee Damage Bonus (%)
+['mainatkrawdmg', 'mainatkrawdam', 'mainatkneutraldmg', 'mainatkneutraldam','meleerawdmg', 'meleerawdam', 'meleeneutraldmg', 'meleeneutraldam', 'mdraw']: Melee Damage (Raw)
+['spelldmg', 'spelldam', 'spelldmg%', 'spelldam%', 'sdpct']: Spell Damage (%)
+['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw']: Spell Damage (Raw)
+['attackspeed', 'atkspd']: Item Attack Speed
+['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'attackspeed+', 'atkspd+', 'atktier']: Attack Speed Bonus
+['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier']: Total Attack Speed (Base speed + bonus)
+
+['earthdef', 'edef']: Earth Defense Raw
+['thunderdef', 'tdef']: Thunder Defense Raw
+['waterdef', 'wdef']: Water Defense Raw
+['firedef', 'fdef']: Fire Defense Raw
+['airdef', 'adef']: Air Defense Raw
+['sumdef', 'totaldef']: Total Defense Raw
+
+['earthdef%', 'edef%', 'edefpct']: Total Defense %
+['thunderdef%', 'tdef%', 'tdefpct']: Total Defense %
+['waterdef%', 'wdef%', 'wdefpct']: Total Defense %
+['firedef%', 'fdef%', 'fdefpct']: Total Defense %
+['airdef%', 'adef%', 'adefpct']: Total Defense %
+['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct']: Total Defense %
+
+['health', 'hp']: Health
+['bonushealth', 'healthid', 'bonushp', 'hpid', 'health+', 'hp+', 'hpbonus']: Health bonus
+['sumhealth', 'sumhp', 'totalhealth', 'totalhp']: Total Health (health + health bonus)
+
+['hpregen', 'hpr', 'hr', 'hprraw']: Raw Health Regen
+['hpregen%', 'hpr%', 'hr%', 'hprpct']: Health Regen %
+['lifesteal', 'ls']: Lifesteal
+['manaregen', 'mr']: Mana Regen
+['manasteal', 'ms']: Mana Steal
+
+['walkspeed', 'movespeed', 'ws', 'spd']: Walk Speed Bonus
+'sprint': Sprint Bonus
+['sprintregen', 'sprintreg']: Sprint Regen
+['jumpheight', 'jh']: Jump Height
+
+['spellcost1', 'rawspellcost1', 'spcost1', 'spraw1']: 1st Spell Cost Raw (min roll)
+['spellcost1%', 'spcost1%', 'sppct1']: 1st Spell Cost % (min roll)
+['spellcost2', 'rawspellcost2', 'spcost2', 'spraw2']: 2nd Spell Cost Raw (min roll)
+['spellcost2%', 'spcost2%', 'sppct2']: 2nd Spell Cost % (min roll)
+['spellcost3', 'rawspellcost3', 'spcost3', 'spraw3']: 3rd Spell Cost Raw (min roll)
+['spellcost3%', 'spcost3%', 'sppct3']: 3rd Spell Cost % (min roll)
+['spellcost4', 'rawspellcost4', 'spcost4', 'spraw4']: 4th Spell Cost Raw (min roll)
+['spellcost4%', 'spcost4%', 'sppct4']: 4th Spell Cost % (min roll)
+['sumspellcost', 'totalspellcost', 'sumrawspellcost', 'totalrawspellcost', 'sumspcost', 'totalspcost', 'sumspraw', 'totalspraw']: Sum (Spell Cost Raw)
+['sumspellcost%', 'totalspellcost%', 'sumspcost%', 'totalspcost%', 'sumsppct', 'totalsppct']: Sum (Spell Cost %)
+
+['exploding', 'expl', 'expd']: Exploding
+'poison': Poison
+'thorns': Thorns
+['reflection', 'refl', 'ref']: Reflection
+['soulpointregen', 'spr', 'spregen']: Soul Point Regen
+['lootbonus', 'lb']: Loot Bonus
+['xpbonus', 'xpb', 'xb']: XP Bonus
+['stealing', 'esteal']: Stealing
+['powderslots', 'powders', 'slots', 'sockets']: # Powder Slots
diff --git a/query_2.js b/query_2.js
new file mode 100644
index 0000000..b5d5bf9
--- /dev/null
+++ b/query_2.js
@@ -0,0 +1,604 @@
+/*
+ * disj := conj "|" disj
+ * | conj
+ *
+ * conj := cmp "&" conj
+ * | cmpEq
+ *
+ * cmpEq := cmpRel "=" cmpEq
+ * | cmpRel "?=" prim
+ * | cmpRel "!=" cmpEq
+ *
+ * cmpRel := sum "<=" cmpRel
+ * | sum "<" cmpRel
+ * | sum ">" cmpRel
+ * | sum ">=" cmpRel
+ * | sum
+ *
+ * sum := prod "+" sum
+ * | prod "-" sum
+ * | prod
+ *
+ * prod := exp "*" prod
+ * | exp "/" prod
+ * | exp
+ *
+ * exp := unary "^" exp
+ * | unary
+ *
+ * unary := "-" unary
+ * | "!" unary
+ * | prim
+ *
+ * prim := nLit
+ * | bLit
+ * | sLit
+ * | ident "(" [disj ["," disj...]] ")"
+ * | ident
+ * | "(" disj ")"
+ */
+
+// a list of tokens indexed by a single pointer
+class TokenList {
+ constructor(tokens) {
+ this.tokens = tokens;
+ this.ptr = 0;
+ }
+
+ get here() {
+ if (this.ptr >= this.tokens.length) throw new Error('Reached end of expression');
+ return this.tokens[this.ptr];
+ }
+
+ advance(steps = 1) {
+ this.ptr = Math.min(this.ptr + steps, this.tokens.length);
+ }
+}
+
+// type casts
+function checkBool(v) {
+ if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
+ return v;
+}
+
+function checkNum(v) {
+ if (typeof v !== 'number') throw new Error(`Expected number, but got ${typeof v}`);
+ return v;
+}
+
+function checkStr(v) {
+ if (typeof v !== 'string') throw new Error(`Expected string, but got ${typeof v}`);
+ return v;
+}
+
+// properties of items that can be looked up
+const itemQueryProps = (function() {
+ const props = {};
+ function prop(names, getProp) {
+ if (Array.isArray(names)) {
+ for (name of names) {
+ props[name] = getProp;
+ }
+ } else {
+ props[names] = getProp;
+ }
+ }
+ function maxId(names, idKey) {
+ prop(names, (i, ie) => ie.get('maxRolls').get(idKey) || 0);
+ }
+ function minId(names, idKey) {
+ prop(names, (i, ie) => ie.get('minRolls').get(idKey) || 0);
+ }
+ function rangeAvg(names, getProp) {
+ prop(names, (i, ie) => {
+ const range = getProp(i, ie);
+ if (!range) return 0;
+ const ndx = range.indexOf('-');
+ return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2;
+ });
+ }
+ function map(names, comps, f) {
+ return prop(names, (i, ie) => {
+ const args = [];
+ for (let k = 0; k < comps.length; k++) args.push(comps[k](i, ie));
+ return f.apply(null, args);
+ });
+ }
+ function sum(names, ...comps) {
+ return map(names, comps, (...summands) => {
+ let total = 0;
+ for (let i = 0; i < summands.length; i++) total += summands[i];
+ return total;
+ });
+ }
+
+ prop('name', (i, ie) => i.displayName || i.name);
+ prop('type', (i, ie) => i.type);
+ prop(['cat', 'category'], (i, ie) => i.category);
+ const tierIndices = { Normal: 0, Unique: 1, Set: 2, Rare: 3, Legendary: 4, Fabled: 5, Mythic: 6 };
+ prop(['rarityname', 'raritystr', 'tiername', 'tierstr'], (i, ie) => i.tier);
+ prop(['rarity', 'tier'], (i, ie) => tierIndices[i.tier]);
+
+ prop(['level', 'lvl', 'combatlevel', 'combatlvl'], (i, ie) => i.lvl);
+ prop(['strmin', 'strreq'], (i, ie) => i.strReq);
+ prop(['dexmin', 'dexreq'], (i, ie) => i.dexReq);
+ prop(['intmin', 'intreq'], (i, ie) => i.intReq);
+ prop(['defmin', 'defreq'], (i, ie) => i.defReq);
+ prop(['agimin', 'agireq'], (i, ie) => i.agiReq);
+ sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin);
+
+ prop('str', (i, ie) => i.str);
+ prop('dex', (i, ie) => i.dex);
+ prop('int', (i, ie) => i.int);
+ prop('def', (i, ie) => i.def);
+ prop('agi', (i, ie) => i.agi);
+ sum(['skillpoints', 'skillpts', 'attributes', 'attrs'], props.str, props.dex, props.int, props.def, props.agi);
+
+ rangeAvg(['neutraldmg', 'neutraldam', 'ndmg', 'ndam'], (i, ie) => i.nDam);
+ rangeAvg(['earthdmg', 'earthdam', 'edmg', 'edam'], (i, ie) => i.eDam);
+ rangeAvg(['thunderdmg', 'thunderdam', 'tdmg', 'tdam'], (i, ie) => i.tDam);
+ rangeAvg(['waterdmg', 'waterdam', 'wdmg', 'wdam'], (i, ie) => i.wDam);
+ rangeAvg(['firedmg', 'firedam', 'fdmg', 'fdam'], (i, ie) => i.fDam);
+ rangeAvg(['airdmg', 'airdam', 'admg', 'adam'], (i, ie) => i.aDam);
+ sum(['sumdmg', 'sumdam', 'totaldmg', 'totaldam'], props.ndam, props.edam, props.tdam, props.wdam, props.fdam, props.adam);
+
+ maxId(['earthdmg%', 'earthdam%', 'edmg%', 'edam%', 'edampct'], 'eDamPct');
+ maxId(['thunderdmg%', 'thunderdam%', 'tdmg%', 'tdam%', 'tdampct'], 'tDamPct');
+ maxId(['waterdmg%', 'waterdam%', 'wdmg%', 'wdam%', 'wdampct'], 'wDamPct');
+ maxId(['firedmg%', 'firedam%', 'fdmg%', 'fdam%', 'fdampct'], 'fDamPct');
+ maxId(['airdmg%', 'airdam%', 'admg%', 'adam%', 'adampct'], 'aDamPct');
+ sum(['sumdmg%', 'sumdam%', 'totaldmg%', 'totaldam%', 'sumdampct', 'totaldampct'], props.edampct, props.tdampct, props.wdampct, props.fdampct, props.adampct);
+
+ maxId(['mainatkdmg', 'mainatkdam', 'mainatkdmg%', 'mainatkdam%', 'meleedmg', 'meleedam', 'meleedmg%', 'meleedam%', 'mdpct'], 'mdPct');
+ maxId(['mainatkrawdmg', 'mainatkrawdam', 'mainatkneutraldmg', 'mainatkneutraldam', 'meleerawdmg', 'meleerawdam', 'meleeneutraldmg', 'meleeneutraldam', 'mdraw'], 'mdRaw');
+ maxId(['spelldmg', 'spelldam', 'spelldmg%', 'spelldam%', 'sdpct'], 'sdPct');
+ maxId(['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw'], 'sdRaw');
+
+ const atkSpdIndices = { SUPER_SLOW: -3, VERY_SLOW: -2, SLOW: -1, NORMAL: 0, FAST: 1, VERY_FAST: 2, SUPER_FAST: 3 };
+ prop(['attackspeed', 'atkspd'], (i, ie) => i.atkSpd ? atkSpdIndices[i.atkSpd] : 0);
+ maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'attackspeed+', 'atkspd+', 'atktier'], 'atkTier');
+ sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier);
+
+ prop(['earthdef', 'edef'], (i, ie) => i.eDef || 0);
+ prop(['thunderdef', 'tdef'], (i, ie) => i.tDef || 0);
+ prop(['waterdef', 'wdef'], (i, ie) => i.wDef || 0);
+ prop(['firedef', 'fdef'], (i, ie) => i.fDef || 0);
+ prop(['airdef', 'adef'], (i, ie) => i.aDef || 0);
+ sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef);
+
+ maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct');
+ maxId(['thunderdef%', 'tdef%', 'tdefpct'], 'tDefPct');
+ maxId(['waterdef%', 'wdef%', 'wdefpct'], 'wDefPct');
+ maxId(['firedef%', 'fdef%', 'fdefpct'], 'fDefPct');
+ maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct');
+ sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct);
+
+ prop(['health', 'hp'], (i, ie) => i.hp || 0);
+ maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'health+', 'hp+', 'hpbonus'], 'hpBonus');
+ sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid);
+
+ maxId(['hpregen', 'hpr', 'hr', 'hprraw'], 'hprRaw');
+ maxId(['hpregen%', 'hpr%', 'hr%', 'hprpct'], 'hprPct');
+ maxId(['lifesteal', 'ls'], 'ls');
+ maxId(['manaregen', 'mr'], 'mr');
+ maxId(['manasteal', 'ms'], 'ms');
+
+ maxId(['walkspeed', 'movespeed', 'ws', 'spd'], 'spd');
+ maxId('sprint', 'sprint');
+ maxId(['sprintregen', 'sprintreg'], 'sprintReg');
+ maxId(['jumpheight', 'jh'], 'jh');
+
+ minId(['spellcost1', 'rawspellcost1', 'spcost1', 'spraw1'], 'spRaw1');
+ minId(['spellcost1%', 'spcost1%', 'sppct1'], 'spPct1');
+ minId(['spellcost2', 'rawspellcost2', 'spcost2', 'spraw2'], 'spRaw2');
+ minId(['spellcost2%', 'spcost2%', 'sppct2'], 'spPct2');
+ minId(['spellcost3', 'rawspellcost3', 'spcost3', 'spraw3'], 'spRaw3');
+ minId(['spellcost3%', 'spcost3%', 'sppct3'], 'spPct3');
+ minId(['spellcost4', 'rawspellcost4', 'spcost4', 'spraw4'], 'spRaw4');
+ minId(['spellcost4%', 'spcost4%', 'sppct4'], 'spPct4');
+ sum(['sumspellcost', 'totalspellcost', 'sumrawspellcost', 'totalrawspellcost', 'sumspcost', 'totalspcost', 'sumspraw', 'totalspraw'], props.spraw1, props.spraw2, props.spraw3, props.spraw4);
+ sum(['sumspellcost%', 'totalspellcost%', 'sumspcost%', 'totalspcost%', 'sumsppct', 'totalsppct'], props.sppct1, props.sppct2, props.sppct3, props.sppct4);
+
+ maxId(['exploding', 'expl', 'expd'], 'expd');
+ maxId('poison', 'poison');
+ maxId('thorns', 'thorns');
+ maxId(['reflection', 'refl', 'ref'], 'ref');
+ maxId(['soulpointregen', 'spr', 'spregen'], 'spRegen');
+ maxId(['lootbonus', 'lb'], 'lb');
+ maxId(['xpbonus', 'xpb', 'xb'], 'xpb');
+ maxId(['stealing', 'esteal'], 'eSteal');
+ prop(['powderslots', 'powders', 'slots', 'sockets'], (i, ie) => i.slots || 0);
+
+ return props;
+})();
+
+// functions that can be called in query expressions
+const itemQueryFuncs = {
+ max(args) {
+ if (args.length < 1) throw new Error('Not enough args to max()');
+ let runningMax = -Infinity;
+ for (let i = 0; i < args.length; i++) {
+ if (checkNum(args[i]) > runningMax) runningMax = args[i];
+ }
+ return runningMax;
+ },
+ min(args) {
+ if (args.length < 1) throw new Error('Not enough args to min()');
+ let runningMin = Infinity;
+ for (let i = 0; i < args.length; i++) {
+ if (checkNum(args[i]) < runningMin) runningMin = args[i];
+ }
+ return runningMin;
+ },
+ floor(args) {
+ if (args.length < 1) throw new Error('Not enough args to floor()');
+ return Math.floor(checkNum(args[0]));
+ },
+ ceil(args) {
+ if (args.length < 1) throw new Error('Not enough args to ceil()');
+ return Math.ceil(checkNum(args[0]));
+ },
+ round(args) {
+ if (args.length < 1) throw new Error('Not enough args to ceil()');
+ return Math.round(checkNum(args[0]));
+ },
+ sqrt(args) {
+ if (args.length < 1) throw new Error('Not enough args to ceil()');
+ return Math.sqrt(checkNum(args[0]));
+ },
+ abs(args) {
+ if (args.length < 1) throw new Error('Not enough args to ceil()');
+ return Math.abs(checkNum(args[0]));
+ },
+ contains(args) {
+ if (args.length < 2) throw new Error('Not enough args to contains()');
+ return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase());
+ },
+ atkspdmod(args) {
+ if (args.length < 1) throw new Error('Not enough args to atkSpdMod()');
+ switch (checkNum(args[0])) {
+ case 2: return 3.1;
+ case 1: return 2.5;
+ case 0: return 2.05;
+ case -1: return 1.5;
+ case -2: return 0.83;
+ }
+ if (args[0] <= -3) return 0.51;
+ if (args[0] >= 3) return 4.3;
+ throw new Error('Invalid argument to atkSpdMod()');
+ }
+};
+
+// the compiler itself
+const compileQueryExpr = (function() {
+ // tokenize an expression string
+ function tokenize(exprStr) {
+ exprStr = exprStr.trim();
+ const tokens = [];
+ let col = 0;
+ function pushSymbol(sym) {
+ tokens.push({ type: 'sym', sym });
+ col += sym.length;
+ }
+ while (col < exprStr.length) {
+ // parse fixed symbols, like operators and stuff
+ switch (exprStr[col]) {
+ case '(':
+ case ')':
+ case ',':
+ case '&':
+ case '|':
+ case '+':
+ case '-':
+ case '*':
+ case '/':
+ case '^':
+ case '=':
+ pushSymbol(exprStr[col]);
+ continue;
+ case '>':
+ pushSymbol(exprStr[col + 1] === '=' ? '>=' : '>');
+ continue;
+ case '<':
+ pushSymbol(exprStr[col + 1] === '=' ? '<=' : '<');
+ continue;
+ case '!':
+ pushSymbol(exprStr[col + 1] === '=' ? '!=' : '!');
+ continue;
+ case ' ': // ignore extra whitespace
+ ++col;
+ continue;
+ }
+ if (exprStr.slice(col, col+2) === "?=") {
+ pushSymbol("?=");
+ continue;
+ }
+ // parse a numeric literal
+ let m;
+ if ((m = /^\d+(?:\.\d*)?/.exec(exprStr.substring(col))) !== null) {
+ tokens.push({ type: 'num', value: parseFloat(m[0]) });
+ col += m[0].length;
+ continue;
+ }
+ // parse a string literal
+ if ((m = /^"([^"]+)"/.exec(exprStr.substring(col))) !== null) { // with double-quotes
+ tokens.push({ type: 'str', value: m[1] });
+ col += m[0].length;
+ continue;
+ }
+ if ((m = /^'([^']+)'/.exec(exprStr.substring(col))) !== null) { // with single-quotes
+ tokens.push({ type: 'str', value: m[1] });
+ col += m[0].length;
+ continue;
+ }
+ // parse an identifier or boolean literal
+ if ((m = /^\w[\w\d+%]*/.exec(exprStr.substring(col))) !== null) {
+ switch (m[0]) {
+ case 'true':
+ tokens.push({ type: 'bool', value: true });
+ col += 4;
+ continue;
+ case 'false':
+ tokens.push({ type: 'bool', value: false });
+ col += 5;
+ continue;
+ }
+ tokens.push({ type: 'id', id: m[0] });
+ col += m[0].length;
+ continue;
+ }
+ // if we reach here without successfully parsing a token, it's an error
+ throw new Error(`Could not parse character "${exprStr[col]}" at position ${col}`);
+ }
+ tokens.push({ type: 'eof' });
+ return new TokenList(tokens);
+ }
+
+ // parse tokens into an ast
+ function takeDisj(tokens) {
+ const left = takeConj(tokens);
+ if (tokens.here.type === 'sym' && tokens.here.sym === '|') {
+ tokens.advance();
+ const right = takeDisj(tokens);
+ return (i, ie) => checkBool(left(i, ie)) || checkBool(right(i, ie));
+ }
+ return left;
+ }
+
+ function takeConj(tokens) {
+ const left = takeCmpEq(tokens);
+ if (tokens.here.type === 'sym' && tokens.here.sym === '&') {
+ tokens.advance();
+ const right = takeConj(tokens);
+ return (i, ie) => checkBool(left(i, ie)) && checkBool(right(i, ie));
+ }
+ return left;
+ }
+
+ function takeCmpEq(tokens) {
+ const left = takeCmpRel(tokens);
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case '=': {
+ tokens.advance();
+ const right = takeCmpEq(tokens);
+ return (i, ie) => {
+ const a = left(i, ie), b = right(i, ie);
+ if (typeof a !== typeof b) return false;
+ switch (typeof a) {
+ case 'number':
+ return Math.abs(left(i, ie) - right(i, ie)) < 1e-4;
+ case 'boolean':
+ return a === b;
+ case 'string':
+ return a.toLowerCase() === b.toLowerCase();
+ }
+ throw new Error('???'); // wut
+ };
+ }
+ case '!=': {
+ tokens.advance();
+ const right = takeCmpEq(tokens);
+ return (i, ie) => {
+ const a = left(i, ie), b = right(i, ie);
+ if (typeof a !== typeof b) return false;
+ switch (typeof a) {
+ case 'number':
+ return Math.abs(left(i, ie) - right(i, ie)) >= 1e-4;
+ case 'boolean':
+ return a !== b;
+ case 'string':
+ return a.toLowerCase() !== b.toLowerCase();
+ }
+ throw new Error('???'); // wtf
+ };
+ }
+ case '?=': {
+ tokens.advance();
+ const right = takePrim(tokens);
+ return (i, ie) => {
+ const a = left(i, ie), b = right(i, ie);
+ if (typeof a !== typeof b) return false;
+ switch (typeof a) {
+ case 'number':
+ return Math.abs(left(i, ie) - right(i, ie)) < 1e-4;
+ case 'boolean':
+ return a === b;
+ case 'string':
+ return a.toLowerCase().includes(b.toLowerCase());
+ }
+ throw new Error('???'); // wtf
+ };
+ }
+ }
+ }
+ return left;
+ }
+
+ function takeCmpRel(tokens) {
+ const left = takeSum(tokens);
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case '<=': {
+ tokens.advance();
+ const right = takeCmpRel(tokens);
+ return (i, ie) => checkNum(left(i, ie)) <= checkNum(right(i, ie));
+ }
+ case '<': {
+ tokens.advance();
+ const right = takeCmpRel(tokens);
+ return (i, ie) => checkNum(left(i, ie)) < checkNum(right(i, ie));
+ }
+ case '>': {
+ tokens.advance();
+ const right = takeCmpRel(tokens);
+ return (i, ie) => checkNum(left(i, ie)) > checkNum(right(i, ie));
+ }
+ case '>=': {
+ tokens.advance();
+ const right = takeCmpRel(tokens);
+ return (i, ie) => checkNum(left(i, ie)) >= checkNum(right(i, ie));
+ }
+ }
+ }
+ return left;
+ }
+
+ function takeSum(tokens) {
+ const left = takeProd(tokens);
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case '+': {
+ tokens.advance();
+ const right = takeSum(tokens);
+ return (i, ie) => checkNum(left(i, ie)) + checkNum(right(i, ie));
+ }
+ case '-': {
+ tokens.advance();
+ const right = takeSum(tokens);
+ return (i, ie) => checkNum(left(i, ie)) - checkNum(right(i, ie));
+ }
+ }
+ }
+ return left;
+ }
+
+ function takeProd(tokens) {
+ const left = takeExp(tokens);
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case '*': {
+ tokens.advance();
+ const right = takeProd(tokens);
+ return (i, ie) => checkNum(left(i, ie)) * checkNum(right(i, ie));
+ }
+ case '/': {
+ tokens.advance();
+ const right = takeProd(tokens);
+ return (i, ie) => checkNum(left(i, ie)) / checkNum(right(i, ie));
+ }
+ }
+ }
+ return left;
+ }
+
+ function takeExp(tokens) {
+ const left = takeUnary(tokens);
+ if (tokens.here.type === 'sym' && tokens.here.sym === '^') {
+ tokens.advance();
+ const right = takeExp(tokens);
+ return (i, ie) => checkNum(left(i, ie)) ** checkNum(right(i, ie));
+ }
+ return left;
+ }
+
+ function takeUnary(tokens) {
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case '-': {
+ tokens.advance();
+ const operand = takeUnary(tokens);
+ return (i, ie) => -checkNum(operand(i, ie));
+ }
+ case '!': {
+ tokens.advance();
+ const operand = takeUnary(tokens);
+ return (i, ie) => !checkBool(operand(i, ie));
+ }
+ }
+ }
+ return takePrim(tokens);
+ }
+
+ function takePrim(tokens) {
+ switch (tokens.here.type) {
+ case 'num': {
+ const lit = tokens.here.value;
+ tokens.advance();
+ return (i, ie) => lit;
+ }
+ case 'bool': {
+ const lit = tokens.here.value;
+ tokens.advance();
+ return (i, ie) => lit;
+ }
+ case 'str': {
+ const lit = tokens.here.value;
+ tokens.advance();
+ console.log(lit);
+ return (i, ie) => lit;
+ }
+ case 'id':
+ const id = tokens.here.id;
+ tokens.advance();
+ if (tokens.here.type === 'sym' && tokens.here.sym === '(') { // it's a function call
+ tokens.advance();
+ const argExprs = [];
+ if (tokens.here.type !== 'sym' || tokens.here.sym !== ')') {
+ arg_iter: // collect arg expressions, if there are any
+ while (true) {
+ argExprs.push(takeDisj(tokens));
+ if (tokens.here.type === 'sym') {
+ switch (tokens.here.sym) {
+ case ')':
+ tokens.advance();
+ break arg_iter;
+ case ',':
+ tokens.advance();
+ continue;
+ }
+ }
+ throw new Error(`Expected "," or ")", but got ${JSON.stringify(tokens.here)}`);
+ }
+ }
+ const func = itemQueryFuncs[id.toLowerCase()];
+ if (!func) throw new Error(`Unknown function: ${id}`);
+ return (i, ie) => {
+ const args = [];
+ for (let k = 0; k < argExprs.length; k++) args.push(argExprs[k](i, ie));
+ return func(args);
+ };
+ } else { // not a function call
+ const prop = itemQueryProps[id.toLowerCase()];
+ if (!prop) throw new Error(`Unknown property: ${id}`);
+ return prop;
+ }
+ case 'sym':
+ if (tokens.here.sym === '(') {
+ tokens.advance();
+ const expr = takeDisj(tokens);
+ if (tokens.here.type !== 'sym' || tokens.here.sym !== ')') throw new Error('Bracket mismatch');
+ tokens.advance();
+ return expr;
+ }
+ break;
+ }
+ throw new Error(tokens.here.type === 'eof' ? 'Reached end of expression' : `Unexpected token: ${JSON.stringify(tokens.here)}`);
+ }
+
+ // full compilation function, with extra safety for empty input strings
+ return function(exprStr) {
+ const tokens = tokenize(exprStr);
+ return tokens.tokens.length <= 1 ? null : takeDisj(tokens);
+ };
+})();
diff --git a/test_regress.txt b/test_regress.txt
index 9fb90ad..cd78958 100644
--- a/test_regress.txt
+++ b/test_regress.txt
@@ -3,3 +3,4 @@ Version 1: http://localhost:8000/#1_0690px0CE0QR0050050K40BR0Qk00001004fI
Version 2: http://localhost:8000/#2_2SG2SH2SI2SJ2SK0K22SM2SN05n000t210t0000000
Version 3: https://localhost:8000/#3_0250px0uX0K50K20OK0OJ00A0Qe1z+m21001M1g0000100nZ6
Version 3: https://localhost:8000/#3_0K60iv0CE0Qt0BK0BK0K40Jc0uG160V050o1L1g00001003C6
+Version 4: https://localhost:8000/#4_-1+W+W+W+W+W+W9g91-1+W+W+W+W+W+W9d91-1+W+W+W+W+W+W9i9--1+W+W+W+W+W+W9a91-1+W+W+W+W+W+W9m91-1+W+W+W+W+W+W9m91-1+W+W+W+W+W+W9c91-1+W+W+W+W+W+W9n91-1+W+W+W+W+W+W9q9100000000001g000010036C