misc: Remove the handbook from the main repository

sorry guys, i just wanted to do something cool...
This commit is contained in:
KingRainbow44 2024-09-28 17:45:21 -04:00
parent ab0ec0a0e0
commit 70bb5ca5b5
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
78 changed files with 12 additions and 11291 deletions

View File

@ -36,13 +36,8 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Download Handbook
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/handbook.html
target: src/main/resources/html/
- name: Run Gradle
run: ./gradlew -PskipHandbook=1 && ./gradlew jar -PskipHandbook=1
run: ./gradlew && ./gradlew jar
- name: Upload build
uses: actions/upload-artifact@v3
with:

View File

@ -38,7 +38,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Format Code
run: ./gradlew -PskipHandbook=1 && ./gradlew spotlessApply -PskipHandbook=1
run: ./gradlew && ./gradlew spotlessApply
- run: git config --global user.name "github-actions"
- run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

View File

@ -1,126 +0,0 @@
name: "Handbook"
on:
workflow_dispatch: ~
push:
paths:
- "src/handbook/**.tsx"
branches:
- "development"
- "unstable"
pull_request:
paths:
- "src/handbook/**.tsx"
types:
- opened
- synchronize
- reopened
jobs:
Lint-Code:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '17'
- name: Cache node modules
uses: actions/cache@v2
with:
path: |
~/.npm
~/.cache
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
working-directory: src/handbook
run: npm install --force
- name: Run linter
working-directory: src/handbook
run: npm run lint
- run: git config --global user.name "github-actions"
- run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- run: git stash
- run: git checkout ${{ steps.extract_branch.outputs.branch }} && git pull
- run: git stash pop || true
- name: Commit changes
if: ${{ github.event_name == 'push' }}
run: git add -u && git commit -m "Lint Code [skip actions]" || true
- name: Push changes
if: ${{ github.event_name == 'push' }}
run: git push --set-upstream --force origin ${{ steps.extract_branch.outputs.branch }}
Build-Handbook:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '17'
- name: Cache node modules
uses: actions/cache@v2
with:
path: |
~/.npm
~/.cache
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
working-directory: src/handbook
run: npm install --force
- name: Download Avatar Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/avatars.csv
target: src/handbook/data/
- name: Download Command Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/commands.json
target: src/handbook/data/
- name: Download Entity Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/entities.csv
target: src/handbook/data/
- name: Download Item Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/items.csv
target: src/handbook/data/
- name: Download Scene Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/scenes.csv
target: src/handbook/data/
- name: Download Quest Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/quests.csv
target: src/handbook/data/
- name: Download Main Quest Data
uses: suisei-cn/actions-download-file@v1.4.0
with:
url: https://api.grasscutter.io/static/mainquests.csv
target: src/handbook/data/
- name: Build handbook
working-directory: src/handbook
run: npm run build
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: Handbook
path: src/handbook/dist/*.html

1
.gitignore vendored
View File

@ -67,7 +67,6 @@ tmp/
!entrypoint.sh
GM Handbook*.txt
handbook.html
config.json
mitmdump.exe

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "docs/wiki"]
path = docs/wiki
url = https://github.com/Grasscutters/Grasscutter.wiki.git
[submodule "src/handbook/data/assets"]
path = src/handbook/data/assets
url = https://github.com/genshitters/gm-handbook-assets.git

View File

@ -31,7 +31,7 @@
[4.0.x Client-cloud drive](https://www.123pan.com/s/HoqUVv-U7SBA.html)
- Download the [latest Cultivation version](https://github.com/Grasscutters/Cultivation/releases/latest). Use the `.msi` installer.
- After opening Cultivation (as admin), press the download button in the upper right corner.
- After opening Cultivation (as admin), press the download button in the upper right corner.
- Click `Download All-in-One`
- Click the gear in the upper right corner
- Set the game Install path to where your game is located.
@ -50,7 +50,6 @@ Grasscutter uses Gradle to handle dependencies & building.
- [Java Development Kit 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or higher
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download) (Optional, for building the handbook)
##### Clone
@ -61,8 +60,6 @@ cd Grasscutter
##### Compile
**Note**: Handbook generation may fail on some systems. To disable the handbook generation, append `-PskipHandbook=1` to the `gradlew jar` command.
Windows:
```shell
@ -77,24 +74,6 @@ chmod +x gradlew
./gradlew jar
```
##### Compiling the Handbook (Manually)
With Gradle:
```shell
./gradlew generateHandbook
```
With NPM:
```shell
cd src/handbook
npm install
npm run build
```
You can find the output jar in the root of the project folder.
### Troubleshooting
### Troubleshooting
For a list of common issues and solutions and to ask for help, please join [our Discord server](https://discord.gg/T5vZU6UyeG) and go to the support channel.

View File

@ -1,4 +1,3 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.plugins.ide.eclipse.model.SourceFolder
/*
@ -311,7 +310,6 @@ javadoc {
// Add this to avoid warning caused by lack of comments in proto generated java files
options.addStringOption('Xdoclint:none', '-quiet')
exclude '**/*.md'
exclude 'src/handbook/**/*.*'
exclude 'src/generated/**/*.*'
}
@ -332,81 +330,6 @@ public final class BuildConfig {
}"""
}
tasks.register('generateHandbook') {
if (project.hasProperty('skipHandbook')) {
println('Skipping handbook generation.')
return
}
// Resolve the NPM command.
var npm = 'npm'
if (Os.isFamily(Os.FAMILY_WINDOWS))
npm = 'npm.cmd'
def npmVersion = {
try {
return "${npm} --version".execute()
} catch (ignored) {
ignored.printStackTrace()
return 'NPM_NOT_FOUND'
}
}
// Check if Node is installed.
if (npmVersion() == 'NPM_NOT_FOUND') {
println('NPM is not installed. Skipping handbook generation.')
} else {
// Check if the handbook resources are present.
if (!file('src/handbook/data/commands.json').exists()) {
println('Command data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/avatars.csv').exists()) {
println('Avatar data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/entities.csv').exists()) {
println('Entity data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/items.csv').exists()) {
println('Item data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/mainquests.csv').exists()) {
println('Main quest data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/quests.csv').exists()) {
println('Quest data was not found. Skipping handbook generation.')
return
}
if (!file('src/handbook/data/scenes.csv').exists()) {
println('Scene data was not found. Skipping handbook generation.')
return
}
// Install dependencies before building.
exec {
workingDir 'src/handbook'
commandLine npm, 'install'
}
// Build the handbook.
exec {
workingDir 'src/handbook'
commandLine npm, 'run', 'build'
}
// Copy the handbook from /dist to /src/main/resources.
copy {
from 'src/handbook/dist/index.html'
into 'src/main/resources/html'
rename 'index.html', 'handbook.html'
}
}
}
processResources {
dependsOn 'generateProto'
}

View File

@ -1,27 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Handbook data
data/

View File

@ -1,12 +0,0 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"jsxSingleQuote": false,
"jsxBracketSameLine": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

View File

@ -1,25 +0,0 @@
import tailwind from "tailwindcss";
import autoprefixer from "autoprefixer";
import cssnanoPlugin from "cssnano";
import tailwindConfig from "./tailwind.config.js";
const mode = process.env.NODE_ENV;
const dev = mode === "development";
export default {
plugins: (() => {
let plugins = [
// Some plugins, like TailwindCSS/Nesting, need to run before Tailwind.
tailwind(tailwindConfig),
// But others, like autoprefixer, need to run after.
autoprefixer()
];
!dev && cssnanoPlugin({
preset: "default"
});
return plugins;
})()
}

View File

@ -1,9 +0,0 @@
export default {
content: ["./src/**/*.{html,js,tsx,ts}"],
mode: "jit",
theme: {
extend: {}
},
darkMode: "class",
plugins: []
};

View File

@ -1,58 +0,0 @@
# Handbook Data
Use Grasscutter's dumpers to generate the data to put here.
# Generating Data
When you have Grasscutter set up, you can use the following commands to generate the data:
- Commands - `grasscutter.jar -dump=commands,en-us`
- Items - `grasscutter.jar -dump=items,EN`
- Avatars - `grasscutter.jar -dump=avatars,EN`
- Quests - `grasscutter.jar -dump=quests,EN`
- Entities - `grasscutter.jar -dump=entities,en-us`
- Areas - `grasscutter.jar -dump=areas,EN`
- Scenes - `grasscutter.jar -dump=scenes,en-us`
Grasscutter being "set up" means:
- A Java runtime is installed
- Resources are provided in the working directory
## Language Locales
You can replace `en-us` or `EN` using the language locale which matches the format.
| Grasscutter Language Locale | Handbook Language Locale |
|-----------------------------|--------------------------|
| en-us | EN |
## Files Required
- `mainquests.csv`
- `commands.json`
- `entities.csv`
- `avatars.csv`
- `scenes.csv`
- `quests.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

@ -1 +0,0 @@
Subproject commit 1b9f8b2c0d60a9c06a6eb6ad5fc21c55afd926fd

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GM Handbook</title>
<script>
window["hide"] = ["quests", "achievements"];
window["details"] = {
address: "{{DETAILS_ADDRESS}}",
port: "{{DETAILS_PORT}}",
disable: "{{DETAILS_DISABLE}}"
};
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
{
"name": "handbook",
"description": "The ultimate anime game handbook!",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"postinstall": "npx patch-package",
"lint": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\""
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-d3-tree": "^3.6.1",
"react-collapsible": "^2.10.0",
"react-virtualized": "^9.22.3",
"events": "^3.3.0"
},
"devDependencies": {
"typescript": "^4.9.3",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-virtualized": "^9.21.21",
"@types/events": "^3.0.0",
"vite": "^4.2.0",
"vite-plugin-svgr": "^2.4.0",
"vite-tsconfig-paths": "^4.0.7",
"vite-plugin-singlefile": "^0.13.5",
"@vitejs/plugin-react-swc": "^3.0.0",
"@rollup/plugin-dsv": "^3.0.2",
"sass": "^1.58.3",
"cssnano": "^5.1.15",
"tailwindcss": "^3.2.7",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"postcss-load-config": "^4.0.1",
"postcss-font-magician": "^3.0.0",
"prettier": "^2.8.7",
"patch-package": "^6.5.1"
}
}

View File

@ -1,10 +0,0 @@
diff --git a/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js b/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
index d00f0f1..42456dc 100644
--- a/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
+++ b/node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js
@@ -71,4 +71,3 @@ export function unregisterScrollListener(component, element) {
}
}
}
-import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";
\ No newline at end of file

View File

@ -1,55 +0,0 @@
/**
* Validates a number.
*
* @param value The number to validate.
*/
function invalid(value: number): boolean {
return isNaN(value) || value < 0;
}
/**
* Generates a basic give command.
*/
function basicGive(item: number, amount = 1): string {
// Validate the numbers.
if (invalid(item) || invalid(amount)) return "Invalid arguments.";
return `/give ${item} x${amount}`;
}
/**
* Generates a basic teleport command.
* This creates a relative teleport command.
*/
function teleport(scene: number): string {
// Validate the number.
if (invalid(scene)) return "Invalid arguments.";
return `/teleport ~ ~ ~ ${scene}`;
}
/**
* Generates a basic spawn monster command.
*
* @param monster The monster's ID.
* @param amount The amount of the monster to spawn.
* @param level The level of the monster to spawn.
*/
function spawnMonster(monster: number, amount = 1, level = 1): string {
// Validate the numbers.
if (invalid(monster) || invalid(amount)) return "Invalid arguments.";
return `/spawn ${monster} x${amount} lv${level}`;
}
export const give = {
basic: basicGive
};
export const spawn = {
monster: spawnMonster
};
export const action = {
teleport: teleport
};

View File

@ -1,245 +0,0 @@
import mainQuests from "@data/mainquests.csv";
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 quests from "@data/quests.csv";
import items from "@data/items.csv";
import type { RawNodeDatum } from "react-d3-tree";
import { Quality, ItemType, ItemCategory, SceneType } from "@backend/types";
import type { MainQuest, Command, Avatar, Item, Scene, Entity, Quest } from "@backend/types";
import { inRange } from "@app/utils";
type AvatarDump = { [key: number]: Avatar };
type CommandDump = { [key: string]: Command };
type TaggedItems = { [key: number]: Item[] };
type QuestDump = { [key: number]: Quest };
type MainQuestDump = { [key: number]: MainQuest };
/**
* @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]: [],
[ItemCategory.Material]: [],
[ItemCategory.Miscellaneous]: []
};
export let allMainQuests: MainQuestDump = {};
/**
* Setup function for this file.
* Sorts all items into their respective categories.
*/
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;
}
// 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);
}
});
allMainQuests = getMainQuests();
}
/**
* Fetches and casts all commands in the file.
*/
export function getCommands(): CommandDump {
return commands as CommandDump;
}
/**
* Fetches and lists all the commands in the file.
*/
export function listCommands(): Command[] {
return Object.values(getCommands()).sort((a, b) => a.name[0].localeCompare(b.name[0]));
}
/**
* 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]
};
})
.filter((entity) => !entity.name.includes("Mechanicus"));
}
/**
* Fetches and casts all avatars in the file.
*/
export function getAvatars(): AvatarDump {
const map: AvatarDump = {};
avatars.forEach((avatar) => {
const values = Object.values(avatar) as [string, string, string];
const id = parseInt(values[0]);
map[id] = {
id,
name: values[1],
quality: values[2] as Quality
};
});
return map;
}
/**
* Fetches and lists all the avatars in the file.
*/
export function listAvatars(): Avatar[] {
return Object.values(getAvatars()).sort((a, b) => a.name.localeCompare(b.name));
}
/**
* Fetches and casts all scenes in the file.
*/
export function getScenes(): Scene[] {
return scenes
.map((entry) => {
const values = Object.values(entry) as string[];
const id = parseInt(values[0]);
return {
id,
identifier: values[1],
type: values[2] as SceneType
};
})
.sort((a, b) => a.id - b.id);
}
/**
* Fetches and casts all items in the file.
*/
export function getItems(): Item[] {
return items.map((entry) => {
const values = Object.values(entry) as string[];
const id = parseInt(values[0]);
return {
id,
name: values[1],
type: values[3] as ItemType,
quality: values[2] as Quality,
icon: values[4]
};
});
}
/**
* Fetches and casts all quests in the file.
*/
export function getQuests(): QuestDump {
const map: QuestDump = {};
quests.forEach((quest: Quest) => {
quest.description = quest.description.replaceAll("\\", ",");
map[quest.id] = quest;
});
return map;
}
/**
* Fetches and lists all the quests in the file.
*/
export function listQuests(): Quest[] {
return Object.values(getQuests()).sort((a, b) => a.id - b.id);
}
/**
* Fetches and casts all quests in the file.
*/
export function getMainQuests(): MainQuestDump {
const map: MainQuestDump = {};
mainQuests.forEach((quest: MainQuest) => {
quest.title = quest.title.replaceAll("\\", ",");
map[quest.id] = quest;
});
return map;
}
/**
* Fetches and lists all the quests in the file.
*/
export function listMainQuests(): MainQuestDump[] {
return Object.values(allMainQuests).sort((a, b) => a.id - b.id);
}
/**
* Fetches a quest by its ID.
*
* @param quest The quest ID.
*/
export function getMainQuestFor(quest: Quest): MainQuest {
return allMainQuests[quest.mainId];
}
/**
* Fetches all quests for a main quest.
*
* @param mainQuest The main quest to fetch quests for.
*/
export function listSubQuestsFor(mainQuest: MainQuest): Quest[] {
return listQuests().filter((quest) => quest.mainId == mainQuest.id);
}
/*
* Tree conversion methods.
*/
/**
* Converts a quest to a tree.
*
* @param mainQuest The main quest to convert.
*/
export function questToTree(mainQuest: MainQuest): RawNodeDatum {
return {
name: mainQuest.title,
attributes: {
id: mainQuest.id
},
children: listSubQuestsFor(mainQuest).map((quest) => {
return {
name: quest.id.toString(),
attributes: {
description: quest.description
},
children: []
} as RawNodeDatum;
})
};
}

View File

@ -1,103 +0,0 @@
import { EventEmitter } from "events";
import type { Page } from "@backend/types";
import { isPage } from "@backend/types";
const emitter = new EventEmitter();
const navigation = new EventEmitter();
let navStack: Page[] = [];
let currentPage: number | null = -1;
/**
* Sets up the event system.
*/
export function setup(): void {
window.onpopstate = (event) => {
navigate(event.state, false);
};
setTimeout(() => {
// Check if the window's href is a page.
const page = window.location.href.split("/").pop();
if (page == undefined || page == "") return;
// Convert the page to a Page type.
const pageName = page.charAt(0).toUpperCase() + page.slice(1);
const pageType = pageName as Page;
// Navigate to the page.
isPage(page) && navigate(pageType, false);
}, 3e2);
}
/**
* Adds a navigation listener.
*
* @param listener The listener to add.
*/
export function addNavListener(listener: (page: Page) => void) {
navigation.on("navigate", listener);
}
/**
* Removes a navigation listener.
*
* @param listener The listener to remove.
*/
export function removeNavListener(listener: (page: Page) => void) {
navigation.off("navigate", listener);
}
/**
* Navigates to a page.
* Returns the last page.
*
* @param page The page to navigate to.
* @param update Whether to update the state or not.
*/
export function navigate(page: Page, update: boolean = true): Page | null {
// Check the page.
if (page == undefined) page = "Home";
// Navigate to the new page.
const lastPage = currentPage;
navigation.emit("navigate", page);
if (update) {
// Set the current page.
navStack.push(page);
currentPage = navStack.length - 1;
// Add the page to the window history.
window.history.pushState(page, page, "/" + page.toLowerCase());
}
return lastPage ? navStack[lastPage] : null;
}
/**
* Goes back or forward in the navigation stack.
*
* @param forward Whether to go forward or not.
*/
export function go(forward: boolean): void {
if (currentPage == undefined) return;
// Get the new page.
const newPage = forward ? currentPage + 1 : currentPage - 1;
if (newPage < 0 || newPage >= navStack.length) return;
// Navigate to the new page.
currentPage = newPage;
navigation.emit("navigate", navStack[newPage]);
// Update the window history.
window.history.pushState(navStack[newPage], navStack[newPage], "/" + navStack[newPage].toLowerCase());
}
// This is the global event system.
export default emitter;
// @ts-ignore
window["emitter"] = emitter;
// @ts-ignore
window["navigate"] = navigate;

View File

@ -1,13 +0,0 @@
declare module "*.svg" {
export const ReactComponent: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
}
declare module "*.webp" {
const ref: string;
export default ref;
}
declare module "*.csv" {
const content: any[];
export default content;
}

View File

@ -1,198 +0,0 @@
import type { CommandResponse } from "@backend/types";
import emitter from "@backend/events";
import { getWindowDetails } from "@app/utils";
let playerToken: string | null = null; // The session token for the player.
export let targetPlayer = 0; // The UID of the target player.
// The server's address and port.
export let address: string = getWindowDetails().address,
port: string = getWindowDetails().port.toString();
export let encrypted: boolean = true;
export let lockedPlayer = false; // Whether the UID field is locked.
export let connected = false; // Whether the server is connected.
/**
* Loads the server details from local storage.
*/
export function setup(): void {
// Check if the server is disabled.
if (getWindowDetails().disable) return;
// Load the server details from local storage.
const storedAddress = localStorage.getItem("address");
const storedPort = localStorage.getItem("port");
// Set the server details.
if (storedAddress) address = storedAddress;
if (storedPort) port = storedPort;
}
/**
* Returns the formed URL.
* This assumes that the server upgrades to HTTPS.
*/
export function url(): string {
// noinspection HttpUrlsUsage
return `http${window.isSecureContext || encrypted ? "s" : ""}://${address}:${port}`;
}
/**
* Sets the target player.
*
* @param player The UID of the target player.
* @param token The session token for the player.
*/
export function setTargetPlayer(player: number, token: string | null = null): void {
playerToken = token;
targetPlayer = player;
// Determine connected status.
connected = !isNaN(player) && player > 0;
// Determine locked status.
lockedPlayer = connected && token != null;
// Emit the connected event.
emitter.emit("connected", connected);
}
/**
* Sets the server details.
*
* @param newAddress The server's address.
* @param newPort The server's port.
*/
export async function setServerDetails(newAddress: string | null, newPort: string | number | null): Promise<void> {
if (!getWindowDetails().disable) {
if (typeof newPort == "number") newPort = newPort.toString();
// Apply the new details.
if (newAddress != null) {
address = newAddress;
localStorage.setItem("address", newAddress);
}
if (newPort != null) {
port = newPort;
localStorage.setItem("port", newPort);
}
}
// Check if the server is encrypted.
return new Promise((resolve) => {
encrypted = true;
fetch(`${url()}`)
.catch(() => {
encrypted = false;
resolve();
})
.then(() => resolve());
});
}
/**
* Validates a number.
*
* @param value The number to validate.
*/
function invalid(value: number): boolean {
return isNaN(value) || value < 0;
}
/**
* Grants an avatar to a player.
*
* @param avatar The avatar's ID.
* @param level The avatar's level.
* @param constellations The avatar's unlocked constellations.
* @param talents The level for the avatar's talents.
*/
export async function grantAvatar(
avatar: number,
level = 90,
constellations = 6,
talents = 10
): Promise<CommandResponse> {
// Validate the numbers.
if (invalid(avatar) || invalid(level) || invalid(constellations) || invalid(talents))
return { status: -1, message: "Invalid arguments." };
return await fetch(`${url()}/handbook/avatar`, {
method: "POST",
body: JSON.stringify({
playerToken,
player: targetPlayer.toString(),
avatar: avatar.toString(),
level,
constellations,
talentLevels: talents
})
}).then((res) => res.json());
}
/**
* Gives an item to the player.
* This does not support weapons.
* This does not support relics.
*
* @param item The item's ID.
* @param amount The amount of the item to give.
*/
export async function giveItem(item: number, amount = 1): Promise<CommandResponse> {
// Validate the number.
if (isNaN(amount) || amount < 1) return { status: -1, message: "Invalid amount." };
return await fetch(`${url()}/handbook/item`, {
method: "POST",
body: JSON.stringify({
playerToken,
player: targetPlayer.toString(),
item: item.toString(),
amount
})
}).then((res) => res.json());
}
/**
* Teleports the player to a new scene.
*
* @param scene The scene's ID.
*/
export async function teleportTo(scene: number): Promise<CommandResponse> {
// Validate the number.
if (isNaN(scene) || scene < 1) return { status: -1, message: "Invalid scene." };
return await fetch(`${url()}/handbook/teleport`, {
method: "POST",
body: JSON.stringify({
playerToken,
player: targetPlayer.toString(),
scene: scene.toString()
})
}).then((res) => res.json());
}
/**
* Spawns an entity.
*
* @param entity The entity's ID.
* @param amount The amount of the entity to spawn.
* @param level The level of the entity to spawn.
*/
export async function spawnEntity(entity: number, amount = 1, level = 1): Promise<CommandResponse> {
// Validate the numbers.
if (isNaN(entity) || isNaN(amount) || isNaN(level) || amount < 1 || level < 1 || level > 200)
return { status: -1, message: "Invalid arguments." };
return await fetch(`${url()}/handbook/spawn`, {
method: "POST",
body: JSON.stringify({
playerToken,
player: targetPlayer.toString(),
entity: entity.toString(),
amount,
level
})
}).then((res) => res.json());
}

View File

@ -1,177 +0,0 @@
export type Page = "Home" | "Commands" | "Avatars" | "Items" | "Entities" | "Scenes" | "Quests" | "Achievements";
export type Overlays = "None" | "ServerSettings";
export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
export type MainQuest = {
id: number;
title: string;
};
export type Command = {
name: string[];
description: string;
usage: string[];
permission: string[];
target: Target;
};
export type Avatar = {
name: string;
quality: Quality;
id: number;
};
export type Scene = {
identifier: string;
type: SceneType;
id: number;
};
export type Item = {
id: number;
name: string;
quality: Quality;
type: ItemType;
icon: string;
};
export type Entity = {
id: number;
name: string;
internal: string;
};
export type Quest = {
id: number;
description: string;
mainId: number;
};
// Exported from Project Amber.
export type ItemInfo = {
response: number | 200 | 404;
data: {
name: string;
description: string;
type: string;
recipe: boolean;
mapMark: boolean;
source: {
name: string;
type: string | "domain";
days: Days;
}[];
icon: string;
rank: 1 | 2 | 3 | 4 | 5;
route: string;
};
};
// 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",
Player = "PLAYER",
Online = "ONLINE"
}
export enum Quality {
Legendary = "LEGENDARY",
Epic = "EPIC",
Rare = "RARE",
Uncommon = "UNCOMMON",
Common = "COMMON",
Unknown = "UNKNOWN"
}
export enum ItemType {
None = "ITEM_NONE",
Virtual = "ITEM_VIRTUAL",
Material = "ITEM_MATERIAL",
Reliquary = "ITEM_RELIQUARY",
Weapon = "ITEM_WEAPON",
Display = "ITEM_DISPLAY",
Furniture = "ITEM_FURNITURE"
}
export enum SceneType {
None = "SCENE_NONE",
World = "SCENE_WORLD",
Dungeon = "SCENE_DUNGEON",
Room = "SCENE_ROOM",
HomeWorld = "SCENE_HOME_WORLD",
HomeRoom = "SCENE_HOME_ROOM",
Activity = "SCENE_ACTIVITY"
}
export enum ItemCategory {
Constellation,
Avatar,
Weapon,
Artifact,
Furniture,
Material,
Miscellaneous
}
export type CommandResponse = {
status: number | 200 | 500;
message: string;
};
export type WindowDetails = {
address: string;
port: number;
disable: boolean;
};
/**
* Checks if a string is a page.
*
* @param page The string to check.
*/
export function isPage(page: string): page is Page {
return ["Home", "Commands", "Avatars", "Items", "Entities", "Scenes"].includes(page);
}
/**
* Converts an item type to a string.
*
* @param type The item type to convert.
*/
export function itemTypeToString(type: ItemType): string {
switch (type) {
default:
return "Unknown";
case ItemType.None:
return "None";
case ItemType.Virtual:
return "Virtual";
case ItemType.Material:
return "Material";
case ItemType.Reliquary:
return "Reliquary";
case ItemType.Weapon:
return "Weapon";
case ItemType.Display:
return "Display";
case ItemType.Furniture:
return "Furniture";
}
}

View File

@ -1,88 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Poppins';
src: url('/data/assets/Poppins-Regular.ttf')
}
html {
--background-color: #25294a;
--primary-color: #2d325a;
--secondary-color: #202442;
--accent-color: #4b5396;
--text-primary-color: #FFFFFF;
--unselected-color: #c7c8d0;
--selected-color: #FFFFFF;
--legendary-color: #926d45;
--epic-color: #7b5c90;
// pq = Primary Quest
--pq-bg: #333d49;
--pq-text: #d3bc8e;
--pq-text2: #8c836f;
--quest-unselected: #4e5765;
--quest-selected: #ede5da;
--quest-accent: #9b927d;
// qt = Quest Text
--qt-unselected: #ede5da;
--qt2-unselected: #8e9295;
--qt-selected: #4d5568;
--qt2-selected: #a6a5a7;
overflow: hidden;
}
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100%;
overflow: hidden;
background-color: var(--background-color);
#root {
height: 100%;
width: 100%;
}
* {
font-family: 'SDK_SC_Web', 'SDK_JP_Web', 'Poppins', sans-serif;
}
svg:focus {
outline: none;
}
}
.App {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
button {
background-color: transparent;
border: none;
cursor: pointer;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--accent-color);
border-radius: 10px;
}

View File

@ -1,28 +0,0 @@
p {
color: var(--text-primary-color);
margin: 0;
}
h1 {
color: var(--text-primary-color);
font-style: normal;
font-weight: normal;
font-size: 48px;
margin: 0;
}
h2 {
color: var(--text-primary-color);
font-style: normal;
font-weight: 600;
font-size: 24px;
margin: 0;
}
h3 {
color: var(--text-primary-color);
font-style: normal;
font-weight: 600;
font-size: 18px;
margin: 0;
}

View File

@ -1,4 +0,0 @@
.GridRow {
display: flex;
flex-direction: row;
}

View File

@ -1,31 +0,0 @@
.AvatarsPage {
display: flex;
width: calc(100% - 352px);
background-color: var(--background-color);
flex-direction: column;
padding: 24px;
overflow-y: scroll;
}
.AvatarsPage_Title {
max-width: 275px;
max-height: 60px;
font-size: 48px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
}
.AvatarsPage_List {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
max-width: 90%;
}

View File

@ -1,30 +0,0 @@
.CommandsPage {
display: flex;
height: 100%;
width: 100%;
background-color: var(--background-color);
flex-direction: column;
padding: 24px;
}
.CommandsPage_Title {
max-width: 275px;
max-height: 60px;
font-size: 48px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
}
.CommandsPage_List {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 28px;
overflow-y: scroll;
}

View File

@ -1,93 +0,0 @@
.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(--primary-color);
}
.EntitiesPage_Input {
background: none;
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;
}

View File

@ -1,147 +0,0 @@
.HomePage {
height: 100%;
width: 100%;
overflow-y: scroll;
padding: 0;
display:flex;
flex-direction: column;
justify-content: space-between;
gap: 50px;
background-color: var(--background-color);
div {
display: flex;
}
}
.HomePage_Top {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
gap: 24px;
}
.HomePage_Title {
margin-top: 31px;
margin-bottom: 15px;
}
.HomePage_Buttons {
width: 100%;
max-width: 1376px;
gap: 24px;
justify-content: center;
flex-wrap: wrap;
}
.HomePage_Bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 50px;
}
.HomePage_Box {
display: flex;
background-color: var(--primary-color);
}
.HomePage_Disclaimer {
display: flex;
flex-direction: row;
gap: 30px;
background-color: var(--primary-color);
height: 100px;
align-self: end;
margin: 0 0 0 60px;
border-radius: 10px;
box-sizing: border-box;
padding: 11px;
p {
font-size: 16px;
}
}
.HomePage_Discord {
max-width: 150px;
padding: 10px;
border-radius: 10px;
gap: 8px;
align-self: center;
align-items: center;
svg {
width: 100%;
height: 100%;
max-width: 44px;
max-height: 30px;
}
p {
font-size: 16px;
}
&:hover {
cursor: pointer;
background-color: #5865F2;
box-shadow: 0 0 10px 0 rgba(0,0,0,0.75);
}
}
.HomePage_Text {
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--primary-color);
max-width: 300px;
margin: 13px 60px 0 0;
border-radius: 10px;
box-sizing: border-box;
padding: 11px;
}
.HomePage_Credits {
display: flex;
flex-direction: row;
gap: 5px;
:nth-child(1) {
font-size: 18px;
font-weight: bold;
}
:nth-child(2) {
font-size: 10px;
align-self: center;
}
}
.HomePage_Links {
display: flex;
flex-direction: column;
a {
color: var(--text-primary-color);
text-decoration: none;
padding-right: 10px;
&:hover {
text-decoration: underline;
}
}
}

View File

@ -1,88 +0,0 @@
.ItemsPage {
display: flex;
height: 100%;
width: 100%;
flex-direction: row;
justify-content: space-between;
background-color: var(--background-color);
padding: 24px;
}
.ItemsPage_Content {
display: flex;
flex-direction: column;
width: 80%;
}
.ItemsPage_Header {
display: flex;
flex-direction: row;
gap: 30px;
align-content: center;
margin-bottom: 30px;
}
.ItemsPage_Title {
max-width: 130px;
max-height: 60px;
font-size: 48px;
font-weight: bold;
text-align: center;
justify-content: center;
}
.ItemsPage_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(--primary-color);
}
.ItemsPage_Input {
background-color: transparent;
border: none;
color: var(--text-primary-color);
font-size: 20px;
width: 100%;
padding: 11px;
&:focus, &:active {
outline: none;
}
}
.ItemsPage_Input::placeholder {
color: var(--text-secondary-color);
opacity: 1;
}
.ItemsPage_List {
margin-bottom: 28px;
overflow-y: scroll;
}
.ItemsPage_Card {
display: flex;
width: 100%;
max-width: 300px;
min-height: 300px;
max-height: 700px;
align-self: center;
}

View File

@ -1,14 +0,0 @@
.QuestsPage {
display: flex;
justify-content: space-between;
width: 100%;
height: 100%;
}
.QuestsPage_Selector {
display: flex;
width: 50%;
height: 100%;
}

View File

@ -1,56 +0,0 @@
.ScenesPage {
display: flex;
height: 100%;
width: 100%;
background-color: var(--background-color);
flex-direction: column;
padding: 24px;
}
.ScenesPage_Title {
max-width: 180px;
max-height: 60px;
font-size: 48px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
}
.ScenesPage_List {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 28px;
overflow-y: scroll;
}
.ScenesPage_Button {
width: 94px;
height: 34px;
margin: 0;
border-radius: 10px;
border: transparent;
font-size: 20px;
color: var(--text-primary-color);
background-color: var(--background-color);
user-select: none;
transition: 0.1s ease-in-out all;
}
.ScenesPage_Button:hover {
cursor: pointer;
}
.ScenesPage_Button:active {
scale: 0.9;
}

View File

@ -1,4 +0,0 @@
.Content {
width: 100%;
height: 100%;
}

View File

@ -1,12 +0,0 @@
.Overlay {
display: flex;
position: absolute;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background-color: rgb(0, 0, 0, 0.35);
}

View File

@ -1,9 +0,0 @@
.PlainText {
margin: 12px 5px 0 12px;
overflow-y: scroll;
overflow-x: scroll;
p {
color: white;
}
}

View File

@ -1,80 +0,0 @@
.SideBar {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
max-width: 300px;
background-color: var(--secondary-color);
gap: 40px;
}
.SideBar_Title {
margin-top: 42px;
line-height: 41px;
font-size: 34px;
max-width: 256px;
max-height: 128px;
text-align: center;
align-self: center;
user-select: none;
}
.SideBar_Title:hover {
cursor: pointer;
}
.SideBar_Buttons {
display: flex;
flex-direction: column;
gap: 15px;
user-select: none;
}
.SideBar_Enter {
display: flex;
width: 100%;
height: 100%;
max-width: 250px;
max-height: 50px;
margin-bottom: 24px;
box-sizing: border-box;
align-self: center;
align-items: center;
border-radius: 10px;
background-color: var(--background-color);
}
.SideBar_Input {
background-color: transparent;
border: none;
color: var(--text-primary-color);
font-size: 20px;
width: 100%;
padding: 11px;
&:focus, &:active {
outline: none;
}
}
.SideBar_Input::placeholder {
color: var(--text-secondary-color);
opacity: 1;
}
.SideBar_Input:disabled {
cursor: not-allowed;
border-radius: 10px ;
background-color: var(--background-color);
}

View File

@ -1,53 +0,0 @@
.Card {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
max-width: 1510px;
border-radius: 15px;
padding: 10px;
box-sizing: border-box;
background-color: var(--primary-color);
}
.Card_Content {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.Card_Header {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
gap: 15px;
align-items: center;
}
.Card_Title {
font-size: 32px;
font-weight: bold;
}
.Card_Alternate {
font-size: 24px;
}
.Card_Description {
color: var(--text-primary-color);
padding-bottom: 5px;
}
.Card_Button {
display: flex;
margin-right: 13px;
align-self: center;
justify-content: center;
}

View File

@ -1,47 +0,0 @@
.Character {
display: flex;
flex-direction: column;
max-width: 96px;
max-height: 135px;
border-radius: 15px;
height: 100%;
width: 100%;
overflow: hidden;
box-sizing: border-box;
&:hover {
cursor: pointer;
transition: 0.1s ease-in-out all;
box-shadow: 0 0 10px 5px var(--primary-color);
}
}
.Character :hover {
cursor: pointer;
}
.Character_Icon {
width: 96px;
height: 96px;
align-self: center;
}
.Character_Label {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--primary-color);
max-width: 100px;
height: 40px;
p {
text-align: center;
margin: 4px;
}
}

View File

@ -1,46 +0,0 @@
.HomeButton {
display: flex;
flex-direction: column;
padding: 20px;
width: min(10vw, 196px);
height: min(20vh, 196px);
background-color: var(--primary-color);
align-items: center;
justify-content: center;
gap: 20px;
border-radius: 10px;
user-select: none;
&:hover {
cursor: pointer;
box-shadow: 0 0 10px 5px var(--accent-color);
scale: 1.01;
}
transition: 0.1s ease-in-out all;
}
.HomeButton:hover {
cursor: pointer;
transition: 0.1s ease-in-out all;
&:hover {
cursor: pointer;
box-shadow: 0 0 10px 5px var(--primary-color);
scale: 1.01;
}
}
.HomeButton_Icon {
max-width: 128px;
max-height: 128px;
}
.HomeButton_Label {
font-size: min(1.3vw, 30px);
text-align: center;
}

View File

@ -1,47 +0,0 @@
.MiniCard {
display: flex;
width: 64px;
height: 64px;
overflow: hidden;
justify-content: center;
transition: 0.1s ease-in-out all;
&:hover {
cursor: pointer;
filter: brightness(0.9);
}
}
.MiniCard_Background {
display: flex;
align-items: center;
max-width: 64px;
max-height: 64px;
border-radius: 10px;
background-color: var(--primary-color);
}
.MiniCard_Icon {
max-width: 64px;
max-height: 64px;
object-fit: scale-down;
border-radius: 10px;
}
.MiniCard_Label {
width: 64px;
max-height: 64px;
text-align: center;
font-size: 12px;
color: var(--text-primary-color);
}
.MiniCard_Info {
position: absolute;
display: flex;
}

View File

@ -1,160 +0,0 @@
.ObjectCard {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 100%;
max-width: 300px;
min-height: 300px;
max-height: 700px;
padding: 20px;
box-sizing: border-box;
border-radius: 10px;
background-color: var(--primary-color);
}
.ObjectCard_Content {
display: flex;
gap: 10px;
flex-direction: column;
}
.ObjectCard_Header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.ObjectCard_Info {
display: flex;
flex-direction: column;
gap: 10px;
:nth-child(1) {
font-weight: bold;
font-size: 20px;
max-width: 170px;
max-height: 60px;
color: var(--text-primary-color);
}
:nth-child(2) {
font-size: 16px;
color: var(--text-primary-color);
}
}
.ObjectCard_Icon {
width: 64px;
height: 64px
}
.ObjectCard_Description {
display: flex;
flex-direction: column;
overflow-y: scroll;
max-width: 250px;
max-height: 460px;
p {
font-size: 14px;
color: var(--text-primary-color);
}
}
.ObjectCard_Actions {
display: flex;
flex-direction: column;
gap: 5px;
padding-top: 10px;
}
.ObjectCard_Counter {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
max-width: 260px;
max-height: 46px;
border-radius: 10px;
padding: 0 13px 0 13px;
box-sizing: border-box;
align-items: center;
background-color: var(--secondary-color);
}
.ObjectCard_Operation {
user-select: none;
display: flex;
width: 30px;
height: 20px;
font-size: 24px;
align-items: center;
justify-content: center;
color: var(--text-primary-color);
background-color: var(--background-color);
}
.ObjectCard_Operation:hover {
cursor: pointer;
}
.ObjectCard_Count {
max-width: 105px;
height: 48px;
font-size: 24px;
text-align: center;
background-color: transparent;
color: var(--text-primary-color);
border: transparent;
}
.ObjectCard_Count:focus {
outline: none;
}
.ObjectCard_Submit {
width: 100%;
height: 46px;
max-width: 260px;
border-radius: 10px;
text-align: center;
justify-content: center;
border: transparent;
font-size: 24px;
color: var(--text-primary-color);
background-color: var(--secondary-color);
user-select: none;
transition: 0.1s ease-in-out all;
}
.ObjectCard_Submit:hover {
cursor: pointer;
scale: 1.05;
}
.ObjectCard_Submit:active {
scale: 0.9;
}

View File

@ -1,117 +0,0 @@
.ServerSettings {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-radius: 10px;
background-color: var(--primary-color);
width: 100%;
height: 100%;
max-width: 620px;
max-height: 400px;
padding: 10px;
box-sizing: border-box;
}
.ServerSettings_Content {
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
gap: 15px;
}
.ServerSettings_Top {
height: 80%;
}
.ServerSettings_Frame {
width: 100%;
height: 100%;
border: 0;
}
.ServerSettings_Title {
font-weight: bold;
font-size: 34px;
text-align: center;
margin-bottom: 15px;
user-select: none;
}
.ServerSettings_Details {
display: flex;
flex-direction: row;
justify-content: space-between;
border-radius: 10px;
background-color: var(--secondary-color);
width: 100%;
height: 100%;
max-width: 590px;
max-height: 50px;
padding: 10px;
box-sizing: border-box;
p {
font-size: 20px;
user-select: none;
}
div {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 5px;
}
input {
border: none;
background: none;
font-size: 18px;
color: var(--text-primary-color);
&:focus, &:active {
outline: none;
}
}
}
.ServerSettings_Authenticate {
font-size: 20px;
border-radius: 10px;
background-color: var(--secondary-color);
width: 100%;
height: 100%;
max-width: 210px;
max-height: 46px;
cursor: pointer;
color: white;
}
.ServerSettings_Save {
font-size: 20px;
border-radius: 10px;
background-color: var(--secondary-color);
width: 100%;
height: 46px;
max-width: 120px;
cursor: pointer;
color: white;
}

View File

@ -1,31 +0,0 @@
.SideBarButton {
display: flex;
flex-direction: row;
gap: 15px;
padding-left: 27px;
height: 64px;
align-items: center;
&:hover {
cursor: pointer;
backdrop-filter: brightness(0.9);
}
transition: 0.2s ease-in-out all;
}
.SideBarButton_Icon {
max-width: 64px;
max-height: 64px;
}
.SideBarButton_Label {
font-size: 22px;
line-height: 29px;
font-style: normal;
max-width: 220px;
}

View File

@ -1,54 +0,0 @@
.NormalQuest {
display: flex;
align-items: center;
justify-content: space-between;
width: 431px;
height: 100%;
min-width: 100px;
min-height: 25px;
max-height: 53px;
background-color: var(--quest-unselected);
padding: 11px 20px 11px 20px;
box-sizing: border-box;
p {
user-select: none;
cursor: pointer;
}
}
.NormalQuest[datatype="right"] {
margin-left: auto;
margin-right: 0;
}
.NormalQuest:hover {
background-color: var(--quest-selected);
p {
color: var(--qt-selected);
}
}
.NormalQuest_Info {
display: flex;
flex-direction: column;
:nth-child(1) {
font-size: 16px;
color: var(--qt-unselected);
}
:nth-child(2) {
font-size: 13px;
color: var(--qt2-unselected);
}
}
.NormalQuest_Icon {
font-size: 16px;
color: var(--quest-accent);
}

View File

@ -1,65 +0,0 @@
.PrimaryQuest {
display: flex;
flex-direction: column;
height: min-content;
}
.PrimaryQuest_List {
display: flex;
flex-direction: column;
width: 97%;
margin-left: auto;
margin-right: 5px;
gap: 8px;
padding: 8px 8px 8px 8px;
background-color: var(--primary-color);
}
/* Trigger related CSS. */
.Trigger {
display: flex;
flex-direction: row;
gap: 10px;
padding: 10px 10px 10px 10px;
box-sizing: border-box;
width: 461px;
height: 100%;
min-width: 100px;
min-height: 25px;
max-height: 60px;
background-color: var(--pq-bg);
p {
user-select: none;
cursor: pointer;
}
}
.Trigger_Icon {
font-size: 20px;
padding-top: 5px;
color: var(--pq-text);
}
.Trigger_Info {
display: flex;
flex-direction: column;
:nth-child(1) {
font-size: 16px;
color: var(--pq-text);
}
:nth-child(2) {
font-size: 14px;
color: var(--pq-text2);
}
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 985 B

View File

@ -1,20 +0,0 @@
import React from "react";
import { createRoot } from "react-dom/client";
import * as data from "@backend/data";
import * as events from "@backend/events";
import * as server from "@backend/server";
import App from "@ui/App";
// Call initial setup functions.
data.setup();
events.setup();
server.setup();
// Render the application.
createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -1,56 +0,0 @@
import React from "react";
import SideBar from "@views/SideBar";
import Content from "@views/Content";
import Overlay from "@views/Overlay";
import PlainText from "@views/PlainText";
import type { Page } from "@backend/types";
import { isPage } from "@backend/types";
import "@css/App.scss";
import "@css/Text.scss";
// Based on the design at: https://www.figma.com/file/PDeAVDkTDF5vvUGGdaIZ39/GM-Handbook.
// Currently designed by: Magix.
interface IState {
initial: Page | null;
plain: boolean;
}
class App extends React.Component<{}, IState> {
constructor(props: any) {
super(props);
// Check if the window's href is a page.
let targetPage = null;
const page = window.location.href.split("/").pop();
if (page != undefined && page != "") {
// Convert the page to a Page type.
const pageName = page.charAt(0).toUpperCase() + page.slice(1);
// Check if the page is a valid page.
if (isPage(pageName)) targetPage = pageName as Page;
}
this.state = {
initial: targetPage as Page | null,
plain: false
};
}
render() {
return (
<div className={"App"}>
<SideBar />
{this.state.plain ? <PlainText /> : <Content initial={this.state.initial} />}
<Overlay />
</div>
);
}
}
export default App;

View File

@ -1,46 +0,0 @@
import React from "react";
import emitter from "@backend/events";
interface IProps {
initial: boolean;
event: string;
text1: string;
text2: string;
}
interface IState {
state: boolean;
}
class TextState extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
state: false
};
}
/**
* Updates the current state.
* @private
*/
private update(state: boolean): void {
this.setState({ state });
}
componentDidMount(): void {
emitter.on(this.props.event, this.update.bind(this));
}
componentWillUnmount(): void {
emitter.off(this.props.event, this.update);
}
render() {
return this.state.state ? this.props.text2 : this.props.text1;
}
}
export default TextState;

View File

@ -1,102 +0,0 @@
import React from "react";
import { List as _List, ListProps, ListRowProps } from "react-virtualized/dist/es/List";
import { AutoSizer as _AutoSizer, AutoSizerProps } from "react-virtualized/dist/es/AutoSizer";
const List = _List as unknown as React.FC<ListProps>;
const AutoSizer = _AutoSizer as unknown as React.FC<AutoSizerProps>;
import "@css/components/VirtualizedGrid.scss";
interface IProps<T> {
list: T[];
render: (item: T) => React.ReactNode;
itemHeight: number;
itemsPerRow?: number;
gap?: number;
itemGap?: number;
}
interface IState {
scrollTop: number;
itemsPerRow: number;
}
class VirtualizedGrid<T> extends React.Component<IProps<T>, IState> {
constructor(props: IProps<T>) {
super(props);
this.state = {
scrollTop: 0,
itemsPerRow: 10
};
}
/**
* Renders a row of items.
*/
private rowRender(props: ListRowProps): React.ReactNode {
const items: React.ReactNode[] = [];
// Calculate the items to render.
const perRow = this.state.itemsPerRow ?? 10;
for (let i = 0; i < perRow; i++) {
const itemIndex = props.index * perRow + i;
if (itemIndex < this.props.list.length) {
items.push(this.props.render(this.props.list[itemIndex]));
}
}
return (
<div
key={props.key}
style={{
...props.style,
gap: this.props.itemGap ?? 0
}}
className={"GridRow"}
>
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
<div style={{ height: this.props.gap ?? 0 }} />
</div>
);
}
componentDidMount() {
this.setState({
itemsPerRow: Math.floor((window.innerWidth - 650) / (this.props.itemHeight + (this.props.gap ?? 0)))
});
window.addEventListener("resize", () => {
this.setState({
itemsPerRow: Math.floor((window.innerWidth - 650) / (this.props.itemHeight + (this.props.gap ?? 0)))
});
});
}
render() {
const { list, itemHeight } = this.props;
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height - 150}
width={width}
rowHeight={itemHeight + (this.props.gap ?? 0)}
rowCount={Math.ceil(list.length / (this.state.itemsPerRow ?? 10))}
rowRenderer={this.rowRender.bind(this)}
scrollTop={this.state.scrollTop}
onScroll={(e) => this.setState({ scrollTop: e.scrollTop })}
/>
)}
</AutoSizer>
);
}
}
export default VirtualizedGrid;

View File

@ -1,39 +0,0 @@
import React from "react";
import Character from "@app/ui/widgets/Character";
import type { Avatar } from "@backend/types";
import { listAvatars } from "@backend/data";
import { grantAvatar } from "@backend/server";
import "@css/pages/AvatarsPage.scss";
class AvatarsPage extends React.PureComponent {
/**
* Grants the avatar to the user.
*
* @param avatar The avatar to grant.
* @private
*/
private async grantAvatar(avatar: Avatar): Promise<void> {
console.log(await grantAvatar(avatar.id));
}
render() {
return (
<div className={"AvatarsPage"}>
<h1 className={"AvatarsPage_Title"}>Characters</h1>
<div className={"AvatarsPage_List"}>
{listAvatars().map((avatar) =>
avatar.id > 11000000 ? undefined : (
<Character key={avatar.id} data={avatar} onClick={this.grantAvatar.bind(this, avatar)} />
)
)}
</div>
</div>
);
}
}
export default AvatarsPage;

View File

@ -1,32 +0,0 @@
import React from "react";
import Card from "@widgets/Card";
import { listCommands } from "@backend/data";
import "@css/pages/CommandsPage.scss";
class CommandsPage extends React.PureComponent {
render() {
return (
<div className={"CommandsPage"}>
<h1 className={"CommandsPage_Title"}>Commands</h1>
<div className={"CommandsPage_List"}>
{listCommands().map((command) => (
<Card
key={command.name[0]}
title={command.name[0]}
alternate={
command.name.length == 1 ? undefined : `(aka /${command.name.slice(1).join(", /")})`
}
description={command.description}
/>
))}
</div>
</div>
);
}
}
export default CommandsPage;

View File

@ -1,153 +0,0 @@
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;

View File

@ -1,75 +0,0 @@
import React from "react";
import HomeButton from "@widgets/HomeButton";
import { ReactComponent as DiscordLogo } from "@icons/discord.svg";
import Icon_Version_Highlights from "@assets/Icon_Version_Highlights.webp";
import Icon_Character_Lumine from "@assets/Icon_Character_Lumine.webp";
import Icon_Inventory from "@assets/Icon_Inventory.webp";
import Icon_Tutorial_Monster from "@assets/Icon_Tutorial_Monster.webp";
import Icon_Map from "@assets/Icon_Map.webp";
import Icon_Quests from "@assets/Icon_Quests.webp";
import Icon_Achievements from "@assets/Icon_Achievements.webp";
import { openUrl } from "@app/utils";
import "@css/pages/HomePage.scss";
class HomePage extends React.Component<any, any> {
constructor(props: any) {
super(props);
}
render() {
return (
<div className={"HomePage"}>
<div className={"HomePage_Top"}>
<h1 className={"HomePage_Title"}>Welcome back, Traveler~</h1>
<div className={"HomePage_Buttons"}>
<HomeButton name={"Commands"} anchor={"Commands"} icon={Icon_Version_Highlights} />
<HomeButton name={"Characters"} anchor={"Avatars"} icon={Icon_Character_Lumine} />
<HomeButton name={"Items"} anchor={"Items"} icon={Icon_Inventory} />
<HomeButton name={"Entities"} anchor={"Entities"} icon={Icon_Tutorial_Monster} />
<HomeButton name={"Scenes"} anchor={"Scenes"} icon={Icon_Map} />
<HomeButton name={"Quests"} anchor={"Quests"} icon={Icon_Quests} />
<HomeButton name={"Achievements"} anchor={"Achievements"} icon={Icon_Achievements} />
</div>
</div>
<div className={"HomePage_Bottom"}>
<div className={"HomePage_Box HomePage_Disclaimer"}>
<p>
<b>This tool is not affiliated with HoYoverse.</b>
<br />
Genshin Impact, game content and materials are
<br />
trademarks and copyrights of HoYoverse.
</p>
<div className={"HomePage_Discord"} onClick={() => openUrl("https://discord.gg/grasscutter")}>
<DiscordLogo />
<p>Join the Community!</p>
</div>
</div>
<div className={"HomePage_Text"}>
<div className={"HomePage_Credits"}>
<p>Credits</p>
<p>(hover to see info)</p>
</div>
<div className={"HomePage_Links"}>
<a href={"https://paimon.moe"}>paimon.moe</a>
<a href={"https://gitlab.com/Dimbreath/AnimeGameData"}>Anime Game Data</a>
<a href={"https://genshin-impact.fandom.com"}>Genshin Impact Wiki</a>
</div>
</div>
</div>
</div>
);
}
}
export default HomePage;

View File

@ -1,157 +0,0 @@
import React, { ChangeEvent } from "react";
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, itemIcon } from "@app/utils";
import "@css/pages/ItemsPage.scss";
interface IState {
filters: ItemCategory[];
search: string;
selected: ItemType | null;
selectedInfo: ItemInfo | null;
}
class ItemsPage extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
filters: [],
search: "",
selected: null,
selectedInfo: null
};
}
/**
* Gets the items to render.
* @private
*/
private getItems(): ItemType[] {
let items: ItemType[] = [];
// Add items based on filters.
const filters = this.state.filters;
if (filters.length == 0) {
items = getItems();
} else {
for (const filter of filters) {
// Add items from the category.
items = items.concat(sortedItems[filter]);
// Remove duplicate items.
items = items.filter((item, index) => {
return items.indexOf(item) == index;
});
}
}
// Filter out items that don't match the search.
const search = this.state.search.toLowerCase();
if (search != "") {
items = items.filter((item) => {
return item.name.toLowerCase().includes(search);
});
}
return items;
}
/**
* Invoked when the search input changes.
*
* @param event The event.
* @private
*/
private onChange(event: ChangeEvent<HTMLInputElement>): void {
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;
}
/**
* Sets the selected item.
*
* @param item The item.
* @private
*/
private async setSelectedItem(item: ItemType): Promise<void> {
let data: ItemInfo | null = null;
try {
data = await fetchItemData(item);
} catch {}
this.setState({
selected: item,
selectedInfo: data
});
}
render() {
const items = this.getItems();
return (
<div className={"ItemsPage"}>
<div className={"ItemsPage_Content"}>
<div className={"ItemsPage_Header"}>
<h1 className={"ItemsPage_Title"}>Items</h1>
<div className={"ItemsPage_Search"}>
<input
type={"text"}
className={"ItemsPage_Input"}
placeholder={"Search..."}
onChange={this.onChange.bind(this)}
/>
</div>
</div>
{items.length > 0 ? (
<VirtualizedGrid
list={items.filter((item) => this.showItem(item))}
itemHeight={64}
itemsPerRow={18}
gap={5}
itemGap={5}
render={(item) => (
<MiniCard
key={item.id}
data={item}
icon={itemIcon(item)}
onClick={() => this.setSelectedItem(item)}
/>
)}
/>
) : undefined}
</div>
<div className={"ItemsPage_Card"}>
<ItemCard item={this.state.selected} info={this.state.selectedInfo} />
</div>
</div>
);
}
}
export default ItemsPage;

View File

@ -1,50 +0,0 @@
import React from "react";
import Tree, { RawNodeDatum } from "react-d3-tree";
import PrimaryQuest from "@widgets/quest/PrimaryQuest";
import "@css/pages/QuestsPage.scss";
const defaultTree: RawNodeDatum = {
name: "No Quest Selected",
attributes: {
questId: -1
},
children: []
};
interface IState {
tree: RawNodeDatum | null;
}
class QuestsPage extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
tree: null
};
}
render() {
return (
<div className={"QuestsPage"}>
<div className={"QuestsPage_Selector"}>
<PrimaryQuest
quest={{
id: 351,
title: "Across the Sea"
}}
/>
</div>
<div className={"QuestsPage_Tree"}>
<Tree data={this.state.tree ?? defaultTree} />
</div>
</div>
);
}
}
export default QuestsPage;

View File

@ -1,78 +0,0 @@
import React from "react";
import Card from "@widgets/Card";
import { SceneType } from "@backend/types";
import { getScenes } from "@backend/data";
import { connected, teleportTo } from "@backend/server";
import { action } from "@backend/commands";
import { copyToClipboard } from "@app/utils";
import "@css/pages/ScenesPage.scss";
/**
* Converts a scene type to a string.
*
* @param type The scene type.
*/
function sceneTypeToString(type: SceneType): string {
switch (type) {
default:
return "Unknown";
case SceneType.None:
return "None";
case SceneType.World:
return "World";
case SceneType.Activity:
return "Activity";
case SceneType.Dungeon:
return "Dungeon";
case SceneType.Room:
return "Room";
case SceneType.HomeRoom:
return "Home Room";
case SceneType.HomeWorld:
return "Home World";
}
}
class ScenesPage extends React.PureComponent {
/**
* Teleports the player to the specified scene.
* @private
*/
private async teleport(scene: number): Promise<void> {
if (connected) {
await teleportTo(scene);
} else {
await copyToClipboard(action.teleport(scene));
}
}
render() {
return (
<div className={"ScenesPage"}>
<h1 className={"ScenesPage_Title"}>Scenes</h1>
<div className={"ScenesPage_List"}>
{getScenes().map((scene) => (
<Card
key={scene.id}
title={scene.identifier}
alternate={`ID: ${scene.id} | ${sceneTypeToString(scene.type)}`}
button={
<button className={"ScenesPage_Button"} onClick={() => this.teleport(scene.id)}>
Teleport
</button>
}
rightOffset={13}
height={75}
/>
))}
</div>
</div>
);
}
}
export default ScenesPage;

View File

@ -1,73 +0,0 @@
import React from "react";
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 QuestsPage from "@pages/QuestsPage";
import type { Page } from "@backend/types";
import { addNavListener, removeNavListener } from "@backend/events";
import "@css/views/Content.scss";
interface IProps {
initial?: Page | null;
}
interface IState {
current: Page;
}
class Content extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
current: props.initial ?? "Home"
};
}
/**
* Navigates to the specified page.
*
* @param page The page to navigate to.
* @private
*/
private navigate(page: Page): void {
this.setState({ current: page });
}
componentDidMount() {
addNavListener(this.navigate.bind(this));
}
componentWillUnmount() {
removeNavListener(this.navigate.bind(this));
}
render() {
switch (this.state.current) {
default:
return undefined;
case "Home":
return <HomePage />;
case "Commands":
return <CommandsPage />;
case "Avatars":
return <AvatarsPage />;
case "Items":
return <ItemsPage />;
case "Entities":
return <EntitiesPage />;
case "Scenes":
return <ScenesPage />;
case "Quests":
return <QuestsPage />;
}
}
}
export default Content;

View File

@ -1,57 +0,0 @@
import React from "react";
import ServerSettings from "@widgets/ServerSettings";
import type { Overlays } from "@backend/types";
import "@css/views/Overlay.scss";
import events from "@backend/events";
interface IState {
page: Overlays;
}
class Overlay extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
page: "None"
};
}
/**
* Sets the page to display.
*
* @param page The page to display.
*/
private setPage(page: Overlays): void {
this.setState({ page });
}
/**
* Gets the page to display.
*/
private getPage(): React.ReactNode {
switch (this.state.page) {
default:
return undefined;
case "ServerSettings":
return <ServerSettings />;
}
}
componentDidMount() {
events.on("overlay", this.setPage.bind(this));
}
componentWillUnmount() {
events.off("overlay", this.setPage.bind(this));
}
render() {
return this.state.page != "None" ? <div className={"Overlay"}>{this.getPage()}</div> : undefined;
}
}
export default Overlay;

View File

@ -1,171 +0,0 @@
import React from "react";
import {
listCommands,
listAvatars,
getItems,
getEntities,
getScenes,
listQuests,
getMainQuestFor
} from "@backend/data";
import "@css/views/PlainText.scss";
class PlainText extends React.PureComponent {
/**
* Creates a paragraph of commands.
* @private
*/
private getCommands(): React.ReactNode {
return (
<>
{listCommands().map((command) => (
<p key={command.name[0]}>{`${command.name[0]} : ${command.description}`}</p>
))}
</>
);
}
/**
* Creates a paragraph of avatars.
* @private
*/
private getAvatars(): React.ReactNode {
return (
<>
{listAvatars()
.sort((a, b) => a.id - b.id)
.map((avatar) => (
<p key={avatar.id}>{`${avatar.id} : ${avatar.name}`}</p>
))}
</>
);
}
/**
* Creates a paragraph of items.
* @private
*/
private getItems(): React.ReactNode {
return (
<>
{getItems()
.sort((a, b) => a.id - b.id)
.map((item) => (
<p key={item.id}>{`${item.id} : ${item.name}`}</p>
))}
</>
);
}
/**
* Creates a paragraph of monsters.
* @private
*/
private getMonsters(): React.ReactNode {
return (
<>
{getEntities()
.sort((a, b) => a.id - b.id)
.map((entity) => (
<p key={entity.id}>{`${entity.id} : ${entity.name}`}</p>
))}
</>
);
}
/**
* Creates a paragraph of scenes.
* @private
*/
private getScenes(): React.ReactNode {
return (
<>
{getScenes()
.sort((a, b) => a.id - b.id)
.map((scene) => (
<p key={scene.id}>{`${scene.id} : ${scene.identifier} [${scene.type}]`}</p>
))}
</>
);
}
/**
* Creates a paragraph of quests.
* @private
*/
private getQuests(): React.ReactNode {
return (
<>
{listQuests()
.sort((a, b) => a.id - b.id)
.map((quest) => (
<p key={quest.id}>{`${quest.id} : ${getMainQuestFor(quest)?.title ?? "Unknown"} - ${
quest.description
}`}</p>
))}
</>
);
}
render() {
return (
<div className={"PlainText"}>
<p>
// Grasscutter 3.6.0 GM Handbook
<br />
// Generated by the HTML GM Handbook.
<br />
<br />
<br />
// Commands
</p>
{this.getCommands()}
<p>
<br />
<br />
// Avatars
</p>
{this.getAvatars()}
<p>
<br />
<br />
// Items
</p>
{this.getItems()}
<p>
<br />
<br />
// Monsters
</p>
{this.getMonsters()}
<p>
<br />
<br />
// Scenes
</p>
{this.getScenes()}
<p>
<br />
<br />
// Quests
</p>
{this.getQuests()}
</div>
);
}
}
export default PlainText;

View File

@ -1,123 +0,0 @@
import React, { ChangeEvent } from "react";
import SideBarButton from "@app/ui/widgets/SideBarButton";
import Icon_Version_Highlights from "@assets/Icon_Version_Highlights.webp";
import Icon_Character_Lumine from "@assets/Icon_Character_Lumine.webp";
import Icon_Inventory from "@assets/Icon_Inventory.webp";
import Icon_Tutorial_Monster from "@assets/Icon_Tutorial_Monster.webp";
import Icon_Map from "@assets/Icon_Map.webp";
import Icon_Quests from "@assets/Icon_Quests.webp";
import Icon_Achievements from "@assets/Icon_Achievements.webp";
import events, { navigate } from "@backend/events";
import { targetPlayer, lockedPlayer, setTargetPlayer } from "@backend/server";
import "@css/views/SideBar.scss";
interface IState {
uid: string | null;
uidLocked: boolean;
}
class SideBar extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
uid: targetPlayer > 0 ? targetPlayer.toString() : null,
uidLocked: lockedPlayer
};
}
/**
* Invoked when the player's UID changes.
* @private
*/
private updateUid(): void {
this.setState({
uid: targetPlayer > 0 ? targetPlayer.toString() : null,
uidLocked: lockedPlayer
});
}
/**
* Invoked when the UID input changes.
*
* @param event The event.
* @private
*/
private onChange(event: ChangeEvent<HTMLInputElement>): void {
const input = event.target.value;
const uid = input == "" ? null : input;
if (uid && uid.length > 10) return;
setTargetPlayer(parseInt(uid ?? "0"));
}
/**
* Invoked when the UID input is right-clicked.
*
* @param event The event.
* @private
*/
private onRightClick(event: React.MouseEvent<HTMLInputElement, MouseEvent>): void {
// Remove focus from the input.
event.currentTarget.blur();
event.preventDefault();
// Open the server settings overlay.
events.emit("overlay", "ServerSettings");
}
componentDidMount() {
events.on("connected", this.updateUid.bind(this));
}
componentWillUnmount() {
events.off("connected", this.updateUid.bind(this));
}
render() {
return (
<div className={"SideBar"}>
<h1 className={"SideBar_Title"} onClick={() => navigate("Home")}>
The Ultimate Anime Game Handbook
</h1>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
height: "100%"
}}
>
<div className={"SideBar_Buttons"}>
<SideBarButton name={"Commands"} anchor={"Commands"} icon={Icon_Version_Highlights} />
<SideBarButton name={"Characters"} anchor={"Avatars"} icon={Icon_Character_Lumine} />
<SideBarButton name={"Items"} anchor={"Items"} icon={Icon_Inventory} />
<SideBarButton name={"Entities"} anchor={"Entities"} icon={Icon_Tutorial_Monster} />
<SideBarButton name={"Scenes"} anchor={"Scenes"} icon={Icon_Map} />
<SideBarButton name={"Quests"} anchor={"Quests"} icon={Icon_Quests} />
<SideBarButton name={"Achievements"} anchor={"Achievements"} icon={Icon_Achievements} />
</div>
<div className={"SideBar_Enter"}>
<input
type={"text"}
className={"SideBar_Input"}
placeholder={"Enter UID..."}
value={this.state.uid ?? undefined}
disabled={this.state.uidLocked}
onChange={this.onChange.bind(this)}
onContextMenu={this.onRightClick.bind(this)}
/>
</div>
</div>
</div>
);
}
}
export default SideBar;

View File

@ -1,67 +0,0 @@
import React from "react";
import "@css/widgets/Card.scss";
interface IProps {
title: string;
alternate?: string;
description?: string | string[];
height?: number | string;
button?: React.ReactNode;
rightOffset?: number;
onClick?: () => void;
onOver?: () => void;
onOut?: () => void;
}
class Card extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<div
className={"Card"}
onClick={this.props.onClick}
onMouseOver={this.props.onOver}
onMouseOut={this.props.onOut}
style={{
height: this.props.height,
cursor: this.props.onClick ? "pointer" : undefined
}}
>
<div className={"Card_Content"}>
<div className={"Card_Header"}>
<p className={"Card_Title"}>{this.props.title}</p>
{this.props.alternate && <p className={"Card_Alternate"}>{this.props.alternate}</p>}
</div>
<div style={{ alignItems: "center" }}>
{this.props.description ? (
Array.isArray(this.props.description) ? (
this.props.description.map((line, index) => (
<p className={"Card_Description"} key={index}>
{line}
</p>
))
) : (
<p className={"Card_Description"}>{this.props.description}</p>
)
) : undefined}
</div>
</div>
{this.props.button ? (
<div className={"Card_Button"} style={{ marginRight: this.props.rightOffset ?? 0 }}>
{this.props.button}
</div>
) : undefined}
</div>
);
}
}
export default Card;

View File

@ -1,56 +0,0 @@
import React from "react";
import type { Avatar } from "@backend/types";
import { colorFor, formatAvatarName } from "@app/utils";
import "@css/widgets/Character.scss";
// Image base URL: https://paimon.moe/images/characters/(name).png
const ignored = [
10000001 // Kate
];
const nameSwitch: { [key: number]: string } = {
10000005: "Lumine",
10000007: "Aether"
};
interface IProps {
data: Avatar;
onClick?: () => void;
}
class Character extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
const { name, quality, id } = this.props.data;
const qualityColor = colorFor(quality);
// Check if the avatar is blacklisted.
if (ignored.includes(id)) return undefined;
const characterName = nameSwitch[id] ?? name;
return (
<div className={"Character"} onClick={this.props.onClick}>
<img
className={"Character_Icon"}
alt={name}
src={`https://paimon.moe/images/characters/${formatAvatarName(name, id)}.png`}
style={{ backgroundColor: `var(${qualityColor})` }}
/>
<div className={"Character_Label"}>
<p style={{ fontSize: characterName.length >= 10 ? 13 : 17 }}>{characterName}</p>
</div>
</div>
);
}
}
export default Character;

View File

@ -1,199 +0,0 @@
import React from "react";
import type { Entity as EntityType, EntityInfo } from "@backend/types";
import { copyToClipboard, entityIcon, notNaN } from "@app/utils";
import { connected, spawnEntity } from "@backend/server";
import { spawn } from "@backend/commands";
import "@css/widgets/ObjectCard.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;
level: number | string;
showingCount: boolean;
}
const defaultState = {
icon: true,
count: 1,
level: 1,
showingCount: true
};
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>) {
let value = event.target.value;
// Remove non-numeric characters.
value = value.replace(/[^0-9]/g, "");
let numeric = parseInt(value);
if (isNaN(numeric) && value.length > 1) return;
// Check if the value should be a level.
if (!this.state.showingCount && numeric > 200) numeric = 200;
const updated: any = this.state.showingCount ? { count: numeric } : { level: numeric };
this.setState(updated);
}
/**
* 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 value = this.state.showingCount ? this.state.count : this.state.level;
if (value === "") value = 1;
if (typeof value == "string") value = parseInt(value);
if (value < 1) value = 1;
let increment = 1;
if (!positive) increment = -1;
if (multiple) increment *= 10;
value = Math.max(1, value + increment);
// Check if the value should be a level.
if (!this.state.showingCount && value > 200) value = 200;
const updated: any = this.state.showingCount ? { count: value } : { level: value };
this.setState(updated);
}
/**
* Summons the entity at the connected player's position.
* @private
*/
private async summonAtPlayer(): Promise<void> {
const entity = this.props.entity?.id ?? 21010101;
const amount = typeof this.state.count == "string" ? parseInt(this.state.count) : this.state.count;
const level = typeof this.state.level == "string" ? parseInt(this.state.level) : this.state.level;
if (connected) {
await spawnEntity(entity, amount, level);
} else {
await copyToClipboard(spawn.monster(entity, amount, level));
}
}
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={"ObjectCard"}>
<div className={"ObjectCard_Content"}>
<div className={"ObjectCard_Header"}>
<div className={"ObjectCard_Info"}>
<p>{data?.name ?? entity.name}</p>
<p>{data?.type ?? ""}</p>
</div>
{this.state.icon && (
<img
className={"ObjectCard_Icon"}
alt={entity.name}
src={entityIcon(entity)}
onError={() => this.setState({ icon: false })}
/>
)}
</div>
<div className={"ObjectCard_Description"}>{toDescription(data?.description)}</div>
</div>
<div className={"ObjectCard_Actions"}>
<div className={"ObjectCard_Counter"}>
<div
onClick={() => this.addCount(false, false)}
onContextMenu={(e) => {
e.preventDefault();
this.addCount(false, true);
}}
className={"ObjectCard_Operation"}
>
-
</div>
<input
type={"text"}
value={
this.state.showingCount
? `x${notNaN(this.state.count)}`
: `Lv${notNaN(this.state.level)}`
}
className={"ObjectCard_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={"ObjectCard_Operation"}
>
+
</div>
</div>
<button
className={"ObjectCard_Submit"}
onClick={this.summonAtPlayer.bind(this)}
onContextMenu={(e) => {
e.preventDefault();
this.setState({ showingCount: !this.state.showingCount });
}}
>
Summon
</button>
</div>
</div>
) : undefined;
}
}
export default EntityCard;

View File

@ -1,45 +0,0 @@
import React from "react";
import type { Page } from "@backend/types";
import { navigate } from "@backend/events";
import "@css/widgets/HomeButton.scss";
interface IProps {
name: string;
icon: string;
anchor: Page;
}
class HomeButton extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
/**
* Redirects the user to the specified anchor.
* @private
*/
private redirect(): void {
navigate(this.props.anchor);
}
/**
* Checks if this component should be showed.
*/
private shouldShow(): boolean {
return !((window as any).hide as string[]).includes(this.props.anchor.toLowerCase());
}
render() {
return this.shouldShow() ? (
<div className={"HomeButton"} onClick={() => this.redirect()}>
<img className={"HomeButton_Icon"} src={this.props.icon} alt={this.props.name} />
<p className={"HomeButton_Label"}>{this.props.name}</p>
</div>
) : undefined;
}
}
export default HomeButton;

View File

@ -1,179 +0,0 @@
import React from "react";
import TextState from "@components/TextState";
import type { Item as ItemType, ItemInfo } from "@backend/types";
import { itemTypeToString } from "@backend/types";
import { copyToClipboard, itemIcon } from "@app/utils";
import { connected, giveItem } from "@backend/server";
import { give } from "@backend/commands";
import "@css/widgets/ObjectCard.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 {
item: ItemType | null;
info: ItemInfo | null;
}
interface IState {
icon: boolean;
count: number | string;
}
const defaultState = {
icon: true,
count: 1
};
class ItemCard 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 item.
*
* @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 });
}
/**
* Adds the item to the player's connected inventory.
* @private
*/
private async addToInventory(): Promise<void> {
const item = this.props.item?.id ?? 102;
const amount = typeof this.state.count == "string" ? parseInt(this.state.count) : this.state.count;
if (connected) {
await giveItem(item, amount);
} else {
await copyToClipboard(give.basic(item, amount));
}
}
componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>, snapshot?: any) {
if (this.props.item != prevProps.item) {
this.setState(defaultState);
}
}
render() {
const { item, info } = this.props;
const data = info?.data;
return item ? (
<div className={"ObjectCard"}>
<div className={"ObjectCard_Content"}>
<div className={"ObjectCard_Header"}>
<div className={"ObjectCard_Info"}>
<p>{data?.name ?? item.name}</p>
<p>{data?.type ?? itemTypeToString(item.type)}</p>
</div>
{this.state.icon && (
<img
className={"ObjectCard_Icon"}
alt={item.name}
src={itemIcon(item)}
onError={() => this.setState({ icon: false })}
/>
)}
</div>
<div className={"ObjectCard_Description"}>{toDescription(data?.description)}</div>
</div>
<div className={"ObjectCard_Actions"}>
<div className={"ObjectCard_Counter"}>
<div
onClick={() => this.addCount(false, false)}
onContextMenu={(e) => {
e.preventDefault();
this.addCount(false, true);
}}
className={"ObjectCard_Operation"}
>
-
</div>
<input
type={"text"}
value={this.state.count}
className={"ObjectCard_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={"ObjectCard_Operation"}
>
+
</div>
</div>
<button className={"ObjectCard_Submit"} onClick={this.addToInventory.bind(this)}>
<TextState
initial={connected}
event={"connected"}
text1={"Copy Command"}
text2={"Add to Inventory"}
/>
</button>
</div>
</div>
) : undefined;
}
}
export default ItemCard;

View File

@ -1,109 +0,0 @@
import React from "react";
import "@css/widgets/MiniCard.scss";
interface IProps {
data: { name: string };
icon: string;
onClick?: () => void;
}
interface IState {
popout: boolean;
icon: boolean;
loaded: boolean;
}
class MiniCard extends React.Component<IProps, IState> {
loading: number | any;
containerRef: React.RefObject<HTMLDivElement>;
textRef: React.RefObject<HTMLDivElement>;
constructor(props: IProps) {
super(props);
this.state = {
popout: false,
icon: true,
loaded: false
};
this.containerRef = React.createRef();
this.textRef = React.createRef();
}
/**
* Replaces the icon with the item's name.
* @private
*/
private replaceIcon(): void {
this.setState({ icon: false, loaded: false });
}
private forceReplace(): void {
if (!this.state.loaded) this.replaceIcon();
}
/**
* Adjusts the font size of the text to fit the container.
* @private
*/
private adjustFontSize() {
const container = this.containerRef.current;
const text = this.textRef.current;
if (!container || !text) {
return;
}
const containerWidth = container.offsetWidth;
const textWidth = text.scrollWidth;
const fontSize = parseFloat(window.getComputedStyle(text).fontSize);
const availableWidth = containerWidth - 10;
const scaleFactor = availableWidth / textWidth;
if (scaleFactor < 1) {
const newFontSize = fontSize * scaleFactor;
text.style.fontSize = newFontSize + "px";
}
}
componentDidMount() {
this.loading = setTimeout(this.forceReplace.bind(this), 1e3);
this.adjustFontSize();
}
componentWillUnmount() {
clearTimeout(this.loading);
this.loading = null;
}
render() {
return (
<div className={"MiniCard"} onClick={this.props.onClick}>
<div className={"MiniCard_Background"} ref={this.containerRef}>
{this.state.icon && (
<img
className={"MiniCard_Icon"}
alt={this.props.data.name}
src={this.props.icon}
onError={this.replaceIcon.bind(this)}
onLoad={() => this.setState({ loaded: true })}
/>
)}
{(!this.state.loaded || !this.state.icon) && (
<p className={"MiniCard_Label"} ref={this.textRef}>
{this.props.data.name}
</p>
)}
</div>
</div>
);
}
}
export default MiniCard;

View File

@ -1,183 +0,0 @@
import React from "react";
import emitter from "@backend/events";
import { targetPlayer, address, port, setServerDetails, url, setTargetPlayer } from "@backend/server";
import { getWindowDetails } from "@app/utils";
import "@css/widgets/ServerSettings.scss";
interface IState {
webview: boolean;
address: string;
port: number;
}
class ServerSettings extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
webview: false,
address: address,
port: Number(port)
};
}
componentDidMount() {
window.addEventListener("keyup", this.escapeListener.bind(this));
}
componentWillUnmount() {
window.removeEventListener("keyup", this.escapeListener.bind(this));
window.removeEventListener("message", this.handleAuthentication.bind(this));
}
/**
* Invoked when the escape key is pressed.
*
* @param e The keyboard event.
* @private
*/
private escapeListener(e: KeyboardEvent): void {
if (e.key === "Escape") {
// Hide the overlay.
emitter.emit("overlay", "None");
}
}
/**
* Invoked when the component tries to authenticate.
* @private
*/
private authenticate(): void {
setServerDetails(this.state.address, this.state.port).then(() => {
this.setState({ webview: true });
});
// Add the event listener for authentication.
window.addEventListener("message", this.handleAuthentication.bind(this));
}
/**
* Finishes the authentication process.
*
* @param e The message event.
* @private
*/
private handleAuthentication(e: MessageEvent): void {
const data = e.data; // The data sent from the server.
if (data == null) return; // If the data is null, return.
// Check if the data is an object.
if (typeof data != "object") return;
// Get the data type.
const type = data["type"] ?? null;
if (type != "handbook-auth") return;
// Get the data.
const uid = data["uid"] ?? null;
const token = data["token"] ?? null;
// Hide the overlay.
emitter.emit("overlay", "None");
// Set the token and user ID.
setTargetPlayer(Number(uid), token);
}
/**
* Invoked when the save button is clicked.
* @private
*/
private save(): void {
// Hide the overlay.
emitter.emit("overlay", "None");
// Save the server settings.
setServerDetails(this.state.address, this.state.port.toString());
}
render() {
const { disable } = getWindowDetails();
return (
<div className={"ServerSettings"}>
{this.state.webview ? (
<iframe
className={"ServerSettings_Frame"}
src={`${url()}/handbook/authenticate?uid=${targetPlayer}`}
/>
) : (
<>
<div className={"ServerSettings_Content ServerSettings_Top"}>
<h1 className={"ServerSettings_Title"}>Server Settings</h1>
<div
className={"ServerSettings_Details"}
style={{
opacity: disable ? 0.5 : 1,
cursor: disable ? "not-allowed" : "default",
userSelect: disable ? "none" : "auto"
}}
>
<div>
<p>Address:</p>
<input
type={"text"}
value={this.state.address}
onChange={(e) => {
const target = e.target as HTMLInputElement;
const value = target.value;
this.setState({ address: value });
}}
disabled={disable}
style={{
cursor: disable ? "not-allowed" : "text",
userSelect: disable ? "none" : "auto"
}}
/>
</div>
<div>
<p>Port:</p>
<input
type={"text"}
value={this.state.port == 0 ? "" : this.state.port}
onChange={(e) => {
const target = e.target as HTMLInputElement;
const value = target.value;
if (isNaN(Number(value)) || value.length > 5) {
return;
}
this.setState({ port: Number(value) });
}}
disabled={disable}
style={{
cursor: disable ? "not-allowed" : "text",
userSelect: disable ? "none" : "auto"
}}
/>
</div>
</div>
<button className={"ServerSettings_Authenticate"} onClick={this.authenticate.bind(this)}>
Authenticate
</button>
</div>
<div className={"ServerSettings_Content"}>
<button className={"ServerSettings_Save"} onClick={this.save.bind(this)}>
Save
</button>
</div>
</>
)}
</div>
);
}
}
export default ServerSettings;

View File

@ -1,45 +0,0 @@
import React from "react";
import type { Page } from "@backend/types";
import { navigate } from "@backend/events";
import "@css/widgets/SideBarButton.scss";
interface IProps {
name: string;
icon: string;
anchor: Page;
}
class SideBarButton extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
/**
* Redirects the user to the specified anchor.
* @private
*/
private redirect(): void {
navigate(this.props.anchor);
}
/**
* Checks if this component should be showed.
*/
private shouldShow(): boolean {
return !((window as any).hide as string[]).includes(this.props.anchor.toLowerCase());
}
render() {
return this.shouldShow() ? (
<div className={"SideBarButton"} onClick={() => this.redirect()}>
<img className={"SideBarButton_Icon"} src={this.props.icon} alt={this.props.name} />
<p className={"SideBarButton_Label"}>{this.props.name}</p>
</div>
) : undefined;
}
}
export default SideBarButton;

View File

@ -1,37 +0,0 @@
import React from "react";
import { IoLocationSharp } from "react-icons/io5";
import type { Quest } from "@backend/types";
import "@css/widgets/quest/NormalQuest.scss";
interface IProps {
quest: Quest;
right?: boolean;
}
class NormalQuest extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
const { quest } = this.props;
return (
<div className={"NormalQuest"} datatype={this.props.right ? "right" : "left"}>
<div className={"NormalQuest_Info"}>
<p className={"font-bold"}>{quest.description}</p>
<p>
ID: {quest.id} | Main: {quest.mainId}
</p>
</div>
<IoLocationSharp className={"NormalQuest_Icon"} />
</div>
);
}
}
export default NormalQuest;

View File

@ -1,52 +0,0 @@
import React from "react";
import { GiSupersonicArrow } from "react-icons/gi";
import Collapsible from "react-collapsible";
import NormalQuest from "@widgets/quest/NormalQuest";
import type { MainQuest } from "@backend/types";
import { listSubQuestsFor } from "@backend/data";
import "@css/widgets/quest/PrimaryQuest.scss";
interface IProps {
quest: MainQuest;
}
function Trigger(props: IProps): React.ReactElement {
return (
<div className={"Trigger"}>
<GiSupersonicArrow className={"Trigger_Icon"} />
<div className={"Trigger_Info"}>
<p className={"font-bold"}>{props.quest.title}</p>
<p>ID: {props.quest.id}</p>
</div>
</div>
);
}
class PrimaryQuest extends React.PureComponent<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<Collapsible
className={"PrimaryQuest"}
openedClassName={"PrimaryQuest"}
trigger={<Trigger quest={this.props.quest} />}
transitionTime={50}
>
<div className={"PrimaryQuest_List"}>
{listSubQuestsFor(this.props.quest).map((quest) => (
<NormalQuest key={quest.id} quest={quest} right />
))}
</div>
</Collapsible>
);
}
}
export default PrimaryQuest;

View File

@ -1,181 +0,0 @@
import type { Entity, Item, EntityInfo, ItemInfo, WindowDetails } from "@backend/types";
import { ItemType, Quality } from "@backend/types";
/**
* Fetches the name of the CSS variable for the quality.
*
* @param quality The quality of the item.
*/
export function colorFor(quality: Quality): string {
switch (quality) {
default:
return "--legendary-color";
case "EPIC":
return "--epic-color";
case "RARE":
return "--rare-color";
case "UNCOMMON":
return "--uncommon-color";
case "COMMON":
return "--common-color";
case "UNKNOWN":
return "--unknown-color";
}
}
/**
* Checks if a value is between two numbers.
*
* @param value The value to check.
* @param min The minimum value.
* @param max The maximum value.
*/
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`;
}
}
/**
* 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
*
* @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"
};
/**
* Gets the route for an item type.
*
* @param type The type of the item.
*/
export function typeToRoute(type: ItemType): string {
switch (type) {
default:
return "material";
case ItemType.Furniture:
return "furniture";
case ItemType.Reliquary:
return "reliquary";
case ItemType.Weapon:
return "weapon";
}
}
/**
* Fetches the data for an item.
* Uses the Project Amber API to get the data.
*
* @route GET https://api.ambr.top/v2/EN/{type}/{id}
* @param item The item to fetch the data for.
*/
export async function fetchItemData(item: Item): Promise<ItemInfo> {
let url = `https://api.ambr.top/v2/EN/(type)/(id)`;
// Replace the type and ID in the URL.
url = url.replace("(type)", typeToRoute(item.type));
url = url.replace("(id)", item.id.toString());
// Fetch the data.
return fetch(url)
.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(() => {});
}
/**
* Attempts to copy text to the clipboard.
* Uses the Clipboard API.
*
* @param text The text to copy.
*/
export async function copyToClipboard(text: string): Promise<void> {
await navigator.clipboard.writeText(text);
}
/**
* Opens a URL in a new tab.
* Uses the window.open() method.
*
* @param url The URL to open.
*/
export function openUrl(url: string): void {
window.open(url, "_blank");
}
/**
* Checks if a value is NaN.
* Returns an empty string if it is.
*
* @param value The value to check.
*/
export function notNaN(value: number | string): string {
const number = parseInt(value.toString());
return isNaN(number) ? "" : number.toString();
}
/**
* Extracts the server details out of the window.
*/
export function getWindowDetails(): WindowDetails {
const details = (window as any).details;
const { address, port, disable } = details;
return {
address: (address as string).includes("DETAILS_ADDRESS") ? "127.0.0.1" : address,
port: (port as string).includes("DETAILS_PORT") ? 443 : parseInt(port),
disable: (disable as string).includes("DETAILS_DISABLE") ? false : disable == "true"
};
}

View File

@ -1,36 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@app/*": ["src/*"],
"@backend/*": ["src/backend/*"],
"@css/*": ["src/css/*"],
"@ui/*": ["src/ui/*"],
"@icons/*": ["src/icons/*"],
"@views/*": ["src/ui/views/*"],
"@pages/*": ["src/ui/pages/*"],
"@widgets/*": ["src/ui/widgets/*"],
"@components/*": ["src/ui/components/*"],
"@data/*": ["data/*"],
"@assets/*": ["data/assets/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

View File

@ -1,25 +0,0 @@
// noinspection JSUnusedGlobalSymbols
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths";
import dsv from "@rollup/plugin-dsv";
import viteSvgr from "vite-plugin-svgr";
import { viteSingleFile } from "vite-plugin-singlefile";
import postcss from "./cfg/postcss.config.js";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [ react(), tsconfigPaths(), dsv(),
viteSvgr(), viteSingleFile() ],
css: { postcss },
optimizeDeps: {
exclude: [
"react-virtualization"
]
}
});

View File

@ -21,11 +21,16 @@ import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
/**
* This is a simple implementation of a server-to-server IPC client.
* It is implemented over WebSockets, and supports all Grasscutter versions past 1.6.0
*/
@Getter
public final class DispatchClient extends WebSocketClient implements IDispatcher {
@Getter private final Logger logger = Grasscutter.getLogger();
@Getter private final Map<Integer, BiConsumer<WebSocket, JsonElement>> handlers = new HashMap<>();
private final Logger logger = Grasscutter.getLogger();
private final Map<Integer, BiConsumer<WebSocket, JsonElement>> handlers = new HashMap<>();
@Getter private final Map<Integer, List<Consumer<JsonElement>>> callbacks = new HashMap<>();
private final Map<Integer, List<Consumer<JsonElement>>> callbacks = new HashMap<>();
public DispatchClient(URI serverUri) {
super(serverUri);

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
margin: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
p {
margin: 2px;
}
</style>
<title>Handbook Authentication</title>
</head>
<body>
<script type="application/javascript">
if ("{{VALUE}}" === "true") {
parent.postMessage({
type: "handbook-auth",
token: "{{SESSION_TOKEN}}",
uid: "{{PLAYER_ID}}"
}, "*");
}
</script>
<p>Input your Player UID here.</p>
<form method="post">
<label>
<input
name="playerid"
type="number"
/>
</label>
<input type="submit" />
</form>
</body>
</html>