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 @@
+
+ Advanced Search +

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 + + +
+
+ + + +
WynnBuilder
+
+ + + +
WynnCrafter
+
+ + + +
WynnAtlas
+
+
+
+
+ +
+
+
+ +
+
+
+ Additional credits +
+
+ Search Guide +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+ + + + + + + + + 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