wynnbuilder-forked-for-changes/query.js

624 lines
27 KiB
JavaScript
Raw Normal View History

// 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
2021-01-23 16:38:13 +00:00
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':
2021-01-25 22:31:00 +00:00
for (let i = 0; i < TypeFilter.ARMOUR_TYPES.length; i++) allowedTypes.add(TypeFilter.ARMOUR_TYPES[i]);
break;
case 'weapon':
2021-01-25 22:31:00 +00:00
for (let i = 0; i < TypeFilter.WEAPON_TYPES.length; i++) allowedTypes.add(TypeFilter.WEAPON_TYPES[i]);
break;
case 'accessory':
case 'bauble':
2021-01-25 22:31:00 +00:00
for (let i = 0; i < TypeFilter.ACCESSORY_TYPES.length; i++) allowedTypes.add(TypeFilter.ACCESSORY_TYPES[i]);
break;
default:
2021-01-25 22:31:00 +00:00
allowedTypes.add(token.toLowerCase());
break;
}
}
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);
2021-01-25 22:58:31 +00:00
this.statExtractor = statExtractor || aliases;
}
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(',')}`;
2021-01-23 16:38:13 +00:00
}
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;
2021-01-23 16:38:13 +00:00
}
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);
2021-01-25 22:58:31 +00:00
this.stringExtractor = stringExtractor || aliases;
}
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;
2021-01-23 16:38:13 +00:00
}
getFilterType() {
return this.type;
2021-01-23 16:38:13 +00:00
}
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;
}
2021-01-23 16:38:13 +00:00
}