Default to old wynnatlas
This commit is contained in:
parent
fd1e54bf81
commit
15670c524c
6 changed files with 1237 additions and 806 deletions
132
items.html
132
items.html
|
@ -1,16 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html scroll-behavior="smooth">
|
||||
<head>
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=device-width, user-scalable=no" />
|
||||
<!-- 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="stylesheet" href="items.css">
|
||||
<link rel="stylesheet" media="screen and (min-width: 1100px)" href="items-wide.css"/>
|
||||
<link rel="stylesheet" media="screen and (max-width: 1099px)" href="items-narrow.css"/>
|
||||
<link rel="icon" href="favicon.png">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<title>WynnAtlas</title>
|
||||
<title>Wynn Clientside</title>
|
||||
</head>
|
||||
<body class="all" style="overflow-y: scroll">
|
||||
<body class="all">
|
||||
<div class="center">
|
||||
<header class = "header nomarginp">
|
||||
<div class = "headerleft">
|
||||
<a href = "./" class = "nomarginp iconlink tooltip">
|
||||
|
@ -37,41 +44,104 @@
|
|||
<div class = "headerright">
|
||||
|
||||
</div>
|
||||
<div class="center" id="advanced">
|
||||
<a href="./items_2.html">Advanced Search</a>
|
||||
</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 ?= "blue" & str >= 15 & dex >= 10" style="width: 25vw; padding: 8px">
|
||||
<br>
|
||||
<div id="search-filter-error" style="color: #ff0000"></div>
|
||||
<br>
|
||||
<br>
|
||||
<div class="itemsearch">
|
||||
<div class="searchbox">
|
||||
<div class="left">
|
||||
<label for="name-choice">Name:</label><br>
|
||||
<input class="searchinput" type="text" id="name-choice" name="name-choice" placeholder="Item name (case insensitive)" tabindex="1"/>
|
||||
<p class="error"></p>
|
||||
</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 class="left">
|
||||
<label for="category-choice">Category:</label><br>
|
||||
<input class="searchinput" list="category-items" id="category-choice" name="category-choice" placeholder="ALL" tabindex="2"/>
|
||||
<datalist id="category-items">
|
||||
<option value="ALL">
|
||||
<option value="armor">
|
||||
<option value="helmet">
|
||||
<option value="chestplate">
|
||||
<option value="leggings">
|
||||
<option value="boots">
|
||||
<option value="accessory">
|
||||
<option value="ring">
|
||||
<option value="bracelet">
|
||||
<option value="necklace">
|
||||
<option value="weapon">
|
||||
<option value="wand">
|
||||
<option value="spear">
|
||||
<option value="bow">
|
||||
<option value="dagger">
|
||||
<option value="relik">
|
||||
</datalist>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="left">
|
||||
<label for="rarity-choice">Rarity:</label><br>
|
||||
<input class="searchinput" list="rarity-items" id="rarity-choice" name="rarity-choice" placeholder="ANY" tabindex="3"/>
|
||||
<datalist id="rarity-items">
|
||||
<option value="ANY">
|
||||
<option value="Normal">
|
||||
<option value="Unique">
|
||||
<option value="Rare">
|
||||
<option value="Legendary">
|
||||
<option value="Fabled">
|
||||
<option value="Mythic">
|
||||
<option value="Sane">
|
||||
</datalist>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="left">
|
||||
<label for="level-choice">Level:</label><br>
|
||||
<input class="searchinput" type="text" id="level-choice" name="level-choice" value="1-106" tabindex="4"/>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
|
||||
<datalist id="filter-items">
|
||||
</datalist>
|
||||
<div class="left">
|
||||
<label for="filter1-choice">Filter 1:</label><br>
|
||||
<input class="searchinput" list="filter-items" id="filter1-choice" name="filter1-choice" placeholder="ANY" tabindex="5"/>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="left">
|
||||
<label for="filter2-choice">Filter 2:</label><br>
|
||||
<input class="searchinput" list="filter-items" id="filter2-choice" name="filter2-choice" placeholder="ANY" tabindex="6"/>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="left">
|
||||
<label for="filter3-choice">Filter 3:</label><br>
|
||||
<input class="searchinput" list="filter-items" id="filter3-choice" name="filter3-choice" placeholder="ANY" tabindex="7"/>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="left">
|
||||
<label for="filter4-choice">Filter 4:</label><br>
|
||||
<input class="searchinput" list="filter-items" id="filter4-choice" name="filter4-choice" placeholder="ANY" tabindex="8"/>
|
||||
<p class="error"></p>
|
||||
</div>
|
||||
<div class="right" style="grid-column:1/span 2">
|
||||
<button class = "button" id = "search-button" onclick = "doItemSearch()" tabindex="9">
|
||||
Search!
|
||||
</button>
|
||||
</div>
|
||||
<div id="summary" class="left" style="grid-column:3/span 2">
|
||||
Hello!
|
||||
</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 class="center items" id="main">
|
||||
</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.js"></script>
|
||||
<script type="text/javascript" src="load.js"></script>
|
||||
<script type="text/javascript" src="items.js"></script>
|
||||
<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.js"></script>
|
||||
<script type="text/javascript" src="/load.js"></script>
|
||||
<script type="text/javascript" src="/items.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
364
items.js
364
items.js
|
@ -1,193 +1,199 @@
|
|||
// 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;
|
||||
|
||||
const translate_mappings = {
|
||||
//"Name": "name",
|
||||
//"Display Name": "displayName",
|
||||
//"tier"Tier": ",
|
||||
//"Set": "set",
|
||||
"Powder Slots": "slots",
|
||||
//"Type": "type",
|
||||
//"armorType", (deleted)
|
||||
//"color", (deleted)
|
||||
//"lore", (deleted)
|
||||
//"material", (deleted)
|
||||
"Drop type": "drop",
|
||||
"Quest requirement": "quest",
|
||||
"Restriction": "restrict",
|
||||
//"Base Neutral Damage": "nDam",
|
||||
//"Base Fire Damage": "fDam",
|
||||
//"Base Water Damage": "wDam",
|
||||
//"Base Air Damage": "aDam",
|
||||
//"Base Thunder Damage": "tDam",
|
||||
//"Base Earth Damage": "eDam",
|
||||
//"Base Attack Speed": "atkSpd",
|
||||
"Health": "hp",
|
||||
"Raw Fire Defense": "fDef",
|
||||
"Raw Water Defense": "wDef",
|
||||
"Raw Air Defense": "aDef",
|
||||
"Raw Thunder Defense": "tDef",
|
||||
"Raw Earth Defense": "eDef",
|
||||
"Combat Level": "lvl",
|
||||
//"Class Requirement": "classReq",
|
||||
"Req Strength": "strReq",
|
||||
"Req Dexterity": "dexReq",
|
||||
"Req Intelligence": "intReq",
|
||||
"Req Agility": "agiReq",
|
||||
"Req Defense": "defReq",
|
||||
"% Health Regen": "hprPct",
|
||||
"Mana Regen": "mr",
|
||||
"% Spell Damage": "sdPct",
|
||||
"% Melee Damage": "mdPct",
|
||||
"Life Steal": "ls",
|
||||
"Mana Steal": "ms",
|
||||
"XP Bonus": "xpb",
|
||||
"Loot Bonus": "lb",
|
||||
"Reflection": "ref",
|
||||
"Strength": "str",
|
||||
"Dexterity": "dex",
|
||||
"Intelligence": "int",
|
||||
"Agility": "agi",
|
||||
"Defense": "def",
|
||||
"Thorns": "thorns",
|
||||
"Exploding": "expd",
|
||||
"Walk Speed": "spd",
|
||||
"Attack Speed Bonus": "atkTier",
|
||||
"Poison": "poison",
|
||||
"Health Bonus": "hpBonus",
|
||||
"Soul Point Regen": "spRegen",
|
||||
"Stealing": "eSteal",
|
||||
"Raw Health Regen": "hprRaw",
|
||||
"Raw Spell": "sdRaw",
|
||||
"Raw Melee": "mdRaw",
|
||||
"% Fire Damage": "fDamPct",
|
||||
"% Water Damage": "wDamPct",
|
||||
"% Air Damage": "aDamPct",
|
||||
"% Thunder Damage": "tDamPct",
|
||||
"% Earth Damage": "eDamPct",
|
||||
"% Fire Defense": "fDefPct",
|
||||
"% Water Defense": "wDefPct",
|
||||
"% Air Defense": "aDefPct",
|
||||
"% Thunder Defense": "tDefPct",
|
||||
"% Earth Defense": "eDefPct",
|
||||
"Fixed IDs": "fixID",
|
||||
"Custom Skin": "skin",
|
||||
//"Item Category": "category",
|
||||
|
||||
"1st Spell Cost %": "spPct1",
|
||||
"1st Spell Cost Raw": "spRaw1",
|
||||
"2nd Spell Cost %": "spPct2",
|
||||
"2nd Spell Cost Raw": "spRaw2",
|
||||
"3rd Spell Cost %": "spPct3",
|
||||
"3rd Spell Cost Raw": "spRaw3",
|
||||
"4th Spell Cost %": "spPct4",
|
||||
"4th Spell Cost Raw": "spRaw4",
|
||||
|
||||
"Rainbow Spell Damage": "rainbowRaw",
|
||||
"Sprint": "sprint",
|
||||
"Sprint Regen": "sprintReg",
|
||||
"Jump Height": "jh",
|
||||
"Loot Quality": "lq",
|
||||
|
||||
"Gather XP Bonus": "gXp",
|
||||
"Gather Speed Bonus": "gSpd",
|
||||
};
|
||||
|
||||
const special_mappings = {
|
||||
"Sum (skill points)": new SumQuery(["str", "dex", "int", "def", "agi"]),
|
||||
"Sum (Mana Sustain)": new SumQuery(["mr", "ms"]),
|
||||
"Sum (Life Sustain)": new SumQuery(["hpr", "ls"]),
|
||||
"Sum (Health + Health Bonus)": new SumQuery(["hp", "hpBonus"]),
|
||||
"No Strength Req": new NegateQuery("strReq"),
|
||||
"No Dexterity Req": new NegateQuery("dexReq"),
|
||||
"No Intelligence Req": new NegateQuery("intReq"),
|
||||
"No Agility Req": new NegateQuery("agiReq"),
|
||||
"No Defense Req": new NegateQuery("defReq"),
|
||||
};
|
||||
|
||||
let itemFilters = document.getElementById("filter-items");
|
||||
for (let x in translate_mappings) {
|
||||
let el = document.createElement("option");
|
||||
el.value = x;
|
||||
itemFilters.appendChild(el);
|
||||
}
|
||||
for (let x in special_mappings) {
|
||||
let el = document.createElement("option");
|
||||
el.value = x;
|
||||
itemFilters.appendChild(el);
|
||||
}
|
||||
|
||||
let itemTypes = [ "helmet", "chestplate", "leggings", "boots", "ring", "bracelet", "necklace", "wand", "spear", "bow", "dagger", "relik" ];
|
||||
let itemCategories = [ "armor", "accessory", "weapon" ];
|
||||
|
||||
function applyQuery(items, query) {
|
||||
return items.filter(query.filter, query).sort(query.compare);
|
||||
}
|
||||
|
||||
function displayItems(items_copy) {
|
||||
let items_parent = document.getElementById("main");
|
||||
for (let i in items_copy) {
|
||||
let item = items_copy[i];
|
||||
let box = document.createElement("div");
|
||||
box.classList.add("box");
|
||||
box.id = "item"+i;
|
||||
items_parent.appendChild(box);
|
||||
displayExpandedItem(expandItem(item, []), box.id);
|
||||
}
|
||||
}
|
||||
|
||||
function doItemSearch() {
|
||||
window.scrollTo(0, 0);
|
||||
let queries = [];
|
||||
queries.push(new NameQuery(document.getElementById("name-choice").value));
|
||||
|
||||
let categoryOrType = document.getElementById("category-choice").value;
|
||||
if (itemTypes.includes(categoryOrType)) {
|
||||
queries.push(new IdMatchQuery("type", categoryOrType));
|
||||
}
|
||||
else if (itemCategories.includes(categoryOrType)) {
|
||||
queries.push(new IdMatchQuery("category", categoryOrType));
|
||||
}
|
||||
|
||||
let rarity = document.getElementById("rarity-choice").value;
|
||||
if (rarity) {
|
||||
if (rarity === "ANY") {
|
||||
|
||||
}
|
||||
else {
|
||||
queries.push(new IdMatchQuery("tier", rarity));
|
||||
}
|
||||
}
|
||||
|
||||
let level_dat = document.getElementById("level-choice").value.split("-");
|
||||
queries.push(new LevelRangeQuery(parseInt(level_dat[0]), parseInt(level_dat[1])));
|
||||
|
||||
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;
|
||||
for (let i = 1; i <= 4; ++i) {
|
||||
let raw_dat = document.getElementById("filter"+i+"-choice").value;
|
||||
let filter_dat = translate_mappings[raw_dat];
|
||||
if (filter_dat !== undefined) {
|
||||
queries.push(new IdQuery(filter_dat));
|
||||
continue;
|
||||
}
|
||||
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}`);
|
||||
filter_dat = special_mappings[raw_dat];
|
||||
if (filter_dat !== undefined) {
|
||||
queries.push(filter_dat);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return ib.lvl - ia.lvl;
|
||||
}
|
||||
|
||||
function stringify(v) {
|
||||
return typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : v;
|
||||
let items_copy = items.slice();
|
||||
document.getElementById("main").textContent = "";
|
||||
for (const query of queries) {
|
||||
items_copy = applyQuery(items_copy, query);
|
||||
}
|
||||
document.getElementById("summary").textContent = items_copy.length + " results."
|
||||
displayItems(items_copy);
|
||||
}
|
||||
|
||||
function init() {
|
||||
const itemList = document.getElementById('item-list');
|
||||
const itemListFooter = document.getElementById('item-list-footer');
|
||||
return;
|
||||
let items_copy = items.slice();
|
||||
//let query = new NameQuery("Bob's");
|
||||
let query1 = new IdQuery("sdRaw");
|
||||
items_copy = applyQuery(items_copy, query1);
|
||||
|
||||
// compile the search db from the item db
|
||||
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
|
||||
let query2 = new TypeQuery("helmet");
|
||||
items_copy = applyQuery(items_copy, query2);
|
||||
|
||||
// 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();
|
||||
displayItems(items_copy);
|
||||
}
|
||||
|
||||
load_init(init);
|
||||
|
|
77
items_2.html
Normal file
77
items_2.html
Normal 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 ?= "blue" & 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
193
items_2.js
Normal 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);
|
673
query.js
673
query.js
|
@ -1,604 +1,85 @@
|
|||
/*
|
||||
* 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 ")"
|
||||
*/
|
||||
let queryTypeMap = new Map();
|
||||
|
||||
// a list of tokens indexed by a single pointer
|
||||
class TokenList {
|
||||
constructor(tokens) {
|
||||
this.tokens = tokens;
|
||||
this.ptr = 0;
|
||||
}
|
||||
class NameQuery {
|
||||
constructor(string) { this.queryString = string.toLowerCase(); }
|
||||
|
||||
get here() {
|
||||
if (this.ptr >= this.tokens.length) throw new Error('Reached end of expression');
|
||||
return this.tokens[this.ptr];
|
||||
}
|
||||
filter(item) {
|
||||
if (item.remapID === undefined) {
|
||||
return (item.displayName.toLowerCase().includes(this.queryString));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
advance(steps = 1) {
|
||||
this.ptr = Math.min(this.ptr + steps, this.tokens.length);
|
||||
}
|
||||
compare(a, b) { return a < b; }
|
||||
}
|
||||
queryTypeMap.set("name", function(s) { return new NameQuery(s); } );
|
||||
|
||||
class LevelRangeQuery {
|
||||
constructor(min, max) { this.min = min; this.max = max; }
|
||||
|
||||
filter(item) {
|
||||
if (item.remapID === undefined) {
|
||||
return (item.lvl <= this.max && item.lvl >= this.min);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
compare(a, b) { return a > b; }
|
||||
}
|
||||
|
||||
// type casts
|
||||
function checkBool(v) {
|
||||
if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
|
||||
return v;
|
||||
class NegateQuery {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.compare = function(a, b) { return 0; };
|
||||
}
|
||||
|
||||
filter(item) {
|
||||
return (!(this.id in item)) || (item[this.id] == 0);
|
||||
}
|
||||
}
|
||||
queryTypeMap.set("null", function(s) { return new IdQuery(s); } );
|
||||
|
||||
class IdQuery {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.compare = function(a, b) {
|
||||
return b[id] - a[id];
|
||||
};
|
||||
}
|
||||
|
||||
filter(item) {
|
||||
return (this.id in item) && (item[this.id]);
|
||||
}
|
||||
}
|
||||
queryTypeMap.set("stat", function(s) { return new IdQuery(s); } );
|
||||
|
||||
class IdMatchQuery {
|
||||
constructor(id, value) {
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.compare = function(a, b) {
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
filter(item) {
|
||||
return (this.id in item) && (item[this.id] == this.value);
|
||||
}
|
||||
}
|
||||
|
||||
function checkNum(v) {
|
||||
if (typeof v !== 'number') throw new Error(`Expected number, but got ${typeof v}`);
|
||||
return v;
|
||||
class SumQuery {
|
||||
constructor(ids) {
|
||||
this.compare = function(a, b) {
|
||||
let balance = 0;
|
||||
for (const id of ids) {
|
||||
if (a[id]) { balance -= a[id]; }
|
||||
if (b[id]) { balance += b[id]; }
|
||||
}
|
||||
return balance;
|
||||
};
|
||||
}
|
||||
|
||||
filter(item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
})();
|
||||
|
|
604
query_2.html
Normal file
604
query_2.html
Normal 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);
|
||||
};
|
||||
})();
|
Loading…
Reference in a new issue