Fix item icons to be more accurate

Project Amber is now the primary icon source!
This commit is contained in:
KingRainbow44 2023-04-08 21:58:46 -04:00
parent 2a5abc1dcb
commit 6c2f66fa2d
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
8 changed files with 149 additions and 49 deletions

View File

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

View File

@ -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]
};
});
}

View File

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

View File

@ -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 {

View File

@ -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 ? (
<VirtualizedGrid
list={items} itemHeight={64}
list={items.filter(item => this.showItem(item))} itemHeight={64}
itemsPerRow={20} gap={5} itemGap={5}
render={(item) => (
<Item key={item.id} data={item} />
)}
render={(item) => <Item key={item.id} data={item} />}
/>
) : undefined
}

View File

@ -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<IProps> {
<img
className={"Character_Icon"}
alt={name}
src={`https://paimon.moe/images/characters/${formatName(name, id)}.png`}
src={`https://paimon.moe/images/characters/${formatAvatarName(name, id)}.png`}
/>
<div className={"Character_Label"}>

View File

@ -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<IProps, IState> {
@ -17,26 +19,34 @@ class Item extends React.Component<IProps, IState> {
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 (
<div className={"Item"}>
<img
className={"Item_Icon"}
alt={this.props.data.name}
src={this.getIcon()}
/>
<div className={"Item_Background"}>
{
this.state.icon ? (
<img
className={"Item_Icon"}
alt={this.props.data.name}
src={itemIcon(this.props.data)}
onError={this.replaceIcon.bind(this)}
/>
) : <p className={"Item_Label"}>{this.props.data.name}</p>
}
</div>
<div className={"Item_Info"}>

View File

@ -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"
};