1
0
Fork 0

implement search for aliases of items

This commit is contained in:
sup39 2022-09-29 05:13:07 +09:00
parent eaa887f592
commit 628b565b94
4 changed files with 1791 additions and 32 deletions

4
package-lock.json generated
View file

@ -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",

View file

@ -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
View file

@ -0,0 +1,4 @@
declare module '*.aliases.yaml' {
const aliases: {[id: string]: string[]};
export default aliases;
}

File diff suppressed because it is too large Load diff