implement search for aliases of items
This commit is contained in:
parent
eaa887f592
commit
628b565b94
4 changed files with 1791 additions and 32 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "botw-hundo-dupl",
|
"name": "botw-hundo-dupl",
|
||||||
"version": "1.0.0",
|
"version": "2.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "botw-hundo-dupl",
|
"name": "botw-hundo-dupl",
|
||||||
"version": "1.0.0",
|
"version": "2.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
||||||
|
|
|
@ -7,11 +7,19 @@ import { getTabFromType, Item, ItemIdMap, ItemStack, ItemTab, ItemType } from ".
|
||||||
import { searchLegacyItemNames } from "./legacy";
|
import { searchLegacyItemNames } from "./legacy";
|
||||||
import { createEquipmentStack } from "./ItemStack";
|
import { createEquipmentStack } from "./ItemStack";
|
||||||
|
|
||||||
|
function normalizeQuery(query: string) {
|
||||||
|
// to lower case
|
||||||
|
query = query.toLowerCase();
|
||||||
|
// katakana to hiragana
|
||||||
|
query = query.replace(/[\u30a1-\u30f6]/g, m=>String.fromCharCode(m.charCodeAt(0)-0x60));
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load items from items.yaml files and registers them in memory
|
* Load items from items.yaml files and registers them in memory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ItemSearchMap = { [id: string]: string}; // id to search phrase
|
type ItemSearchMap = { [query: string]: string}; // query to id
|
||||||
|
|
||||||
type ItemContextFunctions = {
|
type ItemContextFunctions = {
|
||||||
getItem: (id: string) => Item|undefined,
|
getItem: (id: string) => Item|undefined,
|
||||||
|
@ -90,26 +98,28 @@ export const ItemProvider: React.FC<PropsWithChildren> = ({children}) => {
|
||||||
const loadItemDataAsync = async ():Promise<[ItemIdMap, ItemSearchMap]> => {
|
const loadItemDataAsync = async ():Promise<[ItemIdMap, ItemSearchMap]> => {
|
||||||
const itemDataModule = await import("./all.items.yaml");
|
const itemDataModule = await import("./all.items.yaml");
|
||||||
const itemData = itemDataModule["default"];
|
const itemData = itemDataModule["default"];
|
||||||
return loadItemData(itemData);
|
const aliasDataModule = await import("./all.aliases.yaml");
|
||||||
|
const aliasData = aliasDataModule["default"];
|
||||||
|
return loadItemData(itemData, aliasData);
|
||||||
//const imgModule = await import("assets/img");
|
//const imgModule = await import("assets/img");
|
||||||
//const { getImage }= imgModule
|
//const { getImage }= imgModule
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadItemData = (itemData: (typeof import("*.items.yaml"))["default"]): [ItemIdMap, ItemSearchMap] => {
|
export const loadItemData = (itemData: (typeof import("*.items.yaml"))["default"], aliasData: AliasData): [ItemIdMap, ItemSearchMap] => {
|
||||||
const idMap: ItemIdMap = {};
|
const idMap: ItemIdMap = {};
|
||||||
const searchMap: ItemSearchMap = {};
|
const searchMap: ItemSearchMap = {};
|
||||||
// Register each type
|
// Register each type
|
||||||
registerItemCategoryByName(itemData, "weapon", ItemType.Weapon, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "weapon", ItemType.Weapon, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "bow", ItemType.Bow, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "bow", ItemType.Bow, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "arrow", ItemType.Arrow, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "arrow", ItemType.Arrow, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "shield", ItemType.Shield, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "shield", ItemType.Shield, idMap, searchMap);
|
||||||
// Pass in undefined for armor type, as it is resolved by option
|
// Pass in undefined for armor type, as it is resolved by option
|
||||||
registerItemCategoryByName(itemData, "armor", undefined as any, idMap, searchMap); // eslint-disable-line @typescript-eslint/no-explicit-any
|
registerItemCategoryByName(itemData, aliasData, "armor", undefined as any, idMap, searchMap); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
registerItemCategoryByName(itemData, "material", ItemType.Material, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "material", ItemType.Material, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "food", ItemType.Food, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "food", ItemType.Food, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "key", ItemType.Key, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "key", ItemType.Key, idMap, searchMap);
|
||||||
registerItemCategoryByName(itemData, "flag", ItemType.Flag, idMap, searchMap);
|
registerItemCategoryByName(itemData, aliasData, "flag", ItemType.Flag, idMap, searchMap);
|
||||||
|
|
||||||
return [idMap, searchMap];
|
return [idMap, searchMap];
|
||||||
};
|
};
|
||||||
|
@ -117,6 +127,7 @@ export const loadItemData = (itemData: (typeof import("*.items.yaml"))["default"
|
||||||
type ItemData = (typeof import("*.items.yaml"))["default"];
|
type ItemData = (typeof import("*.items.yaml"))["default"];
|
||||||
type ItemCategory = Exclude<ItemData[keyof ItemData], undefined>;
|
type ItemCategory = Exclude<ItemData[keyof ItemData], undefined>;
|
||||||
type ItemOption = Exclude<(ItemCategory["entries"][number]), string>[string];
|
type ItemOption = Exclude<(ItemCategory["entries"][number]), string>[string];
|
||||||
|
type AliasData = {[id: string]: string[]};
|
||||||
|
|
||||||
const DefaultOption: ItemOption = {
|
const DefaultOption: ItemOption = {
|
||||||
stackable: true,
|
stackable: true,
|
||||||
|
@ -124,14 +135,14 @@ const DefaultOption: ItemOption = {
|
||||||
repeatable: true
|
repeatable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerItemCategoryByName = (itemData: ItemData, category: keyof ItemData, type: ItemType, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
const registerItemCategoryByName = (itemData: ItemData, aliasData: AliasData, category: keyof ItemData, type: ItemType, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
||||||
const itemCategory = itemData[category];
|
const itemCategory = itemData[category];
|
||||||
if (itemCategory){
|
if (itemCategory){
|
||||||
registerItemCategory(itemCategory, type, outIdMap, outSearchMap);
|
registerItemCategory(itemCategory, type, aliasData, outIdMap, outSearchMap);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerItemCategory = (itemCategory: ItemCategory, type: ItemType, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
const registerItemCategory = (itemCategory: ItemCategory, type: ItemType, aliasData: AliasData, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
||||||
const globalOption = itemCategory.global || {};
|
const globalOption = itemCategory.global || {};
|
||||||
itemCategory.entries.forEach(entry=>{
|
itemCategory.entries.forEach(entry=>{
|
||||||
let idAndSearch: string;
|
let idAndSearch: string;
|
||||||
|
@ -158,11 +169,11 @@ const registerItemCategory = (itemCategory: ItemCategory, type: ItemType, outIdM
|
||||||
...option
|
...option
|
||||||
};
|
};
|
||||||
|
|
||||||
registerItem(idAndSearch, combinedOption, itemType, outIdMap, outSearchMap);
|
registerItem(idAndSearch, combinedOption, itemType, aliasData, outIdMap, outSearchMap);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerItem = (idAndSearch: string, option: ItemOption, type: ItemType, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
const registerItem = (idAndSearch: string, option: ItemOption, type: ItemType, aliasData: AliasData, outIdMap: ItemIdMap, outSearchMap: ItemSearchMap) => {
|
||||||
const [id, search] = splitIdAndSearch(idAndSearch);
|
const [id, search] = splitIdAndSearch(idAndSearch);
|
||||||
const image = getImageUrl(id, type, false);
|
const image = getImageUrl(id, type, false);
|
||||||
const animatedImage = option.animated ? getImageUrl(id, type, true) : undefined;
|
const animatedImage = option.animated ? getImageUrl(id, type, true) : undefined;
|
||||||
|
@ -185,7 +196,11 @@ const registerItem = (idAndSearch: string, option: ItemOption, type: ItemType, o
|
||||||
|
|
||||||
const item = new ItemImpl(id, type, option.repeatable ?? true, stackable ?? true, image, animatedImage, defaultStackFactory);
|
const item = new ItemImpl(id, type, option.repeatable ?? true, stackable ?? true, image, animatedImage, defaultStackFactory);
|
||||||
outIdMap[id] = item;
|
outIdMap[id] = item;
|
||||||
outSearchMap[id] = search;
|
|
||||||
|
outSearchMap[search] = id;
|
||||||
|
// aliasData[id] || console.warn(id);
|
||||||
|
// seems that aliasData may be undefined in npm test
|
||||||
|
aliasData?.[id]?.forEach(q => outSearchMap[normalizeQuery(q)] = id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageUrl = (id: string, type: ItemType, animated: boolean): string => {
|
const getImageUrl = (id: string, type: ItemType, animated: boolean): string => {
|
||||||
|
@ -201,10 +216,10 @@ const getImageUrl = (id: string, type: ItemType, animated: boolean): string => {
|
||||||
const splitIdAndSearch = (idAndSearch: string): [string, string] => {
|
const splitIdAndSearch = (idAndSearch: string): [string, string] => {
|
||||||
const i = idAndSearch.indexOf(":");
|
const i = idAndSearch.indexOf(":");
|
||||||
if(i<0){
|
if(i<0){
|
||||||
return [idAndSearch, idAndSearch.toLowerCase()];
|
return [idAndSearch, normalizeQuery(idAndSearch)];
|
||||||
}else{
|
}else{
|
||||||
const id = idAndSearch.substring(0, i);
|
const id = idAndSearch.substring(0, i);
|
||||||
return [id, (id+idAndSearch.substring(i+1)).toLowerCase()];
|
return [id, normalizeQuery(id+idAndSearch.substring(i+1))];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,19 +250,16 @@ const searchItemInMap = (name: string, idMap: ItemIdMap, searchMap: ItemSearchMa
|
||||||
return idItem.createDefaultStack();
|
return idItem.createDefaultStack();
|
||||||
}
|
}
|
||||||
// break name into dot separated search phrases
|
// break name into dot separated search phrases
|
||||||
const parts = name.split("*");
|
// prepend an * before + sign automatically
|
||||||
|
const parts = name.replace(/(\++)$/, "*$1").split("*").map(normalizeQuery);
|
||||||
// search is O(mn), where m is number of items and n is number of phrases
|
// search is O(mn), where m is number of items and n is number of phrases
|
||||||
let filteredResult = Object.keys(searchMap);
|
let filteredResult = Object.keys(searchMap);
|
||||||
// it's faster to filter by each phrase, since the sample sizes decreases every time
|
// it's faster to filter by each phrase, since the sample sizes decreases every time
|
||||||
// we can return the result when sample size is 1, even if later phrases might exclude that result
|
// we can return the result when sample size is 1, even if later phrases might exclude that result
|
||||||
// ^ might want to make this togglable in the future
|
// ^ might want to make this togglable in the future
|
||||||
for(let i=0;i<parts.length;i++){
|
for(let i=0;i<parts.length;i++){
|
||||||
const searchKeyLower = parts[i].toLowerCase();
|
const searchKeyLower = parts[i];
|
||||||
filteredResult = filteredResult.filter(id=>{
|
filteredResult = filteredResult.filter(searchPhrase=>searchPhrase.includes(searchKeyLower));
|
||||||
const searchPhrase = searchMap[id];
|
|
||||||
// searchPhrase must be nonnull because the initial array contains all keys
|
|
||||||
return searchPhrase.includes(searchKeyLower);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(filteredResult.length === 0){
|
if(filteredResult.length === 0){
|
||||||
// nothing found
|
// nothing found
|
||||||
|
@ -255,7 +267,7 @@ const searchItemInMap = (name: string, idMap: ItemIdMap, searchMap: ItemSearchMa
|
||||||
}
|
}
|
||||||
if(filteredResult.length === 1){
|
if(filteredResult.length === 1){
|
||||||
// exactly 1 found, can end
|
// exactly 1 found, can end
|
||||||
const foundId = filteredResult[0];
|
const foundId = searchMap[filteredResult[0]];
|
||||||
return idMap[foundId].createDefaultStack();
|
return idMap[foundId].createDefaultStack();
|
||||||
}
|
}
|
||||||
// continue filtering
|
// continue filtering
|
||||||
|
@ -266,7 +278,7 @@ const searchItemInMap = (name: string, idMap: ItemIdMap, searchMap: ItemSearchMa
|
||||||
// returning the first result here to make the search more generous
|
// returning the first result here to make the search more generous
|
||||||
const resultStartCountMap: {[id: string]: number} = {};
|
const resultStartCountMap: {[id: string]: number} = {};
|
||||||
filteredResult.forEach((resultId)=>{
|
filteredResult.forEach((resultId)=>{
|
||||||
resultStartCountMap[resultId] = parts.filter(p=>resultId.toLowerCase().startsWith(p)).length;
|
resultStartCountMap[resultId] = parts.filter(p=>resultId.startsWith(p)).length;
|
||||||
});
|
});
|
||||||
filteredResult.sort((a,b)=>{
|
filteredResult.sort((a,b)=>{
|
||||||
// first see if the result starts with any search key, and prioritize those with more matches
|
// first see if the result starts with any search key, and prioritize those with more matches
|
||||||
|
@ -278,7 +290,7 @@ const searchItemInMap = (name: string, idMap: ItemIdMap, searchMap: ItemSearchMa
|
||||||
// since the longer ones can always be found by adding more words
|
// since the longer ones can always be found by adding more words
|
||||||
return a.length-b.length;
|
return a.length-b.length;
|
||||||
});
|
});
|
||||||
const foundId = filteredResult[0];
|
const foundId = searchMap[filteredResult[0]];
|
||||||
return idMap[foundId].createDefaultStack();
|
return idMap[foundId].createDefaultStack();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
4
src/data/item/aliases.yaml.d.ts
vendored
Normal file
4
src/data/item/aliases.yaml.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.aliases.yaml' {
|
||||||
|
const aliases: {[id: string]: string[]};
|
||||||
|
export default aliases;
|
||||||
|
}
|
1743
src/data/item/all.aliases.yaml
Normal file
1743
src/data/item/all.aliases.yaml
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue