mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-27 21:10:54 +00:00
misc: Remove the handbook from the main repository
sorry guys, i just wanted to do something cool...
This commit is contained in:
parent
ab0ec0a0e0
commit
70bb5ca5b5
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@ -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:
|
||||
|
2
.github/workflows/check_code.yml
vendored
2
.github/workflows/check_code.yml
vendored
@ -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"
|
||||
|
126
.github/workflows/handbook.yml
vendored
126
.github/workflows/handbook.yml
vendored
@ -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
1
.gitignore
vendored
@ -67,7 +67,6 @@ tmp/
|
||||
!entrypoint.sh
|
||||
|
||||
GM Handbook*.txt
|
||||
handbook.html
|
||||
|
||||
config.json
|
||||
mitmdump.exe
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
||||
|
21
README.md
21
README.md
@ -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
|
||||
|
||||
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.
|
||||
|
77
build.gradle
77
build.gradle
@ -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'
|
||||
}
|
||||
|
27
src/handbook/.gitignore
vendored
27
src/handbook/.gitignore
vendored
@ -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/
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"jsxSingleQuote": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
@ -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;
|
||||
})()
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,tsx,ts}"],
|
||||
mode: "jit",
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: []
|
||||
};
|
@ -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
|
@ -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>
|
6236
src/handbook/package-lock.json
generated
6236
src/handbook/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
};
|
@ -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;
|
||||
})
|
||||
};
|
||||
}
|
@ -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;
|
13
src/handbook/src/backend/files.d.ts
vendored
13
src/handbook/src/backend/files.d.ts
vendored
@ -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;
|
||||
}
|
@ -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());
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
.GridRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
.QuestsPage {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.QuestsPage_Selector {
|
||||
display: flex;
|
||||
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
.Content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
@ -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);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
.PlainText {
|
||||
margin: 12px 5px 0 12px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
|
||||
p {
|
||||
color: white;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 |
@ -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>
|
||||
);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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"
|
||||
};
|
||||
}
|
@ -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" }]
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
||||
});
|
@ -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);
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user