mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-23 07:10:46 +00:00
Implement the entities page
This commit is contained in:
parent
127d45f21f
commit
16875e85ac
@ -3,6 +3,7 @@ Use Grasscutter's dumpers to generate the data to put here.
|
||||
|
||||
## Files Required
|
||||
- `commands.json`
|
||||
- `entities.csv`
|
||||
- `avatars.csv`
|
||||
- `scenes.csv`
|
||||
- `items.csv`
|
||||
|
@ -1,10 +1,11 @@
|
||||
import commands from "@data/commands.json";
|
||||
import entities from "@data/entities.csv";
|
||||
import avatars from "@data/avatars.csv";
|
||||
import scenes from "@data/scenes.csv";
|
||||
import items from "@data/items.csv";
|
||||
|
||||
import { Quality, ItemType, ItemCategory, SceneType } from "@backend/types";
|
||||
import type { Command, Avatar, Item, Scene } from "@backend/types";
|
||||
import type { Command, Avatar, Item, Scene, Entity } from "@backend/types";
|
||||
|
||||
import { inRange } from "@app/utils";
|
||||
|
||||
@ -72,6 +73,21 @@ export function listCommands(): Command[] {
|
||||
return Object.values(getCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and casts all entities in the file.
|
||||
*/
|
||||
export function getEntities(): Entity[] {
|
||||
return entities.map((entry) => {
|
||||
const values = Object.values(entry) as string[];
|
||||
const id = parseInt(values[0]);
|
||||
return {
|
||||
id,
|
||||
name: values[1],
|
||||
internal: values[2]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and casts all avatars in the file.
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
export type Page = "Home" | "Commands" | "Avatars" | "Items"
|
||||
| "Scenes";
|
||||
| "Entities" | "Scenes";
|
||||
export type Days = "Sunday" | "Monday" | "Tuesday"
|
||||
| "Wednesday" | "Thursday" | "Friday" | "Saturday";
|
||||
|
||||
@ -31,6 +31,12 @@ export type Item = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export type Entity = {
|
||||
id: number;
|
||||
name: string;
|
||||
internal: string;
|
||||
};
|
||||
|
||||
// Exported from Project Amber.
|
||||
export type ItemInfo = {
|
||||
response: number | 200 | 404;
|
||||
@ -51,6 +57,23 @@ export type ItemInfo = {
|
||||
};
|
||||
};
|
||||
|
||||
// Exported from Project Amber.
|
||||
export type EntityInfo = {
|
||||
response: number | 200 | 404;
|
||||
data: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
route: string;
|
||||
title: string;
|
||||
specialName: string;
|
||||
description: string;
|
||||
entries: any[];
|
||||
tips: null;
|
||||
};
|
||||
};
|
||||
|
||||
export enum Target {
|
||||
None = "NONE",
|
||||
Offline = "OFFLINE",
|
||||
|
93
src/handbook/src/css/pages/EntitiesPage.scss
Normal file
93
src/handbook/src/css/pages/EntitiesPage.scss
Normal file
@ -0,0 +1,93 @@
|
||||
.EntitiesPage {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: var(--background-color);
|
||||
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.EntitiesPage_Content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.EntitiesPage_Header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 30px;
|
||||
align-content: center;
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.EntitiesPage_Title {
|
||||
max-width: 230px;
|
||||
max-height: 60px;
|
||||
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.EntitiesPage_Search {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 465px;
|
||||
max-height: 60px;
|
||||
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.EntitiesPage_Input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
color: var(--text-primary-color);
|
||||
font-size: 20px;
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
|
||||
&:focus, &:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.EntitiesPage_Input::placeholder {
|
||||
color: var(--text-secondary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.EntitiesPage_List {
|
||||
display: grid;
|
||||
gap: 15px 15px;
|
||||
|
||||
grid-template-columns: repeat(15, 100px);
|
||||
|
||||
margin-bottom: 28px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.EntitiesPage_Card {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
min-height: 300px;
|
||||
max-height: 700px;
|
||||
|
||||
align-self: center;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
.Item {
|
||||
.MiniCard {
|
||||
display: flex;
|
||||
|
||||
width: 64px;
|
||||
@ -8,7 +8,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Item_Background {
|
||||
.MiniCard_Background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -19,13 +19,14 @@
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.Item_Icon {
|
||||
.MiniCard_Icon {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
object-fit: scale-down;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.Item_Label {
|
||||
.MiniCard_Label {
|
||||
width: 64px;
|
||||
max-height: 64px;
|
||||
text-align: center;
|
||||
@ -33,7 +34,7 @@
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.Item_Info {
|
||||
.MiniCard_Info {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
}
|
151
src/handbook/src/ui/pages/EntitiesPage.tsx
Normal file
151
src/handbook/src/ui/pages/EntitiesPage.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
|
||||
import MiniCard from "@widgets/MiniCard";
|
||||
import VirtualizedGrid from "@components/VirtualizedGrid";
|
||||
|
||||
import { Entity, ItemCategory } from "@backend/types";
|
||||
import type { Entity as EntityType, EntityInfo } from "@backend/types";
|
||||
import { getEntities } from "@backend/data";
|
||||
import { entityIcon, fetchEntityData } from "@app/utils";
|
||||
|
||||
import "@css/pages/EntitiesPage.scss";
|
||||
import EntityCard from "@widgets/EntityCard";
|
||||
|
||||
interface IState {
|
||||
filters: ItemCategory[];
|
||||
search: string;
|
||||
|
||||
selected: EntityType | null;
|
||||
selectedInfo: EntityInfo | null;
|
||||
}
|
||||
|
||||
class EntitiesPage extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filters: [],
|
||||
search: "",
|
||||
|
||||
selected: null,
|
||||
selectedInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the entity be shown?
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @private
|
||||
*/
|
||||
private showEntity(entity: Entity): boolean {
|
||||
// Check if the entity's name starts with N/A.
|
||||
if (entity.name.includes("[N/A]")) return false;
|
||||
|
||||
return entity.id > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the items to render.
|
||||
* @private
|
||||
*/
|
||||
private getEntities(): EntityType[] {
|
||||
let entities: EntityType[] = [];
|
||||
|
||||
// Add items based on filters.
|
||||
const filters = this.state.filters;
|
||||
if (filters.length == 0) {
|
||||
entities = getEntities();
|
||||
} else {
|
||||
for (const filter of filters) {
|
||||
// Remove duplicate items.
|
||||
entities = entities.filter((item, index) => {
|
||||
return entities.indexOf(item) == index;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out items that don't match the search.
|
||||
const search = this.state.search.toLowerCase();
|
||||
if (search != "") {
|
||||
entities = entities.filter((item) => {
|
||||
return item.name.toLowerCase().includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the search input changes.
|
||||
*
|
||||
* @param event The event.
|
||||
* @private
|
||||
*/
|
||||
private onChange(event: ChangeEvent<HTMLInputElement>): void {
|
||||
this.setState({ search: event.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected entity.
|
||||
*
|
||||
* @param entity The entity.
|
||||
* @private
|
||||
*/
|
||||
private async setSelectedItem(entity: EntityType): Promise<void> {
|
||||
let data: EntityInfo | null = null; try {
|
||||
data = await fetchEntityData(entity);
|
||||
} catch { }
|
||||
|
||||
this.setState({
|
||||
selected: entity,
|
||||
selectedInfo: data
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const entities = this.getEntities();
|
||||
|
||||
return (
|
||||
<div className={"EntitiesPage"}>
|
||||
<div className={"EntitiesPage_Content"}>
|
||||
<div className={"EntitiesPage_Header"}>
|
||||
<h1 className={"EntitiesPage_Title"}>Monsters</h1>
|
||||
|
||||
<div className={"EntitiesPage_Search"}>
|
||||
<input
|
||||
type={"text"}
|
||||
className={"EntitiesPage_Input"}
|
||||
placeholder={"Search..."}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entities.length > 0 ? (
|
||||
<VirtualizedGrid
|
||||
list={entities.filter(entity => this.showEntity(entity))}
|
||||
itemHeight={64}
|
||||
itemsPerRow={18}
|
||||
gap={5}
|
||||
itemGap={5}
|
||||
render={(entity) => <MiniCard
|
||||
key={entity.id} data={entity} icon={entityIcon(entity)}
|
||||
onClick={() => this.setSelectedItem(entity)}
|
||||
/>}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
<div className={"EntitiesPage_Card"}>
|
||||
<EntityCard
|
||||
entity={this.state.selected}
|
||||
info={this.state.selectedInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EntitiesPage;
|
@ -21,7 +21,7 @@ class HomePage extends React.Component<any, any> {
|
||||
<HomeButton name={"Commands"} anchor={"Commands"} />
|
||||
<HomeButton name={"Characters"} anchor={"Avatars"} />
|
||||
<HomeButton name={"Items"} anchor={"Items"} />
|
||||
<HomeButton name={"Entities"} anchor={"Home"} />
|
||||
<HomeButton name={"Entities"} anchor={"Entities"} />
|
||||
<HomeButton name={"Scenes"} anchor={"Scenes"} />
|
||||
</div>
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
|
||||
import Item from "@widgets/Item";
|
||||
import MiniCard from "@widgets/MiniCard";
|
||||
import ItemCard from "@widgets/ItemCard";
|
||||
import VirtualizedGrid from "@components/VirtualizedGrid";
|
||||
|
||||
import { ItemCategory } from "@backend/types";
|
||||
import type { Item as ItemType, ItemInfo } from "@backend/types";
|
||||
import { getItems, sortedItems } from "@backend/data";
|
||||
import { fetchItemData } from "@app/utils";
|
||||
import { fetchItemData, itemIcon } from "@app/utils";
|
||||
|
||||
import "@css/pages/ItemsPage.scss";
|
||||
|
||||
@ -133,8 +133,8 @@ class ItemsPage extends React.Component<{}, IState> {
|
||||
itemsPerRow={18}
|
||||
gap={5}
|
||||
itemGap={5}
|
||||
render={(item) => <Item
|
||||
key={item.id} data={item}
|
||||
render={(item) => <MiniCard
|
||||
key={item.id} data={item} icon={itemIcon(item)}
|
||||
onClick={() => this.setSelectedItem(item)}
|
||||
/>}
|
||||
/>
|
||||
|
@ -4,6 +4,7 @@ import HomePage from "@pages/HomePage";
|
||||
import CommandsPage from "@pages/CommandsPage";
|
||||
import AvatarsPage from "@pages/AvatarsPage";
|
||||
import ItemsPage from "@pages/ItemsPage";
|
||||
import EntitiesPage from "@pages/EntitiesPage";
|
||||
import ScenesPage from "@pages/ScenesPage";
|
||||
|
||||
import type { Page } from "@backend/types";
|
||||
@ -58,6 +59,8 @@ class Content extends React.Component<IProps, IState> {
|
||||
return <AvatarsPage />;
|
||||
case "Items":
|
||||
return <ItemsPage />;
|
||||
case "Entities":
|
||||
return <EntitiesPage />;
|
||||
case "Scenes":
|
||||
return <ScenesPage />;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class SideBar extends React.Component<{}, IState> {
|
||||
<SideBarButton name={"Commands"} anchor={"Commands"} />
|
||||
<SideBarButton name={"Characters"} anchor={"Avatars"} />
|
||||
<SideBarButton name={"Items"} anchor={"Items"} />
|
||||
<SideBarButton name={"Entities"} anchor={"Home"} />
|
||||
<SideBarButton name={"Entities"} anchor={"Entities"} />
|
||||
<SideBarButton name={"Scenes"} anchor={"Scenes"} />
|
||||
<SideBarButton name={"Quests"} anchor={"Home"} />
|
||||
<SideBarButton name={"Achievements"} anchor={"Home"} />
|
||||
|
156
src/handbook/src/ui/widgets/EntityCard.tsx
Normal file
156
src/handbook/src/ui/widgets/EntityCard.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Entity as EntityType, EntityInfo } from "@backend/types";
|
||||
import { entityIcon } from "@app/utils";
|
||||
|
||||
import "@css/widgets/ItemCard.scss";
|
||||
|
||||
/**
|
||||
* Converts a description string into a list of paragraphs.
|
||||
*
|
||||
* @param description The description to convert.
|
||||
*/
|
||||
function toDescription(description: string | undefined): JSX.Element[] {
|
||||
if (!description) return [];
|
||||
|
||||
return description.split("\\n")
|
||||
.map((line, index) => {
|
||||
return <p key={index}>{line}</p>;
|
||||
});
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
entity: EntityType | null;
|
||||
info: EntityInfo | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
icon: boolean;
|
||||
count: number | string;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
icon: true,
|
||||
count: 1
|
||||
};
|
||||
|
||||
class EntityCard extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = defaultState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the count of the item.
|
||||
*
|
||||
* @param event The change event.
|
||||
* @private
|
||||
*/
|
||||
private updateCount(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = event.target.value;
|
||||
if (isNaN(parseInt(value)) && value.length > 1) return;
|
||||
|
||||
this.setState({ count: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to the count of the entity.
|
||||
*
|
||||
* @param positive Is the count being added or subtracted?
|
||||
* @param multiple Is the count being multiplied by 10?
|
||||
* @private
|
||||
*/
|
||||
private addCount(positive: boolean, multiple: boolean) {
|
||||
let { count } = this.state;
|
||||
if (count === "") count = 1;
|
||||
if (typeof count == "string")
|
||||
count = parseInt(count);
|
||||
if (count < 1) count = 1;
|
||||
|
||||
let increment = 1;
|
||||
if (!positive) increment = -1;
|
||||
if (multiple) increment *= 10;
|
||||
|
||||
count = Math.max(1, count + increment);
|
||||
|
||||
this.setState({ count });
|
||||
}
|
||||
|
||||
/**
|
||||
* Summons the entity at the connected player's position.
|
||||
* @private
|
||||
*/
|
||||
private async summonAtPlayer(): Promise<void> {
|
||||
// TODO: Implement server access.
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>, snapshot?: any) {
|
||||
if (this.props.entity != prevProps.entity) {
|
||||
this.setState(defaultState);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entity, info } = this.props;
|
||||
const data = info?.data;
|
||||
|
||||
return entity ? (
|
||||
<div className={"ItemCard"}>
|
||||
<div className={"ItemCard_Content"}>
|
||||
<div className={"ItemCard_Header"}>
|
||||
<div className={"ItemCard_Info"}>
|
||||
<p>{data?.name ?? entity.name}</p>
|
||||
<p>{data?.type ?? ""}</p>
|
||||
</div>
|
||||
|
||||
{ this.state.icon && <img
|
||||
className={"ItemCard_Icon"}
|
||||
alt={entity.name}
|
||||
src={entityIcon(entity)}
|
||||
onError={() => this.setState({ icon: false })}
|
||||
/> }
|
||||
</div>
|
||||
|
||||
<div className={"ItemCard_Description"}>
|
||||
{toDescription(data?.description)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"ItemCard_Actions"}>
|
||||
<div className={"ItemCard_Counter"}>
|
||||
<div onClick={() => this.addCount(false, false)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
this.addCount(false, true);
|
||||
}}
|
||||
className={"ItemCard_Operation"}>-</div>
|
||||
<input type={"text"}
|
||||
value={this.state.count}
|
||||
className={"ItemCard_Count"}
|
||||
onChange={this.updateCount.bind(this)}
|
||||
onBlur={() => {
|
||||
if (this.state.count == "") {
|
||||
this.setState({ count: 1 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div onClick={() => this.addCount(true, false)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
this.addCount(true, true);
|
||||
}}
|
||||
className={"ItemCard_Operation"}>+</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={"ItemCard_Submit"}
|
||||
onClick={this.summonAtPlayer.bind(this)}
|
||||
>Summon</button>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default EntityCard;
|
@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Item as ItemData } from "@backend/types";
|
||||
import { itemIcon } from "@app/utils";
|
||||
|
||||
import "@css/widgets/Item.scss";
|
||||
import "@css/widgets/MiniCard.scss";
|
||||
|
||||
interface IProps {
|
||||
data: ItemData;
|
||||
data: { name: string; };
|
||||
icon: string;
|
||||
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@ -16,7 +17,7 @@ interface IState {
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
class Item extends React.Component<IProps, IState> {
|
||||
class MiniCard extends React.Component<IProps, IState> {
|
||||
loading: number | any;
|
||||
|
||||
constructor(props: IProps) {
|
||||
@ -52,27 +53,25 @@ class Item extends React.Component<IProps, IState> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={"Item"}
|
||||
<div className={"MiniCard"}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className={"Item_Background"}>
|
||||
<div className={"MiniCard_Background"}>
|
||||
{this.state.icon && (
|
||||
<img
|
||||
className={"Item_Icon"}
|
||||
className={"MiniCard_Icon"}
|
||||
alt={this.props.data.name}
|
||||
src={itemIcon(this.props.data)}
|
||||
src={this.props.icon}
|
||||
onError={this.replaceIcon.bind(this)}
|
||||
onLoad={() => this.setState({ loaded: true })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(!this.state.loaded || !this.state.icon) && <p className={"Item_Label"}>{this.props.data.name}</p>}
|
||||
{(!this.state.loaded || !this.state.icon) && <p className={"MiniCard_Label"}>{this.props.data.name}</p>}
|
||||
</div>
|
||||
|
||||
<div className={"Item_Info"}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Item;
|
||||
export default MiniCard;
|
@ -1,5 +1,5 @@
|
||||
import type { Item } from "@backend/types";
|
||||
import { ItemInfo, ItemType, Quality } from "@backend/types";
|
||||
import type { Entity, Item, EntityInfo, ItemInfo } from "@backend/types";
|
||||
import { ItemType, Quality } from "@backend/types";
|
||||
|
||||
/**
|
||||
* Fetches the name of the CSS variable for the quality.
|
||||
@ -56,6 +56,16 @@ export function itemIcon(item: Item): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the icon for an entity.
|
||||
* Uses the Project Amber API to get the icon.
|
||||
*
|
||||
* @param entity The entity to get the icon for. Project Amber data required.
|
||||
*/
|
||||
export function entityIcon(entity: Entity): string {
|
||||
return `https://api.ambr.top/assets/UI/monster/UI_MonsterIcon_${entity.internal}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a character's name to fit with the reference name.
|
||||
* Example: Hu Tao -> hu_tao
|
||||
@ -111,3 +121,16 @@ export async function fetchItemData(item: Item): Promise<ItemInfo> {
|
||||
.then((res) => res.json())
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the data for an entity.
|
||||
* Uses the Project Amber API to get the data.
|
||||
*
|
||||
* @route GET https://api.ambr.top/v2/en/monster/{id}
|
||||
* @param entity The entity to fetch the data for.
|
||||
*/
|
||||
export async function fetchEntityData(entity: Entity): Promise<EntityInfo> {
|
||||
return fetch(`https://api.ambr.top/v2/en/monster/${entity.id}`)
|
||||
.then((res) => res.json())
|
||||
.catch(() => {});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user