mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-23 12:36:06 +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
|
## Files Required
|
||||||
- `commands.json`
|
- `commands.json`
|
||||||
|
- `entities.csv`
|
||||||
- `avatars.csv`
|
- `avatars.csv`
|
||||||
- `scenes.csv`
|
- `scenes.csv`
|
||||||
- `items.csv`
|
- `items.csv`
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import commands from "@data/commands.json";
|
import commands from "@data/commands.json";
|
||||||
|
import entities from "@data/entities.csv";
|
||||||
import avatars from "@data/avatars.csv";
|
import avatars from "@data/avatars.csv";
|
||||||
import scenes from "@data/scenes.csv";
|
import scenes from "@data/scenes.csv";
|
||||||
import items from "@data/items.csv";
|
import items from "@data/items.csv";
|
||||||
|
|
||||||
import { Quality, ItemType, ItemCategory, SceneType } from "@backend/types";
|
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";
|
import { inRange } from "@app/utils";
|
||||||
|
|
||||||
@ -72,6 +73,21 @@ export function listCommands(): Command[] {
|
|||||||
return Object.values(getCommands());
|
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.
|
* Fetches and casts all avatars in the file.
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export type Page = "Home" | "Commands" | "Avatars" | "Items"
|
export type Page = "Home" | "Commands" | "Avatars" | "Items"
|
||||||
| "Scenes";
|
| "Entities" | "Scenes";
|
||||||
export type Days = "Sunday" | "Monday" | "Tuesday"
|
export type Days = "Sunday" | "Monday" | "Tuesday"
|
||||||
| "Wednesday" | "Thursday" | "Friday" | "Saturday";
|
| "Wednesday" | "Thursday" | "Friday" | "Saturday";
|
||||||
|
|
||||||
@ -31,6 +31,12 @@ export type Item = {
|
|||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Entity = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
internal: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Exported from Project Amber.
|
// Exported from Project Amber.
|
||||||
export type ItemInfo = {
|
export type ItemInfo = {
|
||||||
response: number | 200 | 404;
|
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 {
|
export enum Target {
|
||||||
None = "NONE",
|
None = "NONE",
|
||||||
Offline = "OFFLINE",
|
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;
|
display: flex;
|
||||||
|
|
||||||
width: 64px;
|
width: 64px;
|
||||||
@ -8,7 +8,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Item_Background {
|
.MiniCard_Background {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@ -19,13 +19,14 @@
|
|||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Item_Icon {
|
.MiniCard_Icon {
|
||||||
max-width: 64px;
|
max-width: 64px;
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
object-fit: scale-down;
|
object-fit: scale-down;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Item_Label {
|
.MiniCard_Label {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -33,7 +34,7 @@
|
|||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Item_Info {
|
.MiniCard_Info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
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={"Commands"} anchor={"Commands"} />
|
||||||
<HomeButton name={"Characters"} anchor={"Avatars"} />
|
<HomeButton name={"Characters"} anchor={"Avatars"} />
|
||||||
<HomeButton name={"Items"} anchor={"Items"} />
|
<HomeButton name={"Items"} anchor={"Items"} />
|
||||||
<HomeButton name={"Entities"} anchor={"Home"} />
|
<HomeButton name={"Entities"} anchor={"Entities"} />
|
||||||
<HomeButton name={"Scenes"} anchor={"Scenes"} />
|
<HomeButton name={"Scenes"} anchor={"Scenes"} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent } from "react";
|
||||||
|
|
||||||
import Item from "@widgets/Item";
|
import MiniCard from "@widgets/MiniCard";
|
||||||
import ItemCard from "@widgets/ItemCard";
|
import ItemCard from "@widgets/ItemCard";
|
||||||
import VirtualizedGrid from "@components/VirtualizedGrid";
|
import VirtualizedGrid from "@components/VirtualizedGrid";
|
||||||
|
|
||||||
import { ItemCategory } from "@backend/types";
|
import { ItemCategory } from "@backend/types";
|
||||||
import type { Item as ItemType, ItemInfo } from "@backend/types";
|
import type { Item as ItemType, ItemInfo } from "@backend/types";
|
||||||
import { getItems, sortedItems } from "@backend/data";
|
import { getItems, sortedItems } from "@backend/data";
|
||||||
import { fetchItemData } from "@app/utils";
|
import { fetchItemData, itemIcon } from "@app/utils";
|
||||||
|
|
||||||
import "@css/pages/ItemsPage.scss";
|
import "@css/pages/ItemsPage.scss";
|
||||||
|
|
||||||
@ -133,8 +133,8 @@ class ItemsPage extends React.Component<{}, IState> {
|
|||||||
itemsPerRow={18}
|
itemsPerRow={18}
|
||||||
gap={5}
|
gap={5}
|
||||||
itemGap={5}
|
itemGap={5}
|
||||||
render={(item) => <Item
|
render={(item) => <MiniCard
|
||||||
key={item.id} data={item}
|
key={item.id} data={item} icon={itemIcon(item)}
|
||||||
onClick={() => this.setSelectedItem(item)}
|
onClick={() => this.setSelectedItem(item)}
|
||||||
/>}
|
/>}
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ import HomePage from "@pages/HomePage";
|
|||||||
import CommandsPage from "@pages/CommandsPage";
|
import CommandsPage from "@pages/CommandsPage";
|
||||||
import AvatarsPage from "@pages/AvatarsPage";
|
import AvatarsPage from "@pages/AvatarsPage";
|
||||||
import ItemsPage from "@pages/ItemsPage";
|
import ItemsPage from "@pages/ItemsPage";
|
||||||
|
import EntitiesPage from "@pages/EntitiesPage";
|
||||||
import ScenesPage from "@pages/ScenesPage";
|
import ScenesPage from "@pages/ScenesPage";
|
||||||
|
|
||||||
import type { Page } from "@backend/types";
|
import type { Page } from "@backend/types";
|
||||||
@ -58,6 +59,8 @@ class Content extends React.Component<IProps, IState> {
|
|||||||
return <AvatarsPage />;
|
return <AvatarsPage />;
|
||||||
case "Items":
|
case "Items":
|
||||||
return <ItemsPage />;
|
return <ItemsPage />;
|
||||||
|
case "Entities":
|
||||||
|
return <EntitiesPage />;
|
||||||
case "Scenes":
|
case "Scenes":
|
||||||
return <ScenesPage />;
|
return <ScenesPage />;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class SideBar extends React.Component<{}, IState> {
|
|||||||
<SideBarButton name={"Commands"} anchor={"Commands"} />
|
<SideBarButton name={"Commands"} anchor={"Commands"} />
|
||||||
<SideBarButton name={"Characters"} anchor={"Avatars"} />
|
<SideBarButton name={"Characters"} anchor={"Avatars"} />
|
||||||
<SideBarButton name={"Items"} anchor={"Items"} />
|
<SideBarButton name={"Items"} anchor={"Items"} />
|
||||||
<SideBarButton name={"Entities"} anchor={"Home"} />
|
<SideBarButton name={"Entities"} anchor={"Entities"} />
|
||||||
<SideBarButton name={"Scenes"} anchor={"Scenes"} />
|
<SideBarButton name={"Scenes"} anchor={"Scenes"} />
|
||||||
<SideBarButton name={"Quests"} anchor={"Home"} />
|
<SideBarButton name={"Quests"} anchor={"Home"} />
|
||||||
<SideBarButton name={"Achievements"} 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 React from "react";
|
||||||
|
|
||||||
import type { Item as ItemData } from "@backend/types";
|
|
||||||
import { itemIcon } from "@app/utils";
|
import { itemIcon } from "@app/utils";
|
||||||
|
|
||||||
import "@css/widgets/Item.scss";
|
import "@css/widgets/MiniCard.scss";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
data: ItemData;
|
data: { name: string; };
|
||||||
|
icon: string;
|
||||||
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ interface IState {
|
|||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Item extends React.Component<IProps, IState> {
|
class MiniCard extends React.Component<IProps, IState> {
|
||||||
loading: number | any;
|
loading: number | any;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
@ -52,27 +53,25 @@ class Item extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={"Item"}
|
<div className={"MiniCard"}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
>
|
>
|
||||||
<div className={"Item_Background"}>
|
<div className={"MiniCard_Background"}>
|
||||||
{this.state.icon && (
|
{this.state.icon && (
|
||||||
<img
|
<img
|
||||||
className={"Item_Icon"}
|
className={"MiniCard_Icon"}
|
||||||
alt={this.props.data.name}
|
alt={this.props.data.name}
|
||||||
src={itemIcon(this.props.data)}
|
src={this.props.icon}
|
||||||
onError={this.replaceIcon.bind(this)}
|
onError={this.replaceIcon.bind(this)}
|
||||||
onLoad={() => this.setState({ loaded: true })}
|
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>
|
||||||
|
|
||||||
<div className={"Item_Info"}></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Item;
|
export default MiniCard;
|
@ -1,5 +1,5 @@
|
|||||||
import type { Item } from "@backend/types";
|
import type { Entity, Item, EntityInfo, ItemInfo } from "@backend/types";
|
||||||
import { ItemInfo, ItemType, Quality } from "@backend/types";
|
import { ItemType, Quality } from "@backend/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the name of the CSS variable for the quality.
|
* 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.
|
* Formats a character's name to fit with the reference name.
|
||||||
* Example: Hu Tao -> hu_tao
|
* Example: Hu Tao -> hu_tao
|
||||||
@ -111,3 +121,16 @@ export async function fetchItemData(item: Item): Promise<ItemInfo> {
|
|||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch(() => {});
|
.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