mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-18 09:54:59 +01:00
Fix item icons to be more accurate
Project Amber is now the primary icon source!
This commit is contained in:
@@ -5,3 +5,26 @@ Use Grasscutter's dumpers to generate the data to put here.
|
|||||||
- `avatars.json`
|
- `avatars.json`
|
||||||
- `commands.json`
|
- `commands.json`
|
||||||
- `items.csv`
|
- `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 AvatarDump = { [key: number]: Avatar };
|
||||||
type CommandDump = { [key: string]: Command };
|
type CommandDump = { [key: string]: Command };
|
||||||
type TaggedItems = { [key: number]: Item[] }
|
type TaggedItems = { [key: number]: Item[] };
|
||||||
|
type ItemIcons = { [key: number]: string };
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Notes on artifacts:
|
* @see {@file src/handbook/data/README.md}
|
||||||
* TODO: Figure out what suffix is for which artifact type.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const sortedItems: TaggedItems = {
|
export const sortedItems: TaggedItems = {
|
||||||
[ItemCategory.Constellation]: [], // Range: 1102 - 11xx
|
[ItemCategory.Constellation]: [], // Range: 1102 - 11xx
|
||||||
|
[ItemCategory.Avatar]: [], // Range: 1002 - 10xx
|
||||||
[ItemCategory.Weapon]: [],
|
[ItemCategory.Weapon]: [],
|
||||||
[ItemCategory.Artifact]: [],
|
[ItemCategory.Artifact]: [],
|
||||||
[ItemCategory.Furniture]: [],
|
[ItemCategory.Furniture]: [],
|
||||||
@@ -32,16 +33,28 @@ export const sortedItems: TaggedItems = {
|
|||||||
export function setup(): void {
|
export function setup(): void {
|
||||||
getItems().forEach(item => {
|
getItems().forEach(item => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case ItemType.Weapon: sortedItems[ItemCategory.Weapon].push(item); break;
|
case ItemType.Weapon:
|
||||||
case ItemType.Material: sortedItems[ItemCategory.Material].push(item); break;
|
sortedItems[ItemCategory.Weapon].push(item);
|
||||||
case ItemType.Furniture: sortedItems[ItemCategory.Furniture].push(item); break;
|
break;
|
||||||
case ItemType.Reliquary: sortedItems[ItemCategory.Artifact].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.
|
// Sort constellations.
|
||||||
if (inRange(item.id, 1102, 1199)) {
|
if (inRange(item.id, 1102, 1199)) {
|
||||||
sortedItems[ItemCategory.Constellation].push(item);
|
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.
|
* Fetches and casts all items in the file.
|
||||||
*/
|
*/
|
||||||
export function getItems(): Item[] {
|
export function getItems(): Item[] {
|
||||||
return items.map((item) => {
|
return items.map((entry) => {
|
||||||
const values = Object.values(item) as [string, string, string, string];
|
const values = Object.values(entry) as string[];
|
||||||
const id = parseInt(values[0]);
|
const id = parseInt(values[0]);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: values[1],
|
name: values[1],
|
||||||
type: values[2] as ItemType,
|
type: values[3] as ItemType,
|
||||||
quality: values[3] as Quality
|
quality: values[2] as Quality,
|
||||||
|
icon: values[4]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type Item = {
|
|||||||
name: string;
|
name: string;
|
||||||
quality: Quality;
|
quality: Quality;
|
||||||
type: ItemType;
|
type: ItemType;
|
||||||
|
icon: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum Target {
|
export enum Target {
|
||||||
@@ -49,6 +50,7 @@ export enum ItemType {
|
|||||||
|
|
||||||
export enum ItemCategory {
|
export enum ItemCategory {
|
||||||
Constellation,
|
Constellation,
|
||||||
|
Avatar,
|
||||||
Weapon,
|
Weapon,
|
||||||
Artifact,
|
Artifact,
|
||||||
Furniture,
|
Furniture,
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
.Item {
|
.Item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
width: 100%;
|
width: 64px;
|
||||||
height: 100%;
|
height: 64px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Item_Background {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
max-width: 64px;
|
max-width: 64px;
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
|
|
||||||
@@ -11,8 +20,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Item_Icon {
|
.Item_Icon {
|
||||||
|
max-width: 64px;
|
||||||
|
max-height: 64px;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Item_Label {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
max-height: 64px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Item_Info {
|
.Item_Info {
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ class ItemsPage extends React.Component<{}, IState> {
|
|||||||
this.setState({ search: event.target.value });
|
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() {
|
render() {
|
||||||
const items = this.getItems();
|
const items = this.getItems();
|
||||||
|
|
||||||
@@ -87,11 +102,9 @@ class ItemsPage extends React.Component<{}, IState> {
|
|||||||
{
|
{
|
||||||
items.length > 0 ? (
|
items.length > 0 ? (
|
||||||
<VirtualizedGrid
|
<VirtualizedGrid
|
||||||
list={items} itemHeight={64}
|
list={items.filter(item => this.showItem(item))} itemHeight={64}
|
||||||
itemsPerRow={20} gap={5} itemGap={5}
|
itemsPerRow={20} gap={5} itemGap={5}
|
||||||
render={(item) => (
|
render={(item) => <Item key={item.id} data={item} />}
|
||||||
<Item key={item.id} data={item} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Avatar } from "@backend/types";
|
import type { Avatar } from "@backend/types";
|
||||||
import { colorFor } from "@app/utils";
|
import { colorFor, formatAvatarName } from "@app/utils";
|
||||||
|
|
||||||
import "@css/widgets/Character.scss";
|
import "@css/widgets/Character.scss";
|
||||||
|
|
||||||
// Image base URL: https://paimon.moe/images/characters/(name).png
|
// 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 = [
|
const ignored = [
|
||||||
10000001 // Kate
|
10000001 // Kate
|
||||||
];
|
];
|
||||||
|
|
||||||
const refSwitch: { [key: number]: string } = {
|
|
||||||
10000005: "traveler_anemo",
|
|
||||||
10000007: "traveler_geo"
|
|
||||||
};
|
|
||||||
|
|
||||||
const nameSwitch: { [key: number]: string } = {
|
const nameSwitch: { [key: number]: string } = {
|
||||||
10000005: "Lumine",
|
10000005: "Lumine",
|
||||||
10000007: "Aether"
|
10000007: "Aether"
|
||||||
@@ -55,7 +37,7 @@ class Character extends React.PureComponent<IProps> {
|
|||||||
<img
|
<img
|
||||||
className={"Character_Icon"}
|
className={"Character_Icon"}
|
||||||
alt={name}
|
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"}>
|
<div className={"Character_Label"}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Item as ItemData } from "@backend/types";
|
import type { Item as ItemData } from "@backend/types";
|
||||||
|
import { itemIcon } from "@app/utils";
|
||||||
|
|
||||||
import "@css/widgets/Item.scss";
|
import "@css/widgets/Item.scss";
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
popout: boolean;
|
popout: boolean;
|
||||||
|
icon: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Item extends React.Component<IProps, IState> {
|
class Item extends React.Component<IProps, IState> {
|
||||||
@@ -17,26 +19,34 @@ class Item extends React.Component<IProps, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
popout: false
|
popout: false,
|
||||||
|
icon: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the icon for the item.
|
* Replaces the icon with the item's name.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private getIcon(): string {
|
private replaceIcon(): void {
|
||||||
return `https://paimon.moe/images/items/teachings_of_freedom.png`;
|
this.setState({ icon: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={"Item"}>
|
<div className={"Item"}>
|
||||||
<img
|
<div className={"Item_Background"}>
|
||||||
className={"Item_Icon"}
|
{
|
||||||
alt={this.props.data.name}
|
this.state.icon ? (
|
||||||
src={this.getIcon()}
|
<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"}>
|
<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.
|
* 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 {
|
export function inRange(value: number, min: number, max: number): boolean {
|
||||||
return value >= min && value <= max;
|
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"
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user