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",
"version": "1.0.0",
"version": "2.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "botw-hundo-dupl",
"version": "1.0.0",
"version": "2.1.3",
"dependencies": {
"@babel/core": "^7.16.0",
"@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 { 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
*/
type ItemSearchMap = { [id: string]: string}; // id to search phrase
type ItemSearchMap = { [query: string]: string}; // query to id
type ItemContextFunctions = {
getItem: (id: string) => Item|undefined,
@ -90,26 +98,28 @@ export const ItemProvider: React.FC<PropsWithChildren> = ({children}) => {
const loadItemDataAsync = async ():Promise<[ItemIdMap, ItemSearchMap]> => {
const itemDataModule = await import("./all.items.yaml");
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 { 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 searchMap: ItemSearchMap = {};
// Register each type
registerItemCategoryByName(itemData, "weapon", ItemType.Weapon, idMap, searchMap);
registerItemCategoryByName(itemData, "bow", ItemType.Bow, idMap, searchMap);
registerItemCategoryByName(itemData, "arrow", ItemType.Arrow, idMap, searchMap);
registerItemCategoryByName(itemData, "shield", ItemType.Shield, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "weapon", ItemType.Weapon, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "bow", ItemType.Bow, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "arrow", ItemType.Arrow, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "shield", ItemType.Shield, idMap, searchMap);
// 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, "material", ItemType.Material, idMap, searchMap);
registerItemCategoryByName(itemData, "food", ItemType.Food, idMap, searchMap);
registerItemCategoryByName(itemData, "key", ItemType.Key, idMap, searchMap);
registerItemCategoryByName(itemData, "flag", ItemType.Flag, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "armor", undefined as any, idMap, searchMap); // eslint-disable-line @typescript-eslint/no-explicit-any
registerItemCategoryByName(itemData, aliasData, "material", ItemType.Material, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "food", ItemType.Food, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "key", ItemType.Key, idMap, searchMap);
registerItemCategoryByName(itemData, aliasData, "flag", ItemType.Flag, 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 ItemCategory = Exclude<ItemData[keyof ItemData], undefined>;
type ItemOption = Exclude<(ItemCategory["entries"][number]), string>[string];
type AliasData = {[id: string]: string[]};
const DefaultOption: ItemOption = {
stackable: true,
@ -124,14 +135,14 @@ const DefaultOption: ItemOption = {
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];
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 || {};
itemCategory.entries.forEach(entry=>{
let idAndSearch: string;
@ -158,11 +169,11 @@ const registerItemCategory = (itemCategory: ItemCategory, type: ItemType, outIdM
...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 image = getImageUrl(id, type, false);
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);
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 => {
@ -201,10 +216,10 @@ const getImageUrl = (id: string, type: ItemType, animated: boolean): string => {
const splitIdAndSearch = (idAndSearch: string): [string, string] => {
const i = idAndSearch.indexOf(":");
if(i<0){
return [idAndSearch, idAndSearch.toLowerCase()];
return [idAndSearch, normalizeQuery(idAndSearch)];
}else{
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();
}
// 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
let filteredResult = Object.keys(searchMap);
// 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
// ^ might want to make this togglable in the future
for(let i=0;i<parts.length;i++){
const searchKeyLower = parts[i].toLowerCase();
filteredResult = filteredResult.filter(id=>{
const searchPhrase = searchMap[id];
// searchPhrase must be nonnull because the initial array contains all keys
return searchPhrase.includes(searchKeyLower);
});
const searchKeyLower = parts[i];
filteredResult = filteredResult.filter(searchPhrase=>searchPhrase.includes(searchKeyLower));
if(filteredResult.length === 0){
// nothing found
@ -255,7 +267,7 @@ const searchItemInMap = (name: string, idMap: ItemIdMap, searchMap: ItemSearchMa
}
if(filteredResult.length === 1){
// exactly 1 found, can end
const foundId = filteredResult[0];
const foundId = searchMap[filteredResult[0]];
return idMap[foundId].createDefaultStack();
}
// 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
const resultStartCountMap: {[id: string]: number} = {};
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)=>{
// 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
return a.length-b.length;
});
const foundId = filteredResult[0];
const foundId = searchMap[filteredResult[0]];
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