Merge pull request #12 from phantamanta44/adv-item-search-rework

Adv item search rework
This commit is contained in:
hppeng-wynn 2021-03-16 00:57:03 -05:00 committed by GitHub
commit bc8255303e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1821 additions and 654 deletions

168
article.css Normal file
View file

@ -0,0 +1,168 @@
main {
margin: 24px 0 48px;
}
main h2 {
margin: 0 0 8px;
color: #bbb;
font-size: 24pt;
font-weight: bold;
}
main p {
margin: 0 0 8px;
line-height: 1.35em;
text-indent: 2.5em;
text-align: justify;
font-size: 13pt;
font-weight: normal;
}
main .footer {
font-size: 10pt;
text-align: center;
}
main code {
padding: 0 3px;
background-color: #1d1f21;
color: #de935f;
font-family: 'Source Code Pro', 'Ubuntu Mono', 'Courier New', monospace;
}
main pre { /* tomorrow night: https://github.com/chriskempson/tomorrow-theme */
margin: 14px 2px;
padding: 10px 16px;
border-left: 4px solid #7f7f7f;
background-color: #1d1f21;
color: #ddd;
font-size: 12pt;
font-weight: normal;
font-family: 'Source Code Pro', 'Ubuntu Mono', 'Courier New', monospace;
}
main pre > .prop {
color: #cc6666;
}
main pre > .fn {
color: #f0c674;
}
main pre > .bool {
color: #b294bb;
}
main pre > .str {
color: #b5bd68;
}
main pre > .num {
color: #81a2be;
}
main pre > .op {
color: #ccc;
}
main strong {
color: #ccc;
font-weight: bold;
}
main .rb-text {
background-image: linear-gradient(to left, #f5f, #a0a, #5ff, #5f5, #ff5, #fa0, #a00, #f5f);
background-size: 300% 100%;
-webkit-background-clip: text;
color: transparent;
animation: scroll-bg 4s linear infinite;
}
main .math {
font-family: 'CMU Serif', 'Cambria Math', 'Times New Roman', serif;
}
main .heart {
color: #e44078;
}
@keyframes scroll-bg {
0% {
background-position-x: 0;
}
100% {
background-position-x: 300%;
}
}
.full-width {
width: 100%;
margin: 4px 0 32px;
padding: 0;
}
.full-width > img {
width: 100%;
background-color: #000;
filter: brightness(0.7) contrast(1.2) grayscale(0.5);
}
.section {
margin: 0 25vw 28px;
}
.docs {
margin: 14px 0;
}
.docs h3 {
margin: 0 0 8px;
color: #aaa;
font-size: 20pt;
font-weight: bold;
}
.docs-entry {
margin: 4px 0 16px;
}
.docs-entry-key {
margin-bottom: 6px;
padding: 4px 6px;
display: flex;
flex-flow: row;
align-items: baseline;
background-color: #1d1f21;
font-weight: normal;
}
.docs-entry-key-id {
color: #cc6666;
font-size: 16pt;
font-family: 'Source Code Pro', 'Ubuntu Mono', 'Courier New', monospace;
}
.docs-entry-key-type {
flex: 1;
margin-left: 8px;
color: #de935f;
font-size: 11pt;
font-family: 'Source Code Pro', 'Ubuntu Mono', 'Courier New', monospace;
}
.docs-entry-key-alias {
float: right;
color: #515356;
font-size: 11pt;
}
.docs-entry-key-alias > .alias {
float: none;
color: #969896;
font-family: 'Source Code Pro', 'Ubuntu Mono', 'Courier New', monospace;
}
.docs-entry > p {
margin-left: 1.5em;
text-indent: 0;
}

805
expr_parser.js Normal file
View file

@ -0,0 +1,805 @@
/**
* See `expr_parser.md` for notes on the implementation of this parser!
*/
const ExprParser = (function() {
// buffer containing a list of tokens and a movable pointer into the list
class TokenBuf {
constructor(tokens) {
this.tokens = tokens;
this.index = 0;
}
get peek() {
return this.tokens[this.index];
}
consume(termName) {
if (this.peek.type !== termName) {
throw new Error(`Expected a ${termName}, but got a ${this.peek.type}!`)
}
const node = { type: 'term', token: this.peek };
++this.index;
return node;
}
}
// tokenize an expression string
function tokenize(exprStr) {
exprStr = exprStr.trim();
const tokens = [];
let col = 0;
function pushSymbol(sym) {
tokens.push({ type: 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: 'nLit', 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: 'sLit', value: m[1] });
col += m[0].length;
continue;
}
if ((m = /^'([^']+)'/.exec(exprStr.substring(col))) !== null) { // with single-quotes
tokens.push({ type: 'sLit', 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: 'bLit', value: true });
col += 4;
continue;
case 'false':
tokens.push({ type: 'bLit', value: false });
col += 5;
continue;
}
tokens.push({ type: 'ident', 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 TokenBuf(tokens);
}
// parse an AST from a sequence of tokens
function takeExpr(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeConj(tokens));
return { type: 'nonterm', name: 'expr', prod: 0, children };
default:
throw new Error('Could not parse an expression!');
}
}
function takeExprList(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeExpr(tokens));
children.push(takeExprList0(tokens));
return { type: 'nonterm', name: 'exprList', prod: 0, children };
default:
throw new Error('Could not parse an expression list!');
}
}
function takeExprList0(tokens) {
const children = [];
switch (tokens.peek.type) {
case ',':
children.push(tokens.consume(','));
children.push(takeExpr(tokens));
children.push(takeExprList0(tokens));
return { type: 'nonterm', name: 'exprList\'', prod: 0, children };
case ')':
case 'eof':
return { type: 'nonterm', name: 'exprList\'', prod: 1, children };
default:
throw new Error('Could not parse a expression list!');
}
}
function takeConj(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeDisj(tokens));
children.push(takeConj0(tokens));
return { type: 'nonterm', name: 'conj', prod: 0, children };
default:
throw new Error('Could not parse a conjunction!');
}
}
function takeConj0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '&':
children.push(tokens.consume('&'));
children.push(takeDisj(tokens));
children.push(takeConj0(tokens));
return { type: 'nonterm', name: 'conj\'', prod: 0, children };
case ')':
case ',':
case 'eof':
return { type: 'nonterm', name: 'conj\'', prod: 1, children };
default:
throw new Error('Could not parse a conjunction!');
}
}
function takeDisj(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeCmpEq(tokens));
children.push(takeDisj0(tokens));
return { type: 'nonterm', name: 'disj', prod: 0, children };
default:
throw new Error('Could not parse a disjunction!');
}
}
function takeDisj0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '|':
children.push(tokens.consume('|'));
children.push(takeCmpEq(tokens));
children.push(takeDisj0(tokens));
return { type: 'nonterm', name: 'disj\'', prod: 0, children };
case '&':
case ')':
case ',':
case 'eof':
return { type: 'nonterm', name: 'disj\'', prod: 1, children };
default:
throw new Error('Could not parse a disjunction!');
}
}
function takeCmpEq(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeCmpRel(tokens));
children.push(takeCmpEq0(tokens));
return { type: 'nonterm', name: 'cmpEq', prod: 0, children };
default:
throw new Error('Could not parse an equality comparison!');
}
}
function takeCmpEq0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '=':
children.push(tokens.consume('='));
children.push(takeCmpRel(tokens));
children.push(takeCmpEq0(tokens));
return { type: 'nonterm', name: 'cmpEq\'', prod: 0, children };
case '!=':
children.push(tokens.consume('!='));
children.push(takeCmpRel(tokens));
children.push(takeCmpEq0(tokens));
return { type: 'nonterm', name: 'cmpEq\'', prod: 1, children };
case '?=':
children.push(tokens.consume('?='));
children.push(takeCmpRel(tokens));
children.push(takeCmpEq0(tokens));
return { type: 'nonterm', name: 'cmpEq\'', prod: 2, children };
case '&':
case ')':
case ',':
case '|':
case 'eof':
return { type: 'nonterm', name: 'cmpEq\'', prod: 3, children };
default:
throw new Error('Could not parse an equality comparison!');
}
}
function takeCmpRel(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeSum(tokens));
children.push(takeCmpRel0(tokens));
return { type: 'nonterm', name: 'cmpRel', prod: 0, children };
default:
throw new Error('Could not parse a relational comparison!');
}
}
function takeCmpRel0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '<=':
children.push(tokens.consume('<='));
children.push(takeSum(tokens));
children.push(takeCmpRel0(tokens));
return { type: 'nonterm', name: 'cmpRel\'', prod: 0, children };
case '<':
children.push(tokens.consume('<'));
children.push(takeSum(tokens));
children.push(takeCmpRel0(tokens));
return { type: 'nonterm', name: 'cmpRel\'', prod: 1, children };
case '>':
children.push(tokens.consume('>'));
children.push(takeSum(tokens));
children.push(takeCmpRel0(tokens));
return { type: 'nonterm', name: 'cmpRel\'', prod: 2, children };
case '>=':
children.push(tokens.consume('>='));
children.push(takeSum(tokens));
children.push(takeCmpRel0(tokens));
return { type: 'nonterm', name: 'cmpRel\'', prod: 3, children };
case '!=':
case '&':
case ')':
case ',':
case '=':
case '?=':
case '|':
case 'eof':
return { type: 'nonterm', name: 'cmpRel\'', prod: 4, children };
default:
throw new Error('Could not parse a relational comparison!');
}
}
function takeSum(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeProd(tokens));
children.push(takeSum0(tokens));
return { type: 'nonterm', name: 'sum', prod: 0, children };
default:
throw new Error('Could not parse an additive expression!');
}
}
function takeSum0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '+':
children.push(tokens.consume('+'));
children.push(takeProd(tokens));
children.push(takeSum0(tokens));
return { type: 'nonterm', name: 'sum\'', prod: 0, children };
case '-':
children.push(tokens.consume('-'));
children.push(takeProd(tokens));
children.push(takeSum0(tokens));
return { type: 'nonterm', name: 'sum\'', prod: 1, children };
case '!=':
case '&':
case ')':
case ',':
case '<':
case '<=':
case '=':
case '>':
case '>=':
case '?=':
case '|':
case 'eof':
return { type: 'nonterm', name: 'sum\'', prod: 2, children };
default:
throw new Error('Could not parse an additive expression!');
}
}
function takeProd(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeExp(tokens));
children.push(takeProd0(tokens));
return { type: 'nonterm', name: 'prod', prod: 0, children };
default:
throw new Error('Could not parse a multiplicative expression!');
}
}
function takeProd0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '*':
children.push(tokens.consume('*'));
children.push(takeExp(tokens));
children.push(takeProd0(tokens));
return { type: 'nonterm', name: 'prod\'', prod: 0, children };
case '/':
children.push(tokens.consume('/'));
children.push(takeExp(tokens));
children.push(takeProd0(tokens));
return { type: 'nonterm', name: 'prod\'', prod: 1, children };
case '!=':
case '&':
case ')':
case '+':
case ',':
case '-':
case '<':
case '<=':
case '=':
case '>':
case '>=':
case '?=':
case '|':
case 'eof':
return { type: 'nonterm', name: 'prod\'', prod: 2, children };
default:
throw new Error('Could not parse a multiplicative expression!');
}
}
function takeExp(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeUnary(tokens));
children.push(takeExp0(tokens));
return { type: 'nonterm', name: 'exp', prod: 0, children };
default:
throw new Error('Could not parse an exponential expression!');
}
}
function takeExp0(tokens) {
const children = [];
switch (tokens.peek.type) {
case '^':
children.push(tokens.consume('^'));
children.push(takeUnary(tokens));
children.push(takeExp0(tokens));
return { type: 'nonterm', name: 'exp\'', prod: 0, children };
case '!=':
case '&':
case ')':
case '*':
case '+':
case ',':
case '-':
case '/':
case '<':
case '<=':
case '=':
case '>':
case '>=':
case '?=':
case '|':
case 'eof':
return { type: 'nonterm', name: 'exp\'', prod: 1, children };
default:
throw new Error('Could not parse an exponential expression!');
}
}
function takeUnary(tokens) {
const children = [];
switch (tokens.peek.type) {
case '-':
children.push(tokens.consume('-'));
children.push(takeUnary(tokens));
return { type: 'nonterm', name: 'unary', prod: 0, children };
case '!':
children.push(tokens.consume('!'));
children.push(takeUnary(tokens));
return { type: 'nonterm', name: 'unary', prod: 1, children };
case '(':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takePrim(tokens));
return { type: 'nonterm', name: 'unary', prod: 2, children };
default:
throw new Error('Could not parse a unary expression!');
}
}
function takePrim(tokens) {
const children = [];
switch (tokens.peek.type) {
case 'nLit':
children.push(tokens.consume('nLit'));
return { type: 'nonterm', name: 'prim', prod: 0, children };
case 'bLit':
children.push(tokens.consume('bLit'));
return { type: 'nonterm', name: 'prim', prod: 1, children };
case 'sLit':
children.push(tokens.consume('sLit'));
return { type: 'nonterm', name: 'prim', prod: 2, children };
case 'ident':
children.push(tokens.consume('ident'));
children.push(takeIdentTail(tokens));
return { type: 'nonterm', name: 'prim', prod: 3, children };
case '(':
children.push(tokens.consume('('));
children.push(takeExpr(tokens));
children.push(tokens.consume(')'));
return { type: 'nonterm', name: 'prim', prod: 4, children };
default:
throw new Error('Could not parse a primitive value!');
}
}
function takeIdentTail(tokens) {
const children = [];
switch (tokens.peek.type) {
case '(':
children.push(tokens.consume('('));
children.push(takeArgs(tokens));
children.push(tokens.consume(')'));
return { type: 'nonterm', name: 'identTail', prod: 0, children };
case '!=':
case '&':
case ')':
case '*':
case '+':
case ',':
case '-':
case '/':
case '<':
case '<=':
case '=':
case '>':
case '>=':
case '?=':
case '^':
case '|':
case 'eof':
return { type: 'nonterm', name: 'identTail', prod: 1, children };
default:
throw new Error('Could not parse an identifier expression!');
}
}
function takeArgs(tokens) {
const children = [];
switch (tokens.peek.type) {
case '!':
case '(':
case '-':
case 'bLit':
case 'ident':
case 'nLit':
case 'sLit':
children.push(takeExprList(tokens));
return { type: 'nonterm', name: 'args', prod: 0, children };
case ')':
case 'eof':
return { type: 'nonterm', name: 'args', prod: 1, children };
default:
throw new Error('Could not parse an argument list!');
}
}
// apply tree transformations to recover the nominal-grammar AST
function fixUp(ast) {
if (ast.type === 'term') {
return ast;
}
switch (ast.name) {
case 'exprList': // recover left-recursive structures
case 'conj':
case 'disj':
case 'exp':
return fixUpRightRecurse(ast, 1);
case 'sum':
case 'prod':
return fixUpRightRecurse(ast, 2);
case 'cmpEq':
return fixUpRightRecurse(ast, 3);
case 'cmpRel':
return fixUpRightRecurse(ast, 4);
case 'prim': // recover left-factored identifier things
switch (ast.prod) {
case 3:
switch (ast.children[1].prod) {
case 0: // function call
return {
type: 'nonterm', name: 'prim', prod: 3,
children: [fixUp(ast.children[0]), ...mapFixUp(ast.children[1].children)]
};
case 1: // just an identifier
return { type: 'nonterm', name: 'prim', prod: 4, children: [fixUp(ast.children[0])] };
}
break;
case 4:
return { ...ast, prod: 5, children: mapFixUp(ast.children) };
}
break;
}
return { ...ast, children: mapFixUp(ast.children) };
}
function mapFixUp(nodes) {
return nodes.map(chAst => fixUp(chAst));
}
function fixUpRightRecurse(ast, maxProd) {
let nomAst = { type: 'nonterm', name: ast.name, prod: maxProd, children: [fixUp(ast.children[0])] };
let tree = ast.children[1];
while (tree.prod < maxProd) {
nomAst = {
type: 'nonterm', name: ast.name, prod: tree.prod,
children: [nomAst, ...mapFixUp(tree.children.slice(0, tree.children.length - 1))]
};
tree = tree.children[tree.children.length - 1];
}
return nomAst;
}
// compile nominal AST into an item builder expression function
function translate(ast, builtInProps, builtInFuncs) {
function trans(ast) {
return translate(ast, builtInProps, builtInFuncs);
}
if (ast.type === 'term') {
switch (ast.token.type) {
case 'nLit':
return ast.token.value;
case 'bLit':
return ast.token.value;
case 'sLit':
return ast.token.value;
case 'ident':
return ast.token.value;
}
} else if (ast.type === 'nonterm') {
switch (ast.name) {
case 'expr':
return trans(ast.children[0]);
case 'exprList':
switch (ast.prod) {
case 0:
return [...trans(ast.children[0]), trans(ast.children[2])];
case 1:
return [trans(ast.children[0])];
}
break;
case 'conj':
switch (ast.prod) {
case 0:
return new ConjTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return trans(ast.children[0]);
}
break;
case 'disj':
switch (ast.prod) {
case 0:
return new DisjTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return trans(ast.children[0]);
}
break;
case 'cmpEq':
switch (ast.prod) {
case 0:
return new EqTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return new NeqTerm(trans(ast.children[0]), trans(ast.children[2]));
case 2:
return new ContainsTerm(trans(ast.children[0]), trans(ast.children[2]));
case 3:
return trans(ast.children[0]);
}
break;
case 'cmpRel':
switch (ast.prod) {
case 0:
return new LeqTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return new LtTerm(trans(ast.children[0]), trans(ast.children[2]));
case 2:
return new GtTerm(trans(ast.children[0]), trans(ast.children[2]));
case 3:
return new GeqTerm(trans(ast.children[0]), trans(ast.children[2]));
case 4:
return trans(ast.children[0]);
}
break;
case 'sum':
switch (ast.prod) {
case 0:
return new AddTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return new SubTerm(trans(ast.children[0]), trans(ast.children[2]));
case 2:
return trans(ast.children[0]);
}
break;
case 'prod':
switch (ast.prod) {
case 0:
return new MulTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return new DivTerm(trans(ast.children[0]), trans(ast.children[2]));
case 2:
return trans(ast.children[0]);
}
break;
case 'exp':
switch (ast.prod) {
case 0:
return new ExpTerm(trans(ast.children[0]), trans(ast.children[2]));
case 1:
return trans(ast.children[0]);
}
break;
case 'unary':
switch (ast.prod) {
case 0:
return new NegTerm(trans(ast.children[1]));
case 1:
return new InvTerm(trans(ast.children[1]));
case 2:
return trans(ast.children[0]);
}
break;
case 'prim':
switch (ast.prod) {
case 0:
return new NumLitTerm(ast.children[0].token.value);
case 1:
return new BoolLitTerm(ast.children[0].token.value);
case 2:
return new StrLitTerm(ast.children[0].token.value);
case 3:
const fn = builtInFuncs[ast.children[0].token.id.toLowerCase()];
if (!fn) {
throw new Error(`Unknown function: ${ast.children[0].token.id}`);
}
return new FnCallTerm(fn, trans(ast.children[2]));
case 4: {
const prop = builtInProps[ast.children[0].token.id.toLowerCase()];
if (!prop) {
throw new Error(`Unknown property: ${ast.children[0].token.id}`);
}
return new PropTerm(prop);
}
case 5:
return trans(ast.children[1]);
}
break;
case 'args':
switch (ast.prod) {
case 0:
return trans(ast.children[0]);
case 1:
return [];
}
break;
}
}
}
return class ExprParser {
constructor(builtInProps, builtInFuncs) {
this.builtInProps = builtInProps;
this.builtInFuncs = builtInFuncs;
}
parse(exprStr) {
const tokens = tokenize(exprStr);
if (tokens.tokens.length <= 1) {
return null;
}
const transAst = takeExpr(tokens, 0);
if (tokens.peek.type !== 'eof') {
throw new Error('Could not parse entire expression!');
}
const nomAst = fixUp(transAst);
return translate(nomAst, this.builtInProps, this.builtInFuncs);
}
};
})();

197
expr_parser.md Normal file
View file

@ -0,0 +1,197 @@
This file contains notes on how the parser implemented in `expr_parser.js` is designed.
The nominal grammar is as follows:
```
expr := conj
exprList := exprList "," expr
| expr
conj := conj "&" disj
| disj
disj := disj "|" cmpEq
| cmpEq
cmpEq := cmpEq "=" cmpRel
| cmpEq "!=" cmpRel
| cmpEq "?=" cmpRel
| cmpRel
cmpRel := cmpRel "<=" sum
| cmpRel "<" sum
| cmpRel ">" sum
| cmpRel ">=" sum
| sum
sum := sum "+" prod
| sum "-" prod
| prod
prod := prod "*" exp
| prod "/" exp
| exp
exp := exp "^" unary
| unary
unary := "-" unary
| "!" unary
| prim
prim := nLit
| bLit
| sLit
| ident "(" args ")"
| ident
| "(" expr ")"
args := exprList
| ε
```
In order to eliminate left-recursion, the above grammar is transformed into the following:
```
expr := conj
exprList := expr exprListTail
exprList' := "," expr exprList'
| ε
conj := disj conj'
conj' := "&" disj conj'
| ε
disj := cmpEq disj'
disj' := "|" cmpEq disj'
| ε
cmpEq := cmpRel cmpEq'
cmpEq' := "=" cmpRel cmpEq'
| "!=" cmpRel cmpEq'
| "?=" cmpRel cmpEq'
| ε
cmpRel := sum cmpRel'
cmpRel' := "<=" sum cmpRel'
| "<" sum cmpRel'
| ">" sum cmpRel'
| ">=" sum cmpRel'
| ε
sum := prod sum'
sum' := "+" prod sum'
| "-" prod sum'
| ε
prod := exp prod'
prod' := "*" exp prod'
| "/" exp prod'
| ε
exp := unary exp'
exp' := "^" unary exp'
| ε
unary := "-" unary
| "!" unary
| prim
prim := nLit
| bLit
| sLit
| ident identTail
| "(" expr ")"
identTail := "(" args ")"
| ε
args := exprList
| ε
```
The parser itself is a recursive-descent LL(1) parser.
To build the parsing rules, the following `FIRST` and `FOLLOW` sets are computed for the above grammar:
Nonterminal | FIRST
----------- | -----
`expr` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`exprList` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`exprList'` | `","`, `ε`
`conj` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`conj'` | `"&"`, `ε`
`disj` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`disj'` | `"\|"`, `ε`
`cmpEq` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`cmpEq'` | `"!="`, `"="`, `"?="`, `ε`
`cmpRel` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`cmpRel'` | `"<"`, `"<="`, `">"`, `">="`, `ε`
`sum` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`sum'` | `"+"`, `"-"`, `ε`
`prod` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`prod'` | `"*"`, `"/"`, `ε`
`exp` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`exp'` | `"^"`, `ε`
`unary` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
`prim` | `"("`, `bLit`, `ident`, `nLit`, `sLit`
`identTail` | `"("`, `ε`
`args` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`, `ε`
Nonterminal | FOLLOW
----------- | ------
`expr` | `")"`, `","`, `$`
`exprList` | `")"`
`exprList'` | `")"`
`conj` | `")"`, `","`
`conj'` | `")"`, `","`
`disj` | `"&"`, `")"`, `","`
`disj'` | `"&"`, `")"`, `","`
`cmpEq` | `"&"`, `")"`, `","`, `"\|"`
`cmpEq'` | `"&"`, `")"`, `","`, `"\|"`
`cmpRel` | `"!="`, `"&"`, `")"`, `","`, `"="`, `"?="`, `"\|"`
`cmpRel'` | `"!="`, `"&"`, `")"`, `","`, `"="`, `"?="`, `"\|"`
`sum` | `"!="`, `"&"`, `")"`, `","`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`sum'` | `"!="`, `"&"`, `")"`, `","`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`prod` | `"!="`, `"&"`, `")"`, `"+"`, `","`, `"-"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`prod'` | `"!="`, `"&"`, `")"`, `"+"`, `","`, `"-"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`exp` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`exp'` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
`unary` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
`prim` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
`identTail` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
`args` | `")"`
Then, the parsing rule table is as follows, where each table entry refers to the production number for the given nonterminal:
Nonterminal | "!" | "!=" | "&" | "(" | ")" | "*" | "+" | "," | "-" | "/" | "<" | "<=" | "=" | ">" | ">=" | "?=" | "^" | "&#124;" | bLit | ident | nLit | sLit | $
----------- | --- | ---- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---- | --- | --- | ---- | ---- | --- | --- | ---- | ----- | ---- | ---- | ---
expr | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
exprList | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
exprList' | - | - | - | - | 1 | - | - | 0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1
conj | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
conj' | - | - | 0 | - | 1 | - | - | 1 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1
disj | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
disj' | - | - | 1 | - | 1 | - | - | 1 | - | - | - | - | - | - | - | - | - | 0 | - | - | - | - | 1
cmpEq | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
cmpEq' | - | 1 | 3 | - | 3 | - | - | 3 | - | - | - | - | 0 | - | - | 2 | - | 3 | - | - | - | - | 3
cmpRel | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
cmpRel' | - | 4 | 4 | - | 4 | - | - | 4 | - | - | 1 | 0 | 4 | 2 | 3 | 4 | - | 4 | - | - | - | - | 4
sum | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
sum' | - | 2 | 2 | - | 2 | - | 0 | 2 | 1 | - | 2 | 2 | 2 | 2 | 2 | 2 | - | 2 | - | - | - | - | 2
prod | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
prod' | - | 2 | 2 | - | 2 | 0 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | - | 2 | - | - | - | - | 2
exp | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
exp' | - | 1 | 1 | - | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | - | - | - | - | 1
unary | 1 | - | - | 2 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 2 | 2 | 2 | 2 | -
prim | - | - | - | 4 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1 | 3 | 0 | 2 | -
identTail | - | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | - | - | - | - | 1
args | 0 | - | - | 0 | 1 | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | 1

View file

@ -55,27 +55,31 @@
<a href="credits.txt" class="link">Additional credits</a> <a href="credits.txt" class="link">Additional credits</a>
</div> </div>
<div class="center" id="help"> <div class="center" id="help">
<a href="options.txt" class = "link">Search Guide</a> <a href="items_2_help.html" class="link" target="_blank">Search Guide</a>
</div> </div>
<div class="center" id="main" style="padding: 2%"> <div class="center" id="main" style="padding: 2%">
<div id="search-container" style="margin-bottom: 1.5%"> <div id="search-container" style="margin-bottom: 1.5%">
<div class="left" id="search-filter" style="display: inline-block; vertical-align: top"> <div class="left" id="search-filter" style="display: inline-block; vertical-align: top">
<label for="search-filter-field">Filter By:</label> <label for="search-filter-field">Filter By:</label>
<br> <br>
<input id="search-filter-field" type="text" placeholder="name ?= &quot;blue&quot; & str >= 15 & dex >= 10" style="width: 25vw; padding: 8px"> <input id="search-filter-field" type="text"
placeholder="name ?= &quot;blue&quot; & str >= 15 & dex >= 10"
style="width: 25vw; padding: 8px">
<br> <br>
<div id="search-filter-error" style="color: #ff0000"></div> <div id="search-filter-error" style="color: #ff0000"></div>
</div> </div>
<div class="left" id="search-sort" style="display: inline-block; vertical-align: top"> <div class="left" id="search-sort" style="display: inline-block; vertical-align: top">
<label for="search-sort-field">Sort By:</label> <label for="search-sort-field">Sort By:</label>
<br> <br>
<input id="search-sort-field" type="text" placeholder="str + dex; meleerawdmg + spellrawdmg" style="width: 25vw; padding: 8px"> <input id="search-sort-field" type="text" placeholder="str + dex; meleerawdmg + spellrawdmg"
style="width: 25vw; padding: 8px">
<br> <br>
<div id="search-sort-error" style="color: #ff0000"></div> <div id="search-sort-error" style="color: #ff0000"></div>
</div> </div>
</div> </div>
<div id="item-list-container"> <div id="item-list-container">
<div class="left" id="item-list" style="display: flex; flex-flow: row wrap"></div> <div class="left" id="item-list"
style="display: flex; flex-flow: row wrap; justify-content: space-evenly"></div>
<div class="center" id="item-list-footer"></div> <div class="center" id="item-list-footer"></div>
</div> </div>
</div> </div>
@ -84,6 +88,7 @@
<script type="text/javascript" src="damage_calc.js"></script> <script type="text/javascript" src="damage_calc.js"></script>
<script type="text/javascript" src="display.js"></script> <script type="text/javascript" src="display.js"></script>
<script type="text/javascript" src="query_2.js"></script> <script type="text/javascript" src="query_2.js"></script>
<script type="text/javascript" src="expr_parser.js"></script>
<script type="text/javascript" src="load.js"></script> <script type="text/javascript" src="load.js"></script>
<script type="text/javascript" src="items_2.js"></script> <script type="text/javascript" src="items_2.js"></script>
</body> </body>

View file

@ -38,6 +38,8 @@ function compareLexico(ia, keysA, ib, keysB) {
if (aKey > bKey) return 1; if (aKey > bKey) return 1;
break; break;
case 'number': // sort numeric stuff in reverse order case 'number': // sort numeric stuff in reverse order
aKey = isNaN(aKey) ? 0 : aKey;
bKey = isNaN(bKey) ? 0 : bKey;
if (aKey < bKey) return 1; if (aKey < bKey) return 1;
if (aKey > bKey) return -1; if (aKey > bKey) return -1;
break; break;
@ -74,17 +76,23 @@ function init() {
itemEntries.push(itemElem); itemEntries.push(itemElem);
} }
// create the expression parser
const exprParser = new ExprParser(itemQueryProps, itemQueryFuncs);
// the two search query input boxes // the two search query input boxes
const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) { const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) {
const expr = compileQueryExpr(exprStr); const expr = exprParser.parse(exprStr);
return expr !== null ? expr : (i, ie) => true; return expr !== null ? expr : new BoolLitTerm(true);
}); });
const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) { const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) {
const subExprs = exprStr.split(';').map(compileQueryExpr).filter(f => f != null); const subExprs = exprStr.split(';').map(e => exprParser.parse(e)).filter(f => f != null);
return function(i, ie) { return {
type: 'array',
resolve(i, ie) {
const sortKeys = []; const sortKeys = [];
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k](i, ie)); for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k].resolve(i, ie));
return sortKeys; return sortKeys;
}
}; };
}); });
@ -108,8 +116,8 @@ function init() {
try { try {
for (let i = 0; i < searchDb.length; i++) { for (let i = 0; i < searchDb.length; i++) {
const item = searchDb[i][0], itemExp = searchDb[i][1]; const item = searchDb[i][0], itemExp = searchDb[i][1];
if (checkBool(searchFilterField.output(item, itemExp))) { if (checkBool(searchFilterField.output.resolve(item, itemExp))) {
searchResults.push({ item, itemExp, sortKeys: searchSortField.output(item, itemExp) }); searchResults.push({ item, itemExp, sortKeys: searchSortField.output.resolve(item, itemExp) });
} }
} }
} catch (e) { } catch (e) {

231
items_2_help.html Normal file
View file

@ -0,0 +1,231 @@
<!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="stylesheet" href="article.css">
<link rel="icon" href="./media/icons/searcher.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>
<a href="./customizer.html" class="nomarginp iconlink tooltip">
<img src="/media/icons/custom.png" class="left linkoptions headericon">
</img>
<div class="tooltiptext center">WynnCustom</div>
</a>
</div>
<div class="headercenter">
<div>
<p class="itemp" id="header">WynnAtlas</p>
</div>
</div>
<div class="headerright"></div>
</header>
<main>
<div class="full-width">
<img src="https://i.imgur.com/Ap6Zd3Q.png"/>
</div>
<div class="section">
<h2>What the heck is &ldquo;Advanced Item Search&rdquo;?</h2>
<p>The WynnBuilder team has been hard at work giving you the latest and greatest tools for optimizing your most complex Wynncraft builds. Now, we're introducing <strong class="rb-text">WynnAtlas</strong>, the new, bigger, better, smarter, powerful-er item guide! Featuring an extremely flexible expression language for filtering and sorting items, WynnAtlas' advanced item search system gives build engineers the power to select items with a high degree of granularity. Picking components for your brand-new Divzer WFA build has never been easier!</p>
</div>
<div class="section">
<h2>So, how am I supposed to use this thing?</h2>
<p>The advanced item search interface uses two separate expressions: one for filtering items and the other for sorting them. The idea is that you can filter for items matching a set of criteria that are absolutely necessary for the item you want, then sort the remaining items to find the optimal build component. The expressions themselves are mathematical formulae that compute values from the properties of items, such as their damage values and identifications.</p>
<p>As an example, suppose you want a helmet granting at least 10 intelligence that maximizes spell damage gain for a build using the Nepta Floodbringer. We would start by writing a filter for helmets with our desired bound on intelligence:</p>
<pre class="hl-expr">{prop:type} {op:=} {str:"helmet"} {op:&} {prop:int} {op:&gt;=} {num:10}</pre>
<p>Then, wee would write an expression that computes the amount of damage we get from the helmet. To do this, we first check the damage numbers for the Nepta Floodbringer: assuming we aren't using any powders, we have 96&ndash;112 water damage scaled by the super-fast attack speed modifier of 4.3, giving us a total of <span class="math">4.3 &times; (96 + 112) &div; 2 = 447.2</span>. We then multiply in the spell damage and water damage modifiers and add raw spell damage, giving us:</p>
<pre class="hl-expr">{prop:sdRaw} {op:+} {num:447.2} {op:*} {op:(}{prop:sdPct} {op:+} {prop:wDamPct}{op:)} {op:/} {num:100}</pre>
<p>And, voilà! In the blink of an eye, we've discovered that Caesura is the best helmet for our criteria, granting 448.14 spell damage.</p>
</div>
<div class="section">
<h2>Cool! What kinds of things can I write?</h2>
<p>Roughly speaking, the expression language just consists of mathematical expressions with simple operations and functions. Each value in the expression is either a number, a boolean (true or false), or a string (a chunk of text). Numbers are floating-point rather than integers, so any kind of fractional value can also be expressed. String literals can use either single- or double-quotes, so the following is true:
<pre class="hl-expr">{str:"Hello, world!"} {op:=} {str:'Hello, world!'}</pre>
<p>The filtering expression should produce a boolean in the end, which represents whether an item matches the filter or not, and the sorting expression should produce either a number or a string, which are used to order the items. The sorting expression can even contain multiple sub-expressions separated by semicolons, which are then used to sort the result lexicographically. For example, if I want to sort items by loot bonus, then XP bonus if items have equal loot bonus, I could write:</p>
<pre class="hl-expr">{prop:lb}; {prop:xpb}</pre>
<p>The supported mathematical operations include the basic arithmetic operations as well as exponentiation. These operators obey the usual order of operations, with exponentiation having the highest priority. Parentheses are also supported for grouping.</p>
<pre class="hl-expr">({num:1} {op:+} {num:2}) {op:-} {num:3} {op:*} ({num:4} {op:/} {num:5}) {op:^} {num:6}</pre>
<p>The supported boolean operations include conjunction (&ldquo;AND&rdquo;) and disjunction (&ldquo;OR&rdquo;) using the <code>&</code> and <code>|</code> symbols, as well as unary inversion (&ldquo;NOT&rdquo;) using the <code>!</code> symbol. Disjunction has higher priority than conjunction, so the following is true:</p>
<pre class="hl-expr">{bool:true} {op:|} {bool:false} {op:&} {bool:true} {op:|} {bool:false} {op:=} {op:(}{bool:true} {op:|} {bool:false}{op:)} {op:&} {op:(}{bool:true} {op:|} {bool:false}{op:)}</pre>
<p>For comparisons, equality and non-equality are represented by the <code>=</code> and <code>!=</code> symbols. The two operands can be of any type, and the result will always be a boolean describing whether the operands are equal. String comparisons are case-insensitive.</p>
<pre class="hl-expr">{prop:type} {op:=} {str:"wand"} {op:|} {prop:cat} {op:!=} {str:"weapon"}</pre>
<p>The relational comparisons are given by <code>&lt;</code>, <code>&lt;=</code>, <code>&gt;=</code>, and <code>&gt;</code>. The two operands must be of some comparable type&mdash;booleans cannot be compared, so it's an error to pass them to a relational comparison.</p>
<pre class="hl-expr">{prop:str} {op:>=} {num:20} {op:&} {prop:lvl} {op:<} {num:80}</pre>
<p>In addition to these comparison operators, there's also a &ldquo;contains&rdquo; operator <code>?=</code>, which checks whether a string contains a given substring or not. This allows for matching more loosely against strings; for example, if I wanted to see all the Hive master weapons, I could use the filter:</p>
<pre class="hl-expr">{prop:name} {op:?=} {str:"hive infused"}</pre>
<p>Some handy functions are also available, and can be called using a C-like syntax:</p>
<pre class="hl-expr">{fn:min}{op:(}{prop:int}{op:,} {num:105} {op:-} {prop:intReq}{op:)} {op:+} {fn:min}{op:(}{prop:def}{op:,} {num:80} {op:-} {prop:defReq}{op:)}</pre>
<p>Try some of these examples out in the item guide! Experimenting with weird and unusual expressions is a great way to get used to the syntax. The more you practice, the faster and more effective you'll be at using WynnAtlas to optimize your powerful Wynncraft builds!</p>
</div>
<div class="section">
<h2>What properties and functions do I get?</h2>
<p>What follows are lists of existing built-in properties and functions. Note that some aliases are omitted for properties with way too many aliases. If you want, you can also check out <a class="link" href="https://github.com/wynnbuilder/wynnbuilder.github.io/blob/master/expr_parser.md" target="_blank" rel="noreferrer">the source code</a> to see the actual declarations for all the built-in objects.</p>
<div id="docs-props" class="docs">
<h3>Built-in Properties</h3>
</div>
<div id="docs-fns" class="docs">
<h3>Built-in Functions</h3>
</div>
</div>
<div class="section">
<h2>Is there a formal specification?</h2>
<p>You can check out the <a class="link" href="https://github.com/wynnbuilder/wynnbuilder.github.io/blob/master/expr_parser.md" target="_blank" rel="noreferrer">implementation notes</a> for the expression parser, as well as the <a class="link" href="https://github.com/wynnbuilder/wynnbuilder.github.io/blob/master/expr_parser.js" target="_blank" rel="noreferrer">parser code itself</a>, over at the WynnBuilder GitHub repository. You can also ask around on the Atlas Inc. Discord server if you want more details.</p>
</div>
<div class="section">
<div class="footer">Written by <strong class="heart">Phanta</strong>, the same person who designed the advanced search system!</div>
</div>
</main>
<script type="text/javascript">
for (const codeBlk of document.getElementsByClassName('hl-expr')) {
codeBlk.innerHTML = codeBlk.innerText.replace(/{(\w+):([^}]+)}/g, (_, k, v) => `<span class="${k}">${v}</span>`);
}
function genDocEntry(id, type, aliases, desc) {
const entryElem = document.createElement('div');
entryElem.classList.add('docs-entry');
const keyElem = document.createElement('div');
keyElem.classList.add('docs-entry-key');
const idElem = document.createElement('span');
idElem.classList.add('docs-entry-key-id');
idElem.innerText = id;
keyElem.append(idElem);
const typeElem = document.createElement('span');
typeElem.classList.add('docs-entry-key-type');
typeElem.innerText = type;
keyElem.append(typeElem);
if (aliases != null) {
const aliasesElem = document.createElement('span');
aliasesElem.classList.add('docs-entry-key-alias');
aliasesElem.innerHTML = `(aliases: ${aliases.map(a => `<span class="alias">${a}</span>`).join(', ')})`;
keyElem.append(aliasesElem);
}
entryElem.append(keyElem);
const descElem = document.createElement('p');
descElem.innerHTML = desc.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
entryElem.append(descElem);
return entryElem;
}
const docsProps = document.getElementById('docs-props');
for (const entry of [
['name', 'string', null, 'The name of an item.'],
['type', 'string', null, 'The type of an item. Value is one of `"wand"`, `"bow"`, `"dagger"`, `"spear"`, `"relik"`, `"helmet"`, `"chestplate"`, `"leggings"`, `"boots"`, `"ring"`, `"bracelet"`, or `"necklace"`.'],
['category', 'string', ['cat'], 'The category of an item. Value is one of `"weapon"`, `"armor"`, or `"accessory"`.'],
['tierName', 'string', ['tierStr', 'rarityName', 'rarityStr'], 'The name of a tier. Value is one of `"normal"`, `"unique"`, `"set"`, `"rare"`, `"legendary"`, `"fabled"`, or `"mythic"`. For a comparable version, use the `tier` property instead.'],
['tier', 'number', ['rarity'], 'An integer representing an item\'s tier, where normal items have tier 0 and mythic items have tier 6. For a string representation of the tier, use the `tierName` property instead.'],
['level', 'number', ['lvl', 'combatLevel', 'combatLvl'], 'The combat level requirement for an item.'],
['strReq', 'number', ['strMin'], 'The strength requirement for an item.'],
['dexReq', 'number', ['dexMin'], 'The dexterity requirement for an item.'],
['intReq', 'number', ['intMin'], 'The intelligence requirement for an item.'],
['defReq', 'number', ['defMin'], 'The defence requirement for an item.'],
['agiReq', 'number', ['agiMin'], 'The agility requirement for an item.'],
['sumReq', 'number', ['sumMin, totalReq, totalMin'], 'The total skill point requirement for an item.'],
['str', 'number', null, 'The bonus strength imparted by an item.'],
['dex', 'number', null, 'The bonus dexterity imparted by an item.'],
['int', 'number', null, 'The bonus intelligence imparted by an item.'],
['def', 'number', null, 'The bonus defence imparted by an item.'],
['agi', 'number', null, 'The bonus agility imparted by an item.'],
['skillPoints', 'number', ['skillPts', 'attributes', 'attrs'], 'The total bonus skill points imparted by an item.'],
['neutralDmg', 'number', ['neutralDam', 'nDmg', 'nDam'], 'The average neutral damage on a weapon.'],
['earthDmg', 'number', ['earthDam', 'eDmg', 'eDam'], 'The average earth damage on a weapon.'],
['thunderDmg', 'number', ['thunderDam', 'tDmg', 'tDam'], 'The average thunder damage on a weapon.'],
['waterDmg', 'number', ['waterDam', 'wDmg', 'wDam'], 'The average water damage on a weapon.'],
['fireDmg', 'number', ['fireDam', 'fDmg', 'fDam'], 'The average fire damage on a weapon.'],
['airDmg', 'number', ['airDam', 'aDmg', 'aDam'], 'The average air damage on a weapon.'],
['sumDmg', 'number', ['sumDam', 'totalDmg', 'totalDam'], 'The total average damages on a weapon.'],
['earthDmg%', 'number', ['earthDam%', 'eDmg%', 'eDam%', 'eDamPct'], 'The bonus earth damage modifier on an item.'],
['thunderDmg%', 'number', ['thunderDam%', 'tDmg%', 'tDam%', 'tDamPct'], 'The bonus thunder damage modifier on an item.'],
['waterDmg%', 'number', ['waterDam%', 'wDmg%', 'wDam%', 'wDamPct'], 'The bonus water damage modifier on an item.'],
['fireDmg%', 'number', ['fireDam%', 'fDmg%', 'fDam%', 'fDamPct'], 'The bonus fire damage modifier on an item.'],
['airDmg%', 'number', ['airDam%', 'aDmg%', 'aDam%', 'aDamPct'], 'The bonus air damage modifier on an item.'],
['sumDmg%', 'number', ['sumDam%', 'totalDmg%', 'totalDam%', 'sumDamPct', 'totalDamPct'], 'The sum of the bonus elemental damage modifiers on an item.'],
['meleeDmg', 'number', ['meleeDam', 'meleeDmg%', 'meleeDam%', 'mdPct'], 'The bonus main-attack damage modifier on an item.'],
['meleeNeutralDmg', 'number', ['meleeRawDam', 'meleeNeutralDmg', 'meleeNeutralDam', 'mdRaw'], 'The neutral main-attack damage on an item.'],
['spellDmg', 'number', ['spellDam', 'spellDmg%', 'spellDam%', 'sdPct'], 'The bonus spell damage modifier on an item.'],
['spellNeutralDmg', 'number', ['spellRawDam', 'spellNeutralDam', 'sdRaw'], 'The neutral spell damage on an item.'],
['attackSpeed', 'string', ['atkSpd'], 'The attack speed on a weapon. For the bonus attack speed tiers granted by the identification, use `bonusAttackSpeed` instead.'],
['bonusAttackSpeed', 'number', ['bonusAtkSpd', 'attackSpeedId', 'AtkSpdId', 'AtkTier'], 'The bonus attack speed tiers granted by the attack speed identification. For the base attack speed of a weapon, use `attackSpeed` instead.'],
['sumAttackSpeed', 'number', ['totalAttackSpeed', 'sumAtkSpd', 'totalAtkSpd'], 'The total attack speed on an item, including both base weapon attack speed and bonus attack speed. Useful for items like Skien\'s Madness.'],
['earthDef', 'number', ['eDef'], 'The base earth defence afforded by an armor piece or accessory.'],
['thunderDef', 'number', ['tDef'], 'The base thunder defence afforded by an armor piece or accessory.'],
['waterDef', 'number', ['wDef'], 'The base water defence afforded by an armor piece or accessory.'],
['fireDef', 'number', ['fDef'], 'The base fire defence afforded by an armor piece or accessory.'],
['airDef', 'number', ['aDef'], 'The base air defence afforded by an armor piece or accessory.'],
['sumDef', 'number', ['totalDef'], 'The total base elemental defences afforded by an armor piece or accessory.'],
['earthDef%', 'number', ['eDef%', 'eDefPct'], 'The bonus earth defence modifier on an item.'],
['thunderDef%', 'number', ['tDef%', 'tDefPct'], 'The bonus thunder defence modifier on an item.'],
['waterDef%', 'number', ['wDef%', 'wDefPct'], 'The bonus water defence modifier on an item.'],
['fireDef%', 'number', ['fDef%', 'fDefPct'], 'The bonus fire defence modifier on an item.'],
['airDef%', 'number', ['aDef%', 'aDefPct'], 'The bonus air defence modifier on an item.'],
['sumDef%', 'number', ['totalDef%', 'sumDefPct', 'totalDefPct'], 'The sum of the bonus elemental defence modifiers on an item.'],
['health', 'number', ['hp'], 'The base health points granted by an armor piece or accessory. For the health identification, use `bonusHealth` instead.'],
['bonusHealth', 'number', ['healthId', 'bonusHp', 'hpId', 'hpBonus'], 'The bonus health points granted by the identification. For the flat base health on an armor piece or accessory, use `health` instead.'],
['sumHealth', 'number', ['sumHp', 'totalHealth', 'totalHp'], 'The total health granted by an item, including both base health and the identification. Useful for items like Adamantite.'],
['hpRegen', 'number', ['hpr', 'hr', 'hprRaw'], 'The flat health regeneration granted by an item.'],
['hpRegen%', 'number', ['hpr%', 'hr%', 'hprPct'], 'The bonus health regeneration modifier on an item.'],
['lifeSteal', 'number', ['ls'], 'The life steal on an item.'],
['manaRegen', 'number', ['mr'], 'The mana regeneration on an item.'],
['manaSteal', 'number', ['ms'], 'The mana steal on an item.'],
['walkSpeed', 'number', ['moveSpeed', 'ws', 'spd'], 'The walk speed on an item.'],
['sprint', 'number', null, 'The bonus sprint capacity modifier on an item.'],
['sprintRegen', 'number', ['sprintReg'], 'The sprint regeneration modifier on an item.'],
['jumpHeight', 'number', ['jh'], 'The increased jump height granted by an item.'],
['spellCost1', 'number', ['rawSpellCost1', 'spCost1', 'spRaw1'], 'The flat spell cost modifier for the first spell.'],
['spellCost1%', 'number', ['spCost1%', 'spPct1'], 'The percent spell cost modifier for the first spell.'],
['spellCost2', 'number', ['rawSpellCost2', 'spCost2', 'spRaw2'], 'The flat spell cost modifier for the second spell.'],
['spellCost2%', 'number', ['spCost2%', 'spPct2'], 'The percent spell cost modifier for the second spell.'],
['spellCost3', 'number', ['rawSpellCost3', 'spCost3', 'spRaw3'], 'The flat spell cost modifier for the third spell.'],
['spellCost3%', 'number', ['spCost3%', 'spPct3'], 'The percent spell cost modifier for the third spell.'],
['spellCost4', 'number', ['rawSpellCost4', 'spCost4', 'spRaw4'], 'The flat spell cost modifier for the fourth spell.'],
['spellCost4%', 'number', ['spCost4%', 'spPct4'], 'The percent spell cost modifier for the fourth spell.'],
['sumSpellCost', 'number', ['sumRawSpellCost', 'sumSpCost', 'sumSpRaw'], 'The sum of the percent spell cost modifiers on an item.'],
['sumSpellCost%', 'number', ['sumSpCost%', 'sumSpPct'], 'The sum of the flat spell cost modifiers on an item.'],
['exploding', 'number', ['expl', 'expd'], 'The explosion proc chance granted by an item.'],
['poison', 'number', null, 'The poison damage granted by an item.'],
['thorns', 'number', null, 'The thorns proc chance granted by an item.'],
['reflection', 'number', ['refl', 'ref'], 'The reflection proc chance granted by an item.'],
['soulPointRegen', 'number', ['spr', 'spRegen'], 'The bonus soul point regeneration on an item.'],
['lootBonus', 'number', ['lb'], 'The bonus loot modifier on an item.'],
['xpBonus', 'number', ['xpb', 'xb'], 'The bonus experience modifier on an item.'],
['stealing', 'number', ['esteal'], 'The emerald-stealing proc change granted by an item.'],
['powderSlots', 'number', ['powders', 'slots', 'sockets'], 'The number of available powder slots on an item.']
]) {
docsProps.append(genDocEntry(...entry));
}
const docsFns = document.getElementById('docs-fns');
for (const entry of [
['max(x1, x2[, x3...])', 'number', 'Takes the maximum of a set of numbers. Accepts any number of arguments greater than or equal to two.'],
['min(x1, x2[, x3...])', 'number', 'Takes the minimum of a set of numbers. Accepts any number of arguments greater than or equal to two.'],
['floor(x)', 'number', 'Rounds a number down to the greatest integer less than or equal to it.'],
['ceil(x)', 'number', 'Rounds a number up to the least integer greater than or equal to it.'],
['round(x)', 'number', 'Rounds a number to the nearest integer.'],
['sqrt(x)', 'number', 'Computes the square root of a number.'],
['abs(x)', 'number', 'Computes the absolute value of a number.'],
['contains(xs, x)', 'boolean', 'Checks whether a string contains a particular substring or not. Equivalent to the `?=` operator.'],
['atkSpdMod(x)', 'number', 'Gets the attack speed damage modifier for a given attack speed value. Works well with the `sumAttackSpeed` property.'],
]) {
docsFns.append(genDocEntry(entry[0], entry[1], null, entry[2]));
}
</script>
</body>
</html>

View file

@ -1,152 +0,0 @@
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', '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', '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

View file

@ -1,61 +1,4 @@
/* // dynamic type casts
* 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) { function checkBool(v) {
if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`); if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
return v; return v;
@ -71,67 +14,79 @@ function checkStr(v) {
return v; return v;
} }
function checkComparable(v) {
if (typeof v === 'boolean') throw new Error('Boolean is not comparable');
return v;
}
// properties of items that can be looked up // properties of items that can be looked up
// each entry is a function `(item, extended item) -> value`
const itemQueryProps = (function() { const itemQueryProps = (function() {
const props = {}; const props = {};
function prop(names, getProp) {
function prop(names, type, resolve) {
if (Array.isArray(names)) { if (Array.isArray(names)) {
for (name of names) { for (name of names) {
props[name] = getProp; props[name] = { type, resolve };
} }
} else { } else {
props[names] = getProp; props[names] = { type, resolve };
} }
} }
function maxId(names, idKey) { function maxId(names, idKey) {
prop(names, (i, ie) => ie.get('maxRolls').get(idKey) || 0); prop(names, 'number', (i, ie) => ie.get('maxRolls').get(idKey) || 0);
} }
function minId(names, idKey) { function minId(names, idKey) {
prop(names, (i, ie) => ie.get('minRolls').get(idKey) || 0); prop(names, 'number', (i, ie) => ie.get('minRolls').get(idKey) || 0);
} }
function rangeAvg(names, getProp) { function rangeAvg(names, getProp) {
prop(names, (i, ie) => { prop(names, 'number', (i, ie) => {
const range = getProp(i, ie); const range = getProp(i, ie);
if (!range) return 0; if (!range) return 0;
const ndx = range.indexOf('-'); const ndx = range.indexOf('-');
return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2; return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2;
}); });
} }
function map(names, comps, f) {
return prop(names, (i, ie) => { function map(names, comps, outType, f) {
return prop(names, outType, (i, ie) => {
const args = []; const args = [];
for (let k = 0; k < comps.length; k++) args.push(comps[k](i, ie)); for (let k = 0; k < comps.length; k++) args.push(comps[k].resolve(i, ie));
return f.apply(null, args); return f.apply(null, args);
}); });
} }
function sum(names, ...comps) { function sum(names, ...comps) {
return map(names, comps, (...summands) => { return map(names, comps, 'number', (...summands) => {
let total = 0; let total = 0;
for (let i = 0; i < summands.length; i++) total += summands[i]; for (let i = 0; i < summands.length; i++) total += summands[i];
return total; return total;
}); });
} }
prop('name', (i, ie) => i.displayName || i.name); prop('name', 'string', (i, ie) => i.displayName || i.name);
prop('type', (i, ie) => i.type); prop('type', 'string', (i, ie) => i.type);
prop(['cat', 'category'], (i, ie) => i.category); prop(['cat', 'category'], 'string', (i, ie) => i.category);
const tierIndices = { Normal: 0, Unique: 1, Set: 2, Rare: 3, Legendary: 4, Fabled: 5, Mythic: 6 }; 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(['rarityname', 'raritystr', 'tiername', 'tierstr'], 'string', (i, ie) => i.tier);
prop(['rarity', 'tier'], (i, ie) => tierIndices[i.tier]); prop(['rarity', 'tier'], 'string', (i, ie) => tierIndices[i.tier]);
prop(['level', 'lvl', 'combatlevel', 'combatlvl'], (i, ie) => i.lvl); prop(['level', 'lvl', 'combatlevel', 'combatlvl'], 'number', (i, ie) => i.lvl);
prop(['strmin', 'strreq'], (i, ie) => i.strReq); prop(['strmin', 'strreq'], 'number', (i, ie) => i.strReq);
prop(['dexmin', 'dexreq'], (i, ie) => i.dexReq); prop(['dexmin', 'dexreq'], 'number', (i, ie) => i.dexReq);
prop(['intmin', 'intreq'], (i, ie) => i.intReq); prop(['intmin', 'intreq'], 'number', (i, ie) => i.intReq);
prop(['defmin', 'defreq'], (i, ie) => i.defReq); prop(['defmin', 'defreq'], 'number', (i, ie) => i.defReq);
prop(['agimin', 'agireq'], (i, ie) => i.agiReq); prop(['agimin', 'agireq'], 'number', (i, ie) => i.agiReq);
sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin); sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin);
prop('str', (i, ie) => i.str); prop('str', 'number', (i, ie) => i.str);
prop('dex', (i, ie) => i.dex); prop('dex', 'number', (i, ie) => i.dex);
prop('int', (i, ie) => i.int); prop('int', 'number', (i, ie) => i.int);
prop('def', (i, ie) => i.def); prop('def', 'number', (i, ie) => i.def);
prop('agi', (i, ie) => i.agi); prop('agi', 'number', (i, ie) => i.agi);
sum(['skillpoints', 'skillpts', 'attributes', 'attrs'], props.str, props.dex, props.int, props.def, props.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(['neutraldmg', 'neutraldam', 'ndmg', 'ndam'], (i, ie) => i.nDam);
@ -155,15 +110,15 @@ const itemQueryProps = (function() {
maxId(['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw'], 'sdRaw'); 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 }; 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); prop(['attackspeed', 'atkspd'], 'string', (i, ie) => i.atkSpd ? atkSpdIndices[i.atkSpd] : 0);
maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'atktier'], 'atkTier'); maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'atktier'], 'atkTier');
sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier); sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier);
prop(['earthdef', 'edef'], (i, ie) => i.eDef || 0); prop(['earthdef', 'edef'], 'number', (i, ie) => i.eDef || 0);
prop(['thunderdef', 'tdef'], (i, ie) => i.tDef || 0); prop(['thunderdef', 'tdef'], 'number', (i, ie) => i.tDef || 0);
prop(['waterdef', 'wdef'], (i, ie) => i.wDef || 0); prop(['waterdef', 'wdef'], 'number', (i, ie) => i.wDef || 0);
prop(['firedef', 'fdef'], (i, ie) => i.fDef || 0); prop(['firedef', 'fdef'], 'number', (i, ie) => i.fDef || 0);
prop(['airdef', 'adef'], (i, ie) => i.aDef || 0); prop(['airdef', 'adef'], 'number', (i, ie) => i.aDef || 0);
sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef); sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef);
maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct'); maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct');
@ -173,7 +128,7 @@ const itemQueryProps = (function() {
maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct'); maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct');
sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct); sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct);
prop(['health', 'hp'], (i, ie) => i.hp || 0); prop(['health', 'hp'], 'number', (i, ie) => i.hp || 0);
maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'hpbonus'], 'hpBonus'); maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'hpbonus'], 'hpBonus');
sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid); sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid);
@ -207,397 +162,347 @@ const itemQueryProps = (function() {
maxId(['lootbonus', 'lb'], 'lb'); maxId(['lootbonus', 'lb'], 'lb');
maxId(['xpbonus', 'xpb', 'xb'], 'xpb'); maxId(['xpbonus', 'xpb', 'xb'], 'xpb');
maxId(['stealing', 'esteal'], 'eSteal'); maxId(['stealing', 'esteal'], 'eSteal');
prop(['powderslots', 'powders', 'slots', 'sockets'], (i, ie) => i.slots || 0); prop(['powderslots', 'powders', 'slots', 'sockets'], 'number', (i, ie) => i.slots || 0);
return props; return props;
})(); })();
// functions that can be called in query expressions // functions that can be called in query expressions
const itemQueryFuncs = { const itemQueryFuncs = {
max(args) { max: {
type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to max()'); if (args.length < 1) throw new Error('Not enough args to max()');
let runningMax = -Infinity; let runningMax = -Infinity;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (checkNum(args[i]) > runningMax) runningMax = args[i]; if (checkNum(args[i]) > runningMax) runningMax = args[i];
} }
return runningMax; return runningMax;
}
}, },
min(args) { min: {
type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to min()'); if (args.length < 1) throw new Error('Not enough args to min()');
let runningMin = Infinity; let runningMin = Infinity;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (checkNum(args[i]) < runningMin) runningMin = args[i]; if (checkNum(args[i]) < runningMin) runningMin = args[i];
} }
return runningMin; return runningMin;
}
}, },
floor(args) { floor: {
type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to floor()'); if (args.length < 1) throw new Error('Not enough args to floor()');
return Math.floor(checkNum(args[0])); return Math.floor(checkNum(args[0]));
}
}, },
ceil(args) { ceil: {
type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to ceil()'); if (args.length < 1) throw new Error('Not enough args to ceil()');
return Math.ceil(checkNum(args[0])); return Math.ceil(checkNum(args[0]));
}
}, },
round(args) { round: {
if (args.length < 1) throw new Error('Not enough args to ceil()'); type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to round()');
return Math.round(checkNum(args[0])); return Math.round(checkNum(args[0]));
}
}, },
sqrt(args) { sqrt: {
if (args.length < 1) throw new Error('Not enough args to ceil()'); type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to sqrt()');
return Math.sqrt(checkNum(args[0])); return Math.sqrt(checkNum(args[0]));
}
}, },
abs(args) { abs: {
if (args.length < 1) throw new Error('Not enough args to ceil()'); type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to abs()');
return Math.abs(checkNum(args[0])); return Math.abs(checkNum(args[0]));
}
}, },
contains(args) { contains: {
type: 'boolean',
fn: function(item, itemExp, args) {
if (args.length < 2) throw new Error('Not enough args to contains()'); if (args.length < 2) throw new Error('Not enough args to contains()');
return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase()); return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase());
}
}, },
atkspdmod(args) { atkspdmod: {
type: 'number',
fn: function(item, itemExp, args) {
if (args.length < 1) throw new Error('Not enough args to atkSpdMod()'); if (args.length < 1) throw new Error('Not enough args to atkSpdMod()');
switch (checkNum(args[0])) { switch (checkNum(args[0])) {
case 2: return 3.1; case 2:
case 1: return 2.5; return 3.1;
case 0: return 2.05; case 1:
case -1: return 1.5; return 2.5;
case -2: return 0.83; 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 0.51;
if (args[0] >= 3) return 4.3; if (args[0] >= 3) return 4.3;
throw new Error('Invalid argument to atkSpdMod()'); throw new Error('Invalid argument to atkSpdMod()');
} }
}
}; };
// the compiler itself // static type check
const compileQueryExpr = (function() { function staticCheck(expType, term) {
// tokenize an expression string if (expType === 'any' || expType === term.type) {
function tokenize(exprStr) { return true;
exprStr = exprStr.trim();
const tokens = [];
let col = 0;
function pushSymbol(sym) {
tokens.push({ type: 'sym', sym });
col += sym.length;
} }
while (col < exprStr.length) { throw new Error(`Expected ${expType}, but got ${term.type}`);
// 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 // expression terms
function takeDisj(tokens) { class Term {
const left = takeConj(tokens); constructor(type) {
if (tokens.here.type === 'sym' && tokens.here.sym === '|') { this.type = type;
tokens.advance();
const right = takeDisj(tokens);
return (i, ie) => checkBool(left(i, ie)) || checkBool(right(i, ie));
}
return left;
} }
function takeConj(tokens) { resolve(item, itemExt) {
const left = takeCmpEq(tokens); throw new Error('Abstract method!');
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) { class LiteralTerm extends Term {
const left = takeCmpRel(tokens); constructor(type, value) {
if (tokens.here.type === 'sym') { super(type);
switch (tokens.here.sym) { this.value = value;
case '=': { }
tokens.advance();
const right = takeCmpEq(tokens); resolve(item, itemExt) {
return (i, ie) => { return this.value;
const a = left(i, ie), b = right(i, ie); }
if (typeof a !== typeof b) return false; }
switch (typeof a) {
case 'number': class BoolLitTerm extends LiteralTerm {
return Math.abs(left(i, ie) - right(i, ie)) < 1e-4; constructor(value) {
case 'boolean': super('boolean', value);
}
}
class NumLitTerm extends LiteralTerm {
constructor(value) {
super('number', value);
}
}
class StrLitTerm extends LiteralTerm {
constructor(value) {
super('string', value);
}
}
class BinaryOpTerm extends Term {
constructor(type, leftType, left, rightType, right) {
super(type);
staticCheck(leftType, left);
staticCheck(rightType, right);
this.left = left;
this.right = right;
}
resolve(item, itemExt) {
return this.apply(this.left.resolve(item, itemExt), this.right.resolve(item, itemExt));
}
apply(a, b) {
throw new Error('Abstract method!');
}
}
class LogicalTerm extends BinaryOpTerm {
constructor(left, right) {
super('boolean', 'boolean', left, 'boolean', right);
}
}
class ConjTerm extends LogicalTerm {
apply(a, b) {
return a && b;
}
}
class DisjTerm extends LogicalTerm {
apply(a, b) {
return a || b;
}
}
class EqualityTerm extends BinaryOpTerm {
constructor(left, right) {
super('boolean', 'any', left, 'any', right);
}
}
class EqTerm extends EqualityTerm {
apply(a, b) {
return a === b; return a === b;
case 'string':
return a.toLowerCase() === b.toLowerCase();
} }
throw new Error('???'); // wut
};
} }
case '!=': {
tokens.advance(); class NeqTerm extends EqualityTerm {
const right = takeCmpEq(tokens); apply(a, b) {
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; return a !== b;
case 'string':
return a.toLowerCase() !== b.toLowerCase();
} }
throw new Error('???'); // wtf
};
} }
case '?=': {
tokens.advance(); class ContainsTerm extends BinaryOpTerm {
const right = takePrim(tokens); constructor(left, right) {
return (i, ie) => { super('boolean', 'string', left, 'string', right);
const a = left(i, ie), b = right(i, ie); }
if (typeof a !== typeof b) return false;
switch (typeof a) { apply(a, b) {
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()); return a.toLowerCase().includes(b.toLowerCase());
} }
throw new Error('???'); // wtf
};
}
}
}
return left;
} }
function takeCmpRel(tokens) { class InequalityTerm extends BinaryOpTerm {
const left = takeSum(tokens); constructor(left, right) {
if (tokens.here.type === 'sym') { super('boolean', 'any', left, 'any', right);
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) { apply(a, b) {
const left = takeProd(tokens); checkComparable(a);
if (tokens.here.type === 'sym') { checkComparable(b);
switch (tokens.here.sym) { return this.compare(a, b);
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) { compare(a, b) {
const left = takeExp(tokens); throw new Error('Abstract method!');
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) { class LeqTerm extends InequalityTerm {
const left = takeUnary(tokens); compare(a, b) {
if (tokens.here.type === 'sym' && tokens.here.sym === '^') { return a <= b;
tokens.advance();
const right = takeExp(tokens);
return (i, ie) => checkNum(left(i, ie)) ** checkNum(right(i, ie));
} }
return left;
} }
function takeUnary(tokens) { class LtTerm extends InequalityTerm {
if (tokens.here.type === 'sym') { compare(a, b) {
switch (tokens.here.sym) { return a < b;
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) { class GtTerm extends InequalityTerm {
switch (tokens.here.type) { compare(a, b) {
case 'num': { return a > b;
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();
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 class GeqTerm extends InequalityTerm {
return function(exprStr) { compare(a, b) {
const tokens = tokenize(exprStr); return a >= b;
return tokens.tokens.length <= 1 ? null : takeDisj(tokens); }
}; }
})();
class ArithmeticTerm extends BinaryOpTerm {
constructor(left, right) {
super('number', 'number', left, 'number', right);
}
}
class AddTerm extends ArithmeticTerm {
apply(a, b) {
return a + b;
}
}
class SubTerm extends ArithmeticTerm {
apply(a, b) {
return a - b;
}
}
class MulTerm extends ArithmeticTerm {
apply(a, b) {
return a * b;
}
}
class DivTerm extends ArithmeticTerm {
apply(a, b) {
return a / b;
}
}
class ExpTerm extends ArithmeticTerm {
apply(a, b) {
return a ** b;
}
}
class UnaryOpTerm extends Term {
constructor(type, inType, inVal) {
super(type);
staticCheck(inType, inVal);
this.inVal = inVal;
}
resolve(item, itemExt) {
return this.apply(this.inVal.resolve(item, itemExt));
}
apply(x) {
throw new Error('Abstract method!');
}
}
class NegTerm extends UnaryOpTerm {
constructor(inVal) {
super('number', 'number', inVal);
}
apply(x) {
return -x;
}
}
class InvTerm extends UnaryOpTerm {
constructor(inVal) {
super('boolean', 'boolean', inVal);
}
apply(x) {
return !x;
}
}
class FnCallTerm extends Term {
constructor(fn, argExprs) {
super(fn.type);
this.fn = fn;
this.argExprs = argExprs;
}
resolve(item, itemExt) {
const argVals = [];
for (const argExpr of this.argExprs) {
argVals.push(argExpr.resolve(item, itemExt));
}
return this.fn.fn(item, itemExt, argVals);
}
}
class PropTerm extends Term {
constructor(prop) {
super(prop.type);
this.prop = prop;
}
resolve(item, itemExt) {
return this.prop.resolve(item, itemExt);
}
}