mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-25 17:02:57 +08:00
Fix item icons to be more accurate
Project Amber is now the primary icon source!
This commit is contained in:
parent
2a5abc1dcb
commit
6c2f66fa2d
@ -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
|
||||
|
@ -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]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"}>
|
||||
|
@ -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"}>
|
||||
|
||||
|
@ -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"
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user