Implement item list + minor memory optimizations for displaying items

This commit is contained in:
phantamanta44 2021-01-25 16:06:41 -06:00
parent 2eb187afeb
commit d17bb7e106
6 changed files with 724 additions and 57 deletions

View file

@ -6,13 +6,15 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier,
let buildStats = new Map(stats);
if(externalStats) { //if nothing is passed in, then this hopefully won't trigger
for (const [key,value] of externalStats) {
for (let i = 0; i < externalStats.length; i++) {
const key = externalStats[i][0];
const value = externalStats[i][1];
if (typeof value === "number") {
buildStats.set(key, buildStats.get(key) + value);
} else if (Array.isArray(value)) {
arr = [];
for (let i = 0; i < value.length; i++) {
arr[i] = buildStats.get(key)[i] + value[i];
for (let j = 0; j < value.length; j++) {
arr[j] = buildStats.get(key)[j] + value[j];
}
buildStats.set(key, arr);
}
@ -21,8 +23,9 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier,
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
let damages = [];
for (const damage_string of buildStats.get("damageRaw")) {
const damage_vals = damage_string.split("-").map(Number);
const rawDamages = buildStats.get("damageRaw");
for (let i = 0; i < rawDamages.length; i++) {
const damage_vals = rawDamages[i].split("-").map(Number);
damages.push(damage_vals);
}
@ -40,10 +43,11 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier,
}
//console.log(damages);
let rawBoosts = [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]];
for (const powderID of weapon.get("powders")) {
const powder = powderStats[powderID];
const powders = weapon.get("powders");
for (let i = 0; i < powders.length; i++) {
const powder = powderStats[powders[i]];
// Bitwise to force conversion to integer (integer division).
const element = (powderID/6) | 0;
const element = (powders[i]/6) | 0;
let conversionRatio = powder.convert/100;
if (neutralRemainingRaw[1] > 0) {
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);

View file

@ -420,7 +420,7 @@ function displayExpandedItem(item, parent_id){
let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], 0, 0, 0, item, [0, 0, 0, 0, 0], 1, undefined);
let damages = results[2];
let damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ];
for (const i in damage_keys) {
for (let i = 0; i < damage_keys.length; i++) {
item.set(damage_keys[i], damages[i][0]+"-"+damages[i][1]);
}
}
@ -483,7 +483,8 @@ function displayExpandedItem(item, parent_id){
let active_elem;
let fix_id = item.has("fixID") && item.get("fixID");
let elemental_format = false;
for (const command of display_commands) {
for (let i = 0; i < display_commands.length; i++) {
const command = display_commands[i];
if (command.charAt(0) === "#") {
if (command === "#cdiv") {
active_elem = document.createElement('div');
@ -696,12 +697,12 @@ function displayExpandedItem(item, parent_id){
effects = powderSpecial["armorSpecialEffects"];
specialTitle.textContent += powderSpecial["armorSpecialName"] + ": ";
}
for (const [key,value] of effects) {
if (key !== "Description") {
for (let i = 0; i < effects.length; i++) {
if (effects[i][0] !== "Description") {
let effect = document.createElement("p");
effect.classList.add("itemp");
effect.textContent += key + ": " + value[power] + specialSuffixes.get(key);
if(key === "Damage"){
effect.textContent += effects[i][0] + ": " + effects[i][1][power] + specialSuffixes.get(effects[i][0]);
if(effects[i][0] === "Damage"){
effect.textContent += elementIcons[skp_elements.indexOf(element)];
}
if (element === "w") {

View file

@ -8,11 +8,11 @@
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.png">
<link rel="manifest" href="manifest.json">
<title>Wynn Clientside</title>
<title>WynnBuilder</title>
</head>
<body class="all">
<body class="all" style="overflow-y: scroll">
<div class="header" id="header">
WynnAtlas
WynnBuilder Item List
</div>
<div class="center" id="header2">
Made by: hppeng and ferricles
@ -20,20 +20,27 @@
<div class="center" id="credits">
<a href="credits.txt">Additional credits</a>
</div>
<br>
<br>
<br>
<br>
<div class="center" id="main">
<div class="box" id="test" style="width: 25vw; margin: auto">
<div class="center" id="test-inner">item</div>
<div class="center" id="main" style="padding: 2%">
<div id="search-container" style="margin-bottom: 1.5%">
<div class="left" id="search" style="display: inline-block">
<label for="search-field">Search Items:</label>
<br>
<input id="search-field" type="text" style="width: 50vw; padding: 8px">
<br>
<div id="search-error" style="color: #ff0000"></div>
</div>
</div>
<div id="item-list-container">
<div class="left" id="item-list" style="display: inline-block"></div>
<div class="center" id="item-list-footer"></div>
</div>
</div>
<script type="text/javascript" src="/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>

View file

@ -1,11 +1,65 @@
function init() {
let items_copy = items.slice();
let query = new NameQuery("Bob's");
items_copy = items_copy.filter(query.filter, query).sort(query.compare);
let item = items_copy[0];
console.log(item);
displayExpandedItem(expandItem(item, []), "test");
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
const searchField = document.getElementById('search-field');
const searchError = document.getElementById('search-error');
const itemList = document.getElementById('item-list');
const itemListFooter = document.getElementById('item-list-footer');
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.width = '20vw';
itemElem.style.margin = '1vw';
itemElem.style.verticalAlign = 'top';
itemList.append(itemElem);
itemEntries.push(itemElem);
}
let currentSearchStr = null;
function updateSearch() {
if (searchField.value === currentSearchStr) return;
currentSearchStr = searchField.value;
itemListFooter.innerText = '';
try {
for (const itemEntry of itemEntries) itemEntry.style.display = 'none';
const searchState = ItemSearchState.parseSearchString(currentSearchStr);
const searchResults = [];
for (let i = 0; i < searchDb.length; i++) {
if (searchState.test(searchDb[i][0], searchDb[i][1])) searchResults.push(searchDb[i]);
}
if (searchResults.length === 0) {
itemListFooter.innerText = 'No results!';
} else {
searchResults.sort((a, b) => searchState.compare(a[0], a[1], b[0], b[1]));
const searchMax = Math.min(searchResults.length, ITEM_LIST_SIZE);
for (let i = 0; i < searchMax; i++) {
itemEntries[i].style.display = 'inline-block';
displayExpandedItem(searchResults[i][1], `item-entry-${i}`);
}
if (searchMax < searchResults.length) {
itemListFooter.innerText = `${searchResults.length - searchMax} more...`;
}
}
searchError.innerText = '';
} catch (e) {
searchError.innerText = e.message;
}
}
let updateSearchTask = null;
searchField.addEventListener('input', e => {
if (updateSearchTask !== null) {
clearTimeout(updateSearchTask);
}
updateSearchTask = setTimeout(() => {
updateSearchTask = null;
updateSearch();
}, 300);
});
updateSearch();
}
load_init(init);

639
query.js
View file

@ -1,21 +1,622 @@
// ported from Wynntils item search infrastructure, which is licensed under GNU AGPL 3
// the search string format is not one-to-one, but it's close enough
function quoteIfContainsSpace(str) {
return str.indexOf(' ') === -1 ? str : `"${str}"`;
}
class Type {
static typeRegistry = new Map();
static getType(name) {
const type = Type.typeRegistry.get(name.toLowerCase());
if (type == null) throw new Error(`Unknown filter type: ${name}`);
return type;
}
constructor(name, desc, aliases, parseFn) {
if (!Array.isArray(aliases)) {
parseFn = aliases;
aliases = [];
}
this.name = name;
this.desc = desc;
this.parseFn = parseFn;
Type.typeRegistry.set(name.toLowerCase(), this);
for (const alias of aliases) {
Type.typeRegistry.set(alias.toLowerCase(), this);
}
}
parse(filterStr) {
return this.parseFn(filterStr);
}
}
const SortDirection = {
ascending: { prefix: '^', modifyComparison: cmp => cmp },
descending: { prefix: '$', modifyComparison: cmp => -cmp },
none: { prefix: '', modifyComparison: cmp => 0 }
};
const Comparison = {
lt: { symbol: '<', test: cmp => cmp < 0 },
leq: { symbol: '<=', test: cmp => cmp <= 0 },
eq: { symbol: '=', test: cmp => cmp === 0 },
neq: { symbol: '!=', test: cmp => cmp !== 0 },
geq: { symbol: '>=', test: cmp => cmp >= 0 },
gt: { symbol: '>', test: cmp => cmp > 0 }
};
function parseSortDirection(filterStr) {
if (filterStr) {
switch (filterStr.charAt(0)) {
case '^':
return [filterStr.substring(1), SortDirection.ascending];
case '$':
return [filterStr.substring(1), SortDirection.descending];
}
}
return [filterStr, SortDirection.none];
}
function parseComparisons(filterStr, extractKey) {
if (!filterStr) return [];
const rels = [];
const relStrs = filterStr.split(',');
for (let i = 0; i < relStrs.length; i++) {
const relStr = relStrs[i];
if (!relStr) continue;
if (relStr.startsWith('<=')) {
rels.push([Comparison.leq, extractKey(relStr.substring(2))]);
} else if (relStr.startsWith('>=')) {
rels.push([Comparison.geq, extractKey(relStr.substring(2))]);
} else if (relStr.startsWith('!=')) {
rels.push([Comparison.neq, extractKey(relStr.substring(2))]);
} else {
switch (relStr.charAt(0)) {
case '<':
rels.push([Comparison.lt, extractKey(relStr.substring(1))]);
continue;
case '>':
rels.push([Comparison.gt, extractKey(relStr.substring(1))]);
continue;
case '=':
rels.push([Comparison.eq, extractKey(relStr.substring(1))]);
continue;
}
const rangeDelimNdx = relStr.indexOf('..');
if (rangeDelimNdx === -1) {
rels.push([Comparison.eq, extractKey(relStr)]);
continue;
}
rels.push([Comparison.geq, extractKey(relStr.substring(0, rangeDelimNdx))]);
rels.push([Comparison.leq, extractKey(relStr.substring(rangeDelimNdx + 2))]);
}
}
return rels;
}
class NameFilter {
static TYPE = new Type('Name', 'Item Name', function(filterStr) {
return new NameFilter(...parseSortDirection(filterStr));
});
constructor(searchStr, sortDir) {
this.searchStr = searchStr.toLowerCase();
this.sortDir = sortDir;
}
adjoin(other) { // this is a hack; don't call this unless you're ItemSearchState!
this.searchStr = !this.searchStr ? other.searchStr : `${this.searchStr} ${other.searchStr}`;
}
getFilterType() {
return NameFilter.TYPE;
}
toFilterString() {
const str = `${NameFilter.TYPE.name}:${this.sortDir.prefix}`;
return !this.searchStr ? str : (str + quoteIfContainsSpace(this.searchStr));
}
test(item, itemExp) {
return (item.displayName || item.name).toLowerCase().includes(this.searchStr);
}
compare(a, aExp, b, bExp) {
return this.sortDir.modifyComparison((a.displayName || a.name).toLowerCase().localeCompare((b.displayName || b.name).toLowerCase()));
}
}
class TypeFilter {
static TYPE = new Type('Type', 'Item Type', function(filterStr) {
const [typeStr, sortDir] = parseSortDirection(filterStr);
const allowedTypes = new Set();
const tokens = typeStr.split(',');
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i].trim().toLowerCase();
if (!token) continue;
switch (token) {
case 'armor':
case 'armour':
allowedTypes.push(...TypeFilter.ARMOUR_TYPES);
break;
case 'weapon':
allowedTypes.push(...TypeFilter.WEAPON_TYPES);
break;
case 'accessory':
case 'bauble':
allowedTypes.push(...TypeFilter.ACCESSORY_TYPES);
break;
default:
throw new Error(`Unknown item type: ${token.trim()}`);
}
}
return new TypeFilter(allowedTypes, sortDir);
});
static WEAPON_TYPES = ['wand', 'dagger', 'spear', 'bow', 'relik'];
static ARMOUR_TYPES = ['helmet', 'chestplate', 'leggings', 'boots'];
static ACCESSORY_TYPES = ['necklace', 'ring', 'bracelet'];
static TYPE_INDICES = (function() {
const typeIndices = {};
let i = 0;
for (const type of [...TypeFilter.WEAPON_TYPES, ...TypeFilter.ARMOUR_TYPES, ...TypeFilter.ACCESSORY_TYPES]) {
typeIndices[type] = i++;
}
return typeIndices;
})();
constructor(allowedTypes, sortDir) {
this.allowedTypes = allowedTypes;
this.sortDir = sortDir;
}
getFilterType() {
return TypeFilter.TYPE;
}
toFilterString() {
const keys = [];
this.categoryToFilterString(TypeFilter.ARMOUR_TYPES, 'armor', keys);
this.categoryToFilterString(TypeFilter.WEAPON_TYPES, 'weapon', keys);
this.categoryToFilterString(TypeFilter.ACCESSORY_TYPES, 'accessory', keys);
const str = `${TypeFilter.TYPE.name}:${this.sortDir.prefix}`;
return keys.length === 0 ? str : (str + quoteIfContainsSpace(keys.join(',')));
}
categoryToFilterString(types, categoryName, dest) {
for (let i = 0; i < types.length; i++) {
if (!this.allowedTypes.has(types[i])) {
// not all the types in the category are allowed, so add all the individual types
for (let i = 0; i < types.length; i++) {
if (this.allowedTypes.has(types[i])) dest.add(types[i].name.toLowerCase());
}
return;
}
}
dest.add(categoryName); // all the types in the category are allowed, so just add the category
}
test(item, itemExp) {
return this.allowedTypes.has(item.type);
}
compare(a, aExp, b, bExp) {
return this.sortDir.modifyComparison(TypeFilter.TYPE_INDICES[a.type] - TypeFilter.TYPE_INDICES[b.type]);
}
}
class RarityFilter {
static TYPE = new Type('Rarity', 'Item Rarity', ['Tier'], function(filterStr) {
const [rarityStr, sortDir] = parseSortDirection(filterStr);
return new RarityFilter(parseComparisons(rarityStr, s => s.trim().toLowerCase()), sortDir);
});
static RARITY_INDICES = { 'normal': 0, 'unique': 1, 'rare': 2, 'legendary': 3, 'fabled': 4, 'mythic': 5 };
constructor(comps, sortDir) {
this.comps = comps;
this.sortDir = sortDir;
}
getFilterType() {
return RarityFilter.TYPE;
}
toFilterString() {
return `${RarityFilter.TYPE.name}:${this.sortDir.prefix}${this.comps.map(this.stringifyComparison).join(',')}`;
}
stringifyComparison([comp, rarity]) {
return `${comp === Comparison.eq ? '' : comp.symbol}${rarity}`
}
test(item, itemExp) {
for (let i = 0; i < this.comps.length; i++) {
if (!this.comps[i][0].test(RarityFilter.RARITY_INDICES[item.tier.toLowerCase()] - RarityFilter.RARITY_INDICES[this.comps[i][1]])) return false;
}
return true;
}
compare(a, aExp, b, bExp) {
return this.sortDir.modifyComparison(RarityFilter.RARITY_INDICES[a.tier.toLowerCase()] - RarityFilter.RARITY_INDICES[b.tier.toLowerCase()]);
}
}
class StatType extends Type {
static sum(name, desc, ...summands) {
return new StatType(name, desc, i => {
let value = 0;
for (let i = 0; i < summands.length; i++) value += summands[i].extractStat(i);
return value;
});
}
constructor(name, desc, aliases, statExtractor) {
super(name, desc, aliases);
this.statExtractor = statExtractor;
}
extractStat(item, itemExp) {
return this.statExtractor(item, itemExp);
}
parse(filterStr) {
const [valueStr, sortDir] = parseSortDirection(filterStr);
const comparisons = parseComparisons(valueStr, s => {
const value = parseInt(s, 10);
if (isNaN(value)) throw new Error(`Not a number: ${s}`);
return value;
});
return new StatFilter(this, comparisons, sortDir);
}
}
function getAverageDamage(item, dmgType) {
return item.hasOwnProperty(dmgType) ? item[dmgType].split('-').map(s => parseInt(s, 10)) : 0;
}
function getMaxRoll(base) {
return base === 0 ? 0 : (base > 0 ? Math.round(base * 1.3) : Math.round(base ))
}
class StatFilter {
// requirements
static TYPE_COMBAT_LEVEL = new StatType('Level', 'Combat Level', ['Lvl', 'CombatLevel', 'CombatLvl'], (i, ie) => i.lvl);
static TYPE_STR_REQ = new StatType('StrReq', 'Strength Min', ['StrMin'], (i, ie) => i.strReq);
static TYPE_DEX_REQ = new StatType('DexReq', 'Dexterity Min', ['DexMin'], (i, ie) => i.dexReq);
static TYPE_INT_REQ = new StatType('IntReq', 'Intelligence Min', ['IntMin'], (i, ie) => i.intReq);
static TYPE_DEF_REQ = new StatType('DefReq', 'Defence Min', ['DefMin'], (i, ie) => i.defReq);
static TYPE_AGI_REQ = new StatType('AgiReq', 'Agility Min', ['AgiMin'], (i, ie) => i.agiReq);
static TYPE_SUM_REQ = StatType.sum('SumReq', 'Total Skill Points Min', ['SumMin', 'TotalReq', 'TotalMin'],
StatFilter.TYPE_STR_REQ, StatFilter.TYPE_DEX_REQ, StatFilter.TYPE_INT_REQ, StatFilter.TYPE_DEF_REQ, StatFilter.TYPE_AGI_REQ);
// damages
static TYPE_NEUTRAL_DMG = new StatType('NeutralDmg', 'Neutral Damage', (i, ie) => getAverageDamage(i, 'nDam'));
static TYPE_EARTH_DMG = new StatType('EarthDmg', 'Earth Damage', (i, ie) => getAverageDamage(i, 'eDam'));
static TYPE_THUNDER_DMG = new StatType('ThunderDmg', 'Thunder Damage', (i, ie) => getAverageDamage(i, 'tDam'));
static TYPE_WATER_DMG = new StatType('WaterDmg', 'Water Damage', (i, ie) => getAverageDamage(i, 'wDam'));
static TYPE_FIRE_DMG = new StatType('FireDmg', 'Fire Damage', (i, ie) => getAverageDamage(i, 'fDam'));
static TYPE_AIR_DMG = new StatType('AirDmg', 'Air Damage', (i, ie) => getAverageDamage(i, 'aDam'));
static TYPE_SUM_DMG = StatType.sum('SumDmg', 'Total Damage', ['TotalDmg'],
StatFilter.TYPE_NEUTRAL_DMG, StatFilter.TYPE_EARTH_DMG, StatFilter.TYPE_THUNDER_DMG,
StatFilter.TYPE_WATER_DMG, StatFilter.TYPE_FIRE_DMG, StatFilter.TYPE_AIR_DMG);
// defenses
static TYPE_HEALTH = new StatType('Health', 'Health', ['hp'], (i, ie) => i.hp || 0);
static TYPE_EARTH_DEF = new StatType('EarthDef', 'Earth Defence', (i, ie) => i.eDef || 0);
static TYPE_THUNDER_DEF = new StatType('ThunderDef', 'Thunder Defence', (i, ie) => i.tDef || 0);
static TYPE_WATER_DEF = new StatType('WaterDef', 'Water Defence', (i, ie) => i.wDef || 0);
static TYPE_FIRE_DEF = new StatType('FireDef', 'Fire Defence', (i, ie) => i.fDef || 0);
static TYPE_AIR_DEF = new StatType('AirDef', 'Air Defence', (i, ie) => i.aDef || 0);
static TYPE_SUM_DEF = StatType.sum('SumDef', 'Total Defence', ['TotalDef'],
StatFilter.TYPE_EARTH_DEF, StatFilter.TYPE_THUNDER_DEF, StatFilter.TYPE_WATER_DEF, StatFilter.TYPE_FIRE_DEF, StatFilter.TYPE_AIR_DEF);
// attribute ids
static TYPE_STR = new StatType('Str', 'Strength', (i, ie) => i.str);
static TYPE_DEX = new StatType('Dex', 'Dexterity', (i, ie) => i.dex);
static TYPE_INT = new StatType('Int', 'Intelligence', (i, ie) => i.int);
static TYPE_DEF = new StatType('Def', 'Defence', (i, ie) => i.def);
static TYPE_AGI = new StatType('Agi', 'Agility', (i, ie) => i.agi);
static TYPE_SKILL_POINTS = StatType.sum('SkillPoints', 'Total Skill Points', ['SkillPts', 'Attributes', 'Attrs'],
StatFilter.TYPE_STR, StatFilter.TYPE_DEX, StatFilter.TYPE_INT, StatFilter.TYPE_DEF, StatFilter.TYPE_AGI);
// damage ids
static TYPE_MAIN_ATK_NEUTRAL_DMG = new StatType('MainAtkNeutralDmg', 'Main Attack Neutral Damage', ['MainAtkRawDmg'], (i, ie) => ie.get('maxRolls').get('mdRaw'));
static TYPE_MAIN_ATK_DMG = new StatType('MainAtkDmg', 'Main Attack Damage %', ['MainAtkDmg%', '%MainAtkDmg', 'Melee%', '%Melee'], (i, ie) => i.get('maxRolls').get('mdPct'));
static TYPE_SPELL_NEUTRAL_DMG = new StatType('SpellNeutralDmg', 'Neutral Spell Damage', ['SpellRawDmg'], (i, ie) => ie.get('maxRolls').get('sdRaw'));
static TYPE_SPELL_DMG = new StatType('SpellDmg', 'Spell Damage %', ['SpellDmg%', '%SpellDmg', 'Spell%', '%Spell', 'sd'], (i, ie) => ie.get('maxRolls').get('sdPct'));
static TYPE_BONUS_EARTH_DMG = new StatType('BonusEarthDmg', 'Earth Damage %', ['EarthDmg%', '%EarthDmg'], (i, ie) => ie.get('maxRolls').get('eDamPct'));
static TYPE_BONUS_THUNDER_DMG = new StatType('BonusThunderDmg', 'Thunder Damage %', ['ThunderDmg%', '%ThunderDmg'], (i, ie) => ie.get('maxRolls').get('tDamPct'));
static TYPE_BONUS_WATER_DMG = new StatType('BonusWaterDmg', 'Water Damage %', ['WaterDmg%', '%WaterDmg'], (i, ie) => ie.get('maxRolls').get('wDamPct'));
static TYPE_BONUS_FIRE_DMG = new StatType('BonusFireDmg', 'Fire Damage %', ['FireDmg%', '%FireDmg'], (i, ie) => ie.get('maxRolls').get('fDamPct'));
static TYPE_BONUS_AIR_DMG = new StatType('BonusAirDmg', 'Air Damage %', ['AirDmg%', '%AirDmg'], (i, ie) => ie.get('maxRolls').get('aDamPct'));
static TYPE_BONUS_SUM_DMG = StatType.sum('BonusSumDmg', 'Total Damage %', ['SumDmg%', '%SumDmg', 'BonusTotalDmg', 'TotalDmg%', '%TotalDmg'],
StatFilter.TYPE_BONUS_EARTH_DMG, StatFilter.TYPE_BONUS_THUNDER_DMG, StatFilter.TYPE_BONUS_WATER_DMG, StatFilter.TYPE_BONUS_FIRE_DMG, StatFilter.TYPE_BONUS_AIR_DMG);
// defense ids
static TYPE_BONUS_HEALTH = new StatType('BonusHealth', 'Bonus Health', ['Health+', 'hp+'], (i, ie) => ie.get('maxRolls').get('hpBonus'));
static TYPE_SUM_HEALTH = StatType.sum('SumHealth', 'Total Health', ['SumHp', 'TotalHealth', 'TotalHp'],
StatFilter.TYPE_HEALTH, StatFilter.TYPE_BONUS_HEALTH);
static TYPE_BONUS_EARTH_DEF = new StatType('BonusEarthDef', 'Earth Defence %', ['EarthDef%', '%EarthDef'], (i, ie) => ie.get('maxRolls').get('eDefPct'));
static TYPE_BONUS_THUNDER_DEF = new StatType('BonusThunderDef', 'Thunder Defence %', ['ThunderDef%', '%ThunderDef'], (i, ie) => ie.get('maxRolls').get('tDefPct'));
static TYPE_BONUS_WATER_DEF = new StatType('BonusWaterDef', 'Water Defence %', ['WaterDef%', '%WaterDef'], (i, ie) => ie.get('maxRolls').get('wDefPct'));
static TYPE_BONUS_FIRE_DEF = new StatType('BonusFireDef', 'Fire Defence %', ['FireDef%', '%FireDef'], (i, ie) => ie.get('maxRolls').get('fDefPct'));
static TYPE_BONUS_AIR_DEF = new StatType('BonusAirDef', 'Air Defence %', ['AirDef%', '%AirDef'], (i, ie) => ie.get('maxRolls').get('aDefPct'));
static TYPE_BONUS_SUM_DEF = StatType.sum('BonusSumDef', 'Total Defence %', ['SumDef%', '%SumDef', 'BonusTotalDef', 'TotalDef%', '%TotalDef'],
StatFilter.TYPE_BONUS_EARTH_DEF, StatFilter.TYPE_BONUS_THUNDER_DEF, StatFilter.TYPE_BONUS_WATER_DEF,StatFilter.TYPE_BONUS_FIRE_DEF, StatFilter.TYPE_BONUS_AIR_DEF);
// resource regen ids
static TYPE_RAW_HEALTH_REGEN = new StatType('RawHealthRegen', 'Health Regen', ['hpr', 'hr'], (i, ie) => ie.get('maxRolls').get('hprRaw'));
static TYPE_HEALTH_REGEN = new StatType('HealthRegen', 'Health Regen %', ['hpr%', '%hpr', 'hr%', '%hr'], (i, ie) => ie.get('maxRolls').get('hprPct'));
static TYPE_LIFE_STEAL = new StatType('LifeSteal', 'Life Steal', ['ls'], (i, ie) => ie.get('maxRolls').get('ls'));
static TYPE_MANA_REGEN = new StatType('ManaRegen', 'Mana Regen', ['mr'], (i, ie) => ie.get('maxRolls').get('mr'));
static TYPE_MANA_STEAL = new StatType('ManaSteal', 'Mana Steal', ['ms'], (i, ie) => ie.get('maxRolls').get('ms'));
// movement ids
static TYPE_WALK_SPEED = new StatType('WalkSpeed', 'Walk Speed', ['MoveSpeed', 'ws'], (i, ie) => ie.get('maxRolls').get('spd'));
static TYPE_SPRINT = new StatType('Sprint', 'Sprint', (i, ie) => ie.get('maxRolls').get('sprint'));
static TYPE_SPRINT_REGEN = new StatType('SprintRegen', 'Sprint Regen', (i, ie) => ie.get('maxRolls').get('sprintReg'));
static TYPE_JUMP_HEIGHT = new StatType('JumpHeight', 'Jump Height', ['jh'], (i, ie) => ie.get('maxRolls').get('jh'));
// spell cost ids
static TYPE_RAW_SPELL_COST_1 = new StatType('RawSpellCost1', '1st Spell Cost', (i, ie) => ie.get('minRolls').get('spRaw1'));
static TYPE_SPELL_COST_1 = new StatType('SpellCost1', '1st Spell Cost %', (i, ie) => ie.get('minRolls').get('spPct1'));
static TYPE_RAW_SPELL_COST_2 = new StatType('RawSpellCost2', '2nd Spell Cost', (i, ie) => ie.get('minRolls').get('spRaw2'));
static TYPE_SPELL_COST_2 = new StatType('SpellCost2', '2nd Spell Cost %', (i, ie) => ie.get('minRolls').get('spPct2'));
static TYPE_RAW_SPELL_COST_3 = new StatType('RawSpellCost3', '3rd Spell Cost', (i, ie) => ie.get('minRolls').get('spRaw3'));
static TYPE_SPELL_COST_3 = new StatType('SpellCost3', '3rd Spell Cost %', (i, ie) => ie.get('minRolls').get('spPct3'));
static TYPE_RAW_SPELL_COST_4 = new StatType('RawSpellCost4', '4th Spell Cost', (i, ie) => ie.get('minRolls').get('spRaw4'));
static TYPE_SPELL_COST_4 = new StatType('SpellCost4', '4th Spell Cost %', (i, ie) => ie.get('minRolls').get('spPct4'));
static TYPE_RAW_SPELL_COST_SUM = StatType.sum('RawSpellCostSum', 'Total Spell Cost', ['RawSpellCostTotal'],
StatFilter.TYPE_RAW_SPELL_COST_1, StatFilter.TYPE_RAW_SPELL_COST_2, StatFilter.TYPE_RAW_SPELL_COST_3, StatFilter.TYPE_RAW_SPELL_COST_4);
static TYPE_SPELL_COST_SUM = StatType.sum('SpellCostSum', 'Total Spell Cost %', ['SpellCostTotal'],
StatFilter.TYPE_SPELL_COST_1, StatFilter.TYPE_SPELL_COST_2, StatFilter.TYPE_SPELL_COST_3, StatFilter.TYPE_SPELL_COST_4);
// loot and xp ids
static TYPE_LOOT_BONUS = new StatType('LootBonus', 'Loot Bonus', ['lb'], (i, ie) => ie.get('maxRolls').get('lb'));
static TYPE_STEALING = new StatType('Stealing', 'Stealing', (i, ie) => ie.get('maxRolls').get('eSteal'));
static TYPE_XP_BONUS = new StatType('XpBonus', 'Xp Bonus', ['xp', 'xb', 'xpb'], (i, ie) => ie.get('maxRolls').get('xpb'));
static TYPE_LOOT_QUALITY = new StatType('LootQuality', 'Loot Quality', ['lq'], (i, ie) => ie.get('maxRolls').get('lq'));
static TYPE_GATHERING_XP_BONUS = new StatType('GatherXpBonus', 'Gathering Xp Bonus', ['gxp', 'gxpb'], (i, ie) => ie.get('maxRolls').get('gXp'));
static TYPE_GATHERING_SPEED = new StatType('GatherSpeed', 'Gathering Speed', ['gs', 'gspd'], (i, ie) => ie.get('maxRolls').get('gSpd'));
// other ids
static TYPE_BONUS_ATK_SPD = new StatType('BonusAtkSpd', 'Bonus Attack Speed', ['AtkSpd+'], (i, ie) => ie.get('maxRolls').get('atkTier'));
static TYPE_EXPLODING = new StatType('Exploding', 'Exploding', (i, ie) => ie.get('maxRolls').get('expd'));
static TYPE_POISON = new StatType('Poison', 'Poison', (i, ie) => ie.get('maxRolls').get('poison'));
static TYPE_THORNS = new StatType('Thorns', 'Thorns', (i, ie) => ie.get('maxRolls').get('thorns'));
static TYPE_REFLECTION = new StatType('Reflection', 'Reflection', (i, ie) => ie.get('maxRolls').get('ref'));
static TYPE_SOUL_POINT_REGEN = new StatType('SoulPointRegen', 'Soul Point Regen', (i, ie) => ie.get('maxRolls').get('spRegen'));
// other stuff
static TYPE_ATTACK_SPEED = new StatType('AtkSpd', 'Attack Speed', (i, ie) => i.atkSpd ? StatFilter.ATK_SPD_INDICES[i.atkSpd] : 0);
static TYPE_ATK_SPD_SUM = StatType.sum('SumAtkSpd', 'Total Attack Speed', ['TotalAtkSpd'],
StatFilter.TYPE_BONUS_ATK_SPD, StatFilter.TYPE_ATTACK_SPEED);
static TYPE_POWDER_SLOTS = new StatType('PowderSlots', 'Powder Slot Count', ['Powders'], (i, ie) => i.slots);
static ATK_SPD_INDICES = { 'SUPER_SLOW': -3, 'VERY_SLOW': -2, 'SLOW': -1, 'NORMAL': 0, 'FAST': 1, 'VERY_FAST': 2, 'SUPER_FAST': 3 };
constructor(type, comps, sortDir) {
this.type = type;
this.comps = comps;
this.sortDir = sortDir;
}
getFilterType() {
return this.type;
}
toFilterString() {
return `${this.type.name}:${this.sortDir.prefix}${this.comps.map(this.stringifyComparison).join(',')}`;
}
stringifyComparison([comp, value]) {
return `${comp === Comparison.eq ? '' : comp.symbol}${value}`;
}
test(item, itemExp) {
for (let i = 0; i < this.comps.length; i++) {
if (!this.comps[i][0].test(this.type.extractStat(item, itemExp) - this.comps[i][1])) return false;
}
return true;
}
compare(a, aExp, b, bExp) {
return this.sortDir.modifyComparison(this.type.extractStat(a, aExp) - this.type.extractStat(b, bExp));
}
}
class StringType extends Type {
constructor(name, desc, aliases, stringExtractor) {
super(name, desc, aliases);
this.stringExtractor = stringExtractor;
}
extractString(item, itemExp) {
return this.stringExtractor(item, itemExp);
}
parse(filterStr) {
return new StringFilter(this, ...parseSortDirection(filterStr));
}
}
class StringFilter {
static TYPE_SET = new StringType('Set', (i, ie) => i.set);
static TYPE_RESTRICTION = new StringType('Restriction', 'Item Restriction', (i, ie) => i.restrict || null);
constructor(type, matchStr, sortDir) {
this.type = type;
this.matchStr = matchStr.toLowerCase();
this.sortDir = sortDir;
}
getFilterType() {
return this.type;
}
toFilterString() {
return `${this.type.name}:${this.sortDir.prefix}${quoteIfContainsSpace(this.matchStr)}`;
}
test(item, itemExp) {
const s = this.type.extractString(item, itemExp);
return s != null && s.toLowerCase().includes(this.matchStr);
}
compare(a, aExp, b, bExp) {
return this.sortDir.modifyComparison(this.type.extractString(a, aExp).localeCompare(this.type.extractString(b, bExp)));
}
}
class MajorIdFilter {
static TYPE = new Type('MajorId', 'Major Identifications', function(filterStr) {
return new MajorIdFilter(filterStr.split(','));
});
constructor(majorIds) {
this.majorIds = majorIds.map(s => s.toLowerCase()).filter(s => s);
}
getFilterType() {
return MajorIdFilter.TYPE;
}
toFilterString() {
return `${MajorIdFilter.TYPE.name}:${quoteIfContainsSpace(this.majorIds.join(','))}`;
}
test(item, itemExp) {
const itemIds = item.majorIds;
if (!itemIds) return false;
// quadratic-time subset check is bad, but items generally don't have many major IDs so it should be fine in practice
iter_ids:
for (let i = 0; i < this.majorIds.length; i++) {
for (let j = 0; j < itemIds.length; j++) {
if (itemIds[j].toLowerCase().contains(this.majorIds[i])) continue iter_ids;
}
return false;
}
return true;
}
compare(a, aExp, b, bExp) {
return 0;
}
}
function parseFilterString(filterStr) {
const n = filterStr.indexOf(':');
return n === -1 ? NameFilter.TYPE.parse(filterStr) : Type.getType(filterStr.substring(0, n)).parse(filterStr.substring(n + 1));
}
class ItemSearchState {
static parseSearchString(searchStr) {
// tokenize
const tokens = [];
let buf = [];
let inQuotes = false;
for (let i = 0; i < searchStr.length; i++) {
const chr = searchStr.charAt(i);
switch (chr) {
case '"':
inQuotes = !inQuotes;
break;
case ' ':
if (inQuotes) {
buf.push(' ');
} else {
const token = buf.join('').trim();
if (token) tokens.push(token);
buf.length = 0; // lol js
}
break;
default:
buf.push(chr);
break;
}
}
if (inQuotes) throw new Error("Mismatched quotes!");
// pick up a last token, if any
{
const token = buf.join('').trim();
if (token) tokens.push(token);
}
// parse filters
const searchState = new ItemSearchState();
for (let i = 0; i < tokens.length; i++) searchState.addFilter(parseFilterString(tokens[i]));
return searchState;
}
constructor() {
this.filterList = [];
this.filterTable = new Map();
}
addFilter(filter) {
const type = filter.getFilterType();
if (this.filterTable.has(type)) {
if (type === NameFilter.TYPE) { // special-case: adjoin multiple by-name filters
this.getFilter(NameFilter.TYPE).adjoin(filter);
return;
}
throw new Error(`Duplicate filters: $[type.name}`);
}
this.filterList.push(filter);
this.filterTable.set(type, filter);
}
getFilter(type) {
return this.filterTable.get(type);
}
toSearchString() {
return this.filterList.map(f => f.toFilterString()).join(' ');
}
test(item, itemExp) {
for (let i = 0; i < this.filterList.length; i++) {
if (!this.filterList[i].test(item, itemExp)) return false;
}
return true;
}
compare(a, aExp, b, bExp) {
for (let i = 0; i < this.filterList.length; i++) {
const result = this.filterList[i].compare(a, aExp, b, bExp);
if (result !== 0) return result;
}
// default to combat level, descending
return b.lvl - a.lvl;
}
/**
* @description A query into the item
* @module ItemNotFound
*/
class NameQuery {
constructor(string) {
this.queryString = string;
}
filter(item) {
if (item.remapID === undefined) {
return (item.displayName.includes(this.queryString));
}
return false;
}
compare(a, b) {
return a < b;
}
}

View file

@ -415,4 +415,4 @@ button.toggleOn:hover {
.tooltip:hover .tooltiptext {
visibility: visible;
}
}