fixed merge issues

This commit is contained in:
ferricles 2021-01-30 20:07:23 -08:00
commit 48ccac6022
15 changed files with 1241 additions and 124 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
*.swp
*.bat
sets/
.idea/
*.iml

View file

@ -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) {

View file

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

View file

@ -369,6 +369,8 @@ class Craft{
statMap.get("maxRolls").set(id,0);
}
}
statMap.set("crafted", true);
this.statMap = statMap;
}
}
}

View file

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

View file

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

View file

@ -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: [

View file

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

View file

@ -44,6 +44,9 @@
<div class = "headerright">
</div>
<div class="center" id="advanced">
<a href="./items_2.html">Advanced Search</a>
</div>
</header>
</div>
<br>

77
items_2.html Normal file
View file

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html scroll-behavior="smooth">
<head>
<!-- nunito font, copying wynndata -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.png">
<link rel="manifest" href="manifest.json">
<title>WynnAtlas</title>
</head>
<body class="all" style="overflow-y: scroll">
<header class = "header nomarginp">
<div class = "headerleft">
<a href = "./" class = "nomarginp iconlink tooltip">
<img src = "/media/icons/builder.png" class = "left linkoptions headericon">
</img>
<div class = "tooltiptext center">WynnBuilder</div>
</a>
<a href = "./crafter.html" class = "nomarginp iconlink tooltip">
<img src = "/media/icons/crafter.png" class = "left linkoptions headericon">
</img>
<div class = "tooltiptext center">WynnCrafter</div>
</a>
<a href = "./items_2.html" class = "nomarginp iconlink tooltip">
<img src = "/media/icons/searcher.png" class = "left linkoptions headericon">
</img>
<div class = "tooltiptext center">WynnAtlas</div>
</a>
</div>
<div class = "headercenter">
<div >
<p class = "itemp" id = "header">WynnAtlas</p>
</div>
</div>
<div class = "headerright">
</div>
</header>
<div class="center" id="credits">
<a href="credits.txt">Additional credits</a>
</div>
<div class="center" id="help">
<a href="options.txt">Search Guide</a>
</div>
<div class="center" id="main" style="padding: 2%">
<div id="search-container" style="margin-bottom: 1.5%">
<div class="left" id="search-filter" style="display: inline-block; vertical-align: top">
<label for="search-filter-field">Filter By:</label>
<br>
<input id="search-filter-field" type="text" placeholder="name ?= &quot;blue&quot; & str >= 15 & dex >= 10" style="width: 25vw; padding: 8px">
<br>
<div id="search-filter-error" style="color: #ff0000"></div>
</div>
<div class="left" id="search-sort" style="display: inline-block; vertical-align: top">
<label for="search-sort-field">Sort By:</label>
<br>
<input id="search-sort-field" type="text" placeholder="str + dex; meleerawdmg + spellrawdmg" style="width: 25vw; padding: 8px">
<br>
<div id="search-sort-error" style="color: #ff0000"></div>
</div>
</div>
<div id="item-list-container">
<div class="left" id="item-list" style="display: flex; flex-flow: row wrap"></div>
<div class="center" id="item-list-footer"></div>
</div>
</div>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="build_utils.js"></script>
<script type="text/javascript" src="damage_calc.js"></script>
<script type="text/javascript" src="display.js"></script>
<script type="text/javascript" src="query_2.js"></script>
<script type="text/javascript" src="load.js"></script>
<script type="text/javascript" src="items_2.js"></script>
</body>
</html>

193
items_2.js Normal file
View file

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

View file

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

152
options.txt Normal file
View file

@ -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

604
query_2.js Normal file
View file

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

View file

@ -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