diff --git a/src/handbook/data/README.md b/src/handbook/data/README.md index 5935b04b9..90d1ffe54 100644 --- a/src/handbook/data/README.md +++ b/src/handbook/data/README.md @@ -5,3 +5,26 @@ Use Grasscutter's dumpers to generate the data to put here. - `avatars.json` - `commands.json` - `items.csv` + +# Item Icon Notes +- Artifacts: `https://bbs.hoyolab.com/hoyowiki/picture/reliquary/(name)/(piece)_icon.png` + - Alternate source: `https://api.ambr.top/assets/UI/reliquary/UI_RelicIcon_(set)_(piece).png` + - `xxxx4` - `flower_of_life` + - `xxxx5` - `sands_of_eon` + - `xxxx3` - `circlet_of_logos`/`plume_of_death` + - Use `circlet_of_logos` with a complete set + - Use `plume_of_death` with part of a set. + - `xxxx2` - `plume_of_death` + - `xxxx1` - `goblet_of_eonothem` +- Miscellaneous Items: `https://bbs.hoyolab.com/hoyowiki/picture/object/(name)_icon.png` + - Includes: materials, quest items, food, etc. + - Alternate source: `https://api.ambr.top/assets/UI/UI_ItemIcon_(id).png` +- Avatars/Avatar Items: `https://bbs.hoyolab.com/hoyowiki/picture/character/(name)_icon.png` + - Avatar Items are between ranges `1001` and `1099`. +- Weapons: `https://api.ambr.top/assets/UI/UI_EquipIcon_(type)_(name).png` +- Furniture: `https://api.ambr.top/assets/UI/furniture/UI_Homeworld_(location)_(name).png` +- Monsters: `https://api.ambr.top/assets/UI/monster/UI_MonsterIcon_(type)_(variant).png` + +# Credits +- [`...List.json` files](https://raw.githubusercontent.com/Dituon/grasscutter-command-helper/main/data/en-US) - Grasscutter Command Helper +- [Internal Asset API](https://ambr.top) - Project Amber diff --git a/src/handbook/src/backend/data.ts b/src/handbook/src/backend/data.ts index 7ebf0d483..f1c396ad4 100644 --- a/src/handbook/src/backend/data.ts +++ b/src/handbook/src/backend/data.ts @@ -9,15 +9,16 @@ import { inRange } from "@app/utils"; type AvatarDump = { [key: number]: Avatar }; type CommandDump = { [key: string]: Command }; -type TaggedItems = { [key: number]: Item[] } +type TaggedItems = { [key: number]: Item[] }; +type ItemIcons = { [key: number]: string }; -/* - * Notes on artifacts: - * TODO: Figure out what suffix is for which artifact type. +/** + * @see {@file src/handbook/data/README.md} */ export const sortedItems: TaggedItems = { [ItemCategory.Constellation]: [], // Range: 1102 - 11xx + [ItemCategory.Avatar]: [], // Range: 1002 - 10xx [ItemCategory.Weapon]: [], [ItemCategory.Artifact]: [], [ItemCategory.Furniture]: [], @@ -32,16 +33,28 @@ export const sortedItems: TaggedItems = { export function setup(): void { getItems().forEach(item => { switch (item.type) { - case ItemType.Weapon: sortedItems[ItemCategory.Weapon].push(item); break; - case ItemType.Material: sortedItems[ItemCategory.Material].push(item); break; - case ItemType.Furniture: sortedItems[ItemCategory.Furniture].push(item); break; - case ItemType.Reliquary: sortedItems[ItemCategory.Artifact].push(item); break; + case ItemType.Weapon: + sortedItems[ItemCategory.Weapon].push(item); + break; + case ItemType.Material: + sortedItems[ItemCategory.Material].push(item); + break; + case ItemType.Furniture: + sortedItems[ItemCategory.Furniture].push(item); + break; + case ItemType.Reliquary: + sortedItems[ItemCategory.Artifact].push(item); + break; } // Sort constellations. if (inRange(item.id, 1102, 1199)) { sortedItems[ItemCategory.Constellation].push(item); } + // Sort avatars. + if (inRange(item.id, 1002, 1099)) { + sortedItems[ItemCategory.Avatar].push(item); + } }); } @@ -88,14 +101,15 @@ export function listAvatars(): Avatar[] { * Fetches and casts all items in the file. */ export function getItems(): Item[] { - return items.map((item) => { - const values = Object.values(item) as [string, string, string, string]; + return items.map((entry) => { + const values = Object.values(entry) as string[]; const id = parseInt(values[0]); return { id, name: values[1], - type: values[2] as ItemType, - quality: values[3] as Quality + type: values[3] as ItemType, + quality: values[2] as Quality, + icon: values[4] }; }); } diff --git a/src/handbook/src/backend/types.ts b/src/handbook/src/backend/types.ts index 71251a9b6..f44c4ddb5 100644 --- a/src/handbook/src/backend/types.ts +++ b/src/handbook/src/backend/types.ts @@ -19,6 +19,7 @@ export type Item = { name: string; quality: Quality; type: ItemType; + icon: string; }; export enum Target { @@ -49,6 +50,7 @@ export enum ItemType { export enum ItemCategory { Constellation, + Avatar, Weapon, Artifact, Furniture, diff --git a/src/handbook/src/css/widgets/Item.scss b/src/handbook/src/css/widgets/Item.scss index 2e6611575..ba8521e95 100644 --- a/src/handbook/src/css/widgets/Item.scss +++ b/src/handbook/src/css/widgets/Item.scss @@ -1,8 +1,17 @@ .Item { display: flex; - width: 100%; - height: 100%; + width: 64px; + height: 64px; + + overflow: hidden; + justify-content: center; +} + +.Item_Background { + display: flex; + align-items: center; + max-width: 64px; max-height: 64px; @@ -11,8 +20,17 @@ } .Item_Icon { + max-width: 64px; + max-height: 64px; + object-fit: scale-down; +} + +.Item_Label { width: 64px; - height: 64px; + max-height: 64px; + text-align: center; + font-size: 12px; + color: var(--text-primary-color); } .Item_Info { diff --git a/src/handbook/src/ui/pages/ItemsPage.tsx b/src/handbook/src/ui/pages/ItemsPage.tsx index 49ce19205..d28269588 100644 --- a/src/handbook/src/ui/pages/ItemsPage.tsx +++ b/src/handbook/src/ui/pages/ItemsPage.tsx @@ -67,6 +67,21 @@ class ItemsPage extends React.Component<{}, IState> { this.setState({ search: event.target.value }); } + /** + * Should the item be showed? + * + * @param item The item. + * @private + */ + private showItem(item: ItemType): boolean { + // Check if the item has an icon. + if (item.icon.length == 0) return false; + // Check if the item is a TCG card. + if (item.icon.includes("Gcg")) return false; + + return item.id > 0; + } + render() { const items = this.getItems(); @@ -87,11 +102,9 @@ class ItemsPage extends React.Component<{}, IState> { { items.length > 0 ? ( this.showItem(item))} itemHeight={64} itemsPerRow={20} gap={5} itemGap={5} - render={(item) => ( - - )} + render={(item) => } /> ) : undefined } diff --git a/src/handbook/src/ui/widgets/Character.tsx b/src/handbook/src/ui/widgets/Character.tsx index 6682e0530..ee1ae6265 100644 --- a/src/handbook/src/ui/widgets/Character.tsx +++ b/src/handbook/src/ui/widgets/Character.tsx @@ -1,34 +1,16 @@ import React from "react"; import type { Avatar } from "@backend/types"; -import { colorFor } from "@app/utils"; +import { colorFor, formatAvatarName } from "@app/utils"; import "@css/widgets/Character.scss"; // Image base URL: https://paimon.moe/images/characters/(name).png -/** - * Formats a character's name to fit with the reference name. - * Example: Hu Tao -> hu_tao - * - * @param name The character's name. - * @param id The character's ID. - */ -function formatName(name: string, id: number): string { - // Check if a different name is used for the character. - if (refSwitch[id]) name = refSwitch[id]; - return name.toLowerCase().replace(" ", "_"); -} - const ignored = [ 10000001 // Kate ]; -const refSwitch: { [key: number]: string } = { - 10000005: "traveler_anemo", - 10000007: "traveler_geo" -}; - const nameSwitch: { [key: number]: string } = { 10000005: "Lumine", 10000007: "Aether" @@ -55,7 +37,7 @@ class Character extends React.PureComponent { {name}
diff --git a/src/handbook/src/ui/widgets/Item.tsx b/src/handbook/src/ui/widgets/Item.tsx index 28b1244d5..155e65ed5 100644 --- a/src/handbook/src/ui/widgets/Item.tsx +++ b/src/handbook/src/ui/widgets/Item.tsx @@ -1,6 +1,7 @@ import React from "react"; import type { Item as ItemData } from "@backend/types"; +import { itemIcon } from "@app/utils"; import "@css/widgets/Item.scss"; @@ -10,6 +11,7 @@ interface IProps { interface IState { popout: boolean; + icon: boolean; } class Item extends React.Component { @@ -17,26 +19,34 @@ class Item extends React.Component { super(props); this.state = { - popout: false + popout: false, + icon: true }; } /** - * Fetches the icon for the item. + * Replaces the icon with the item's name. * @private */ - private getIcon(): string { - return `https://paimon.moe/images/items/teachings_of_freedom.png`; + private replaceIcon(): void { + this.setState({ icon: false }); } render() { return (
- {this.props.data.name} +
+ { + this.state.icon ? ( + {this.props.data.name} + ) :

{this.props.data.name}

+ } +
diff --git a/src/handbook/src/utils.ts b/src/handbook/src/utils.ts index d68e9243a..2c9ccc331 100644 --- a/src/handbook/src/utils.ts +++ b/src/handbook/src/utils.ts @@ -1,4 +1,5 @@ -import { Quality } from "@backend/types"; +import { ItemType, Quality } from "@backend/types"; +import type { Item } from "@backend/types"; /** * Fetches the name of the CSS variable for the quality. @@ -32,3 +33,40 @@ export function colorFor(quality: Quality): string { export function inRange(value: number, min: number, max: number): boolean { return value >= min && value <= max; } + +/** + * Gets the path to the icon for an item. + * Uses the Project Amber API to get the icon. + * + * @param item The item to get the icon for. + */ +export function itemIcon(item: Item): string { + // Check if the item matches a special case. + if (inRange(item.id, 1001, 1099)) { + return `https://paimon.moe/images/characters/${formatAvatarName(item.name, item.id)}.png`; + } + + switch (item.type) { + default: return `https://api.ambr.top/assets/UI/UI_${item.icon}.png`; + case ItemType.Furniture: return `https://api.ambr.top/assets/UI/furniture/UI_${item.icon}.png`; + case ItemType.Reliquary: return `https://api.ambr.top/assets/UI/reliquary/UI_${item.icon}.png`; + } +} + +/** + * Formats a character's name to fit with the reference name. + * Example: Hu Tao -> hu_tao + * + * @param name The character's name. + * @param id The character's ID. + */ +export function formatAvatarName(name: string, id: number): string { + // Check if a different name is used for the character. + if (refSwitch[id]) name = refSwitch[id]; + return name.toLowerCase().replace(" ", "_"); +} + +const refSwitch: { [key: number]: string } = { + 10000005: "traveler_anemo", + 10000007: "traveler_geo" +};