mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2026-02-05 17:46:59 +01:00
Merge unstable into development (#2173)
* Remove more scene synchronized
* Fix worktop options not appearing
* Format code [skip actions]
* Fix delay with server tasks
* Format code [skip actions]
* Fully fix fairy clock (#2146)
* Fix scene transition
* fully fix fairy clock
* Re-add call to `Player#updatePlayerGameTime`
* Format code [skip actions]
* Initialize the script loader in `ResourceLoader#loadAll`
* Fix region removal checking
* Format code [skip actions]
* Use Lombok's `EqualsAndHashCode` for comparing scene regions
* Format code [skip actions]
* Move 'invalid gather object' to `trace`
* Add more information to the 'unknown condition handler' message
* Move invalid ability action to trace
* Make `KcpTunnel` public
* Validate the NPC being talked to
* Format code [skip actions]
* NPCs are not spawned server side; change logic to handle it
* Format code [skip actions]
* unload scene when there are no players (#2147)
* unload scene when there are no players
* Update src/main/java/emu/grasscutter/game/world/Scene.java
Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
---------
Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
* Check if a command should be copied or HTTP should be used
* Lint Code [skip actions]
* Fix character names rendering incorrectly
* Add basic troubleshooting command
* Implement handbook teleporting
also a few formatting changes and sort data by logical sense
* Fix listener `ConcurrentModificationException` issue
* Add color change to `Join the Community!`
* Lint Code [skip actions]
* Make clickable buttons appear clickable
* Remove 'Mechanicus' entities from the list of entities
* Format code [skip actions]
* Fix going back returning a blank screen
* Implement entity spawning
* Add setting level to entity card
* Add support for 'plain text' mode
* Make descriptions of objects scrollable
* Lint Code [skip actions]
* Format code [skip actions]
* Change the way existing hooks work
* Format code [skip actions]
* Upgrade Javalin to 5.5.0 & Fix project warnings
* Upgrade logging libraries
* Fix gacha mappings static file issue
* Add temporary backwards compatability for `ServerHelper`
* Format code [skip actions]
* Remove artifact signatures from VCS
* Fix forge queue data protocol definition
* Run `spotlessApply`
* Format code [skip actions]
* Download data required for building artifacts
* Add call for Facebook logins
* Add the wiki page as a submodule
* Format code [skip actions]
* Update translation (#2150)
* Update translation
* Update translation
* Separate the dispatch and game servers (pt. 1)
gacha is still broken, handbook still needs to be done
* Format code [skip actions]
* Separate the dispatch and game servers (pt. 2)
this commit fixes the gacha page
* Add description for '/troubleshoot'
* Set default avatar talent level to 10
* Separate the dispatch and game servers (pt. 3)
implement handbook across servers!
* Format code [skip actions]
* Update GitHub Actions to use 'download-file' over 'wget'
* Gm handbook lmao (#2149)
* Fix font issue
* Fix avatars
* Fix text overflow in commands
* Fix virtualized lists and items page 😭😭
* magix why 💀
* use hover style in all minicards
* button
* remove console.log
* lint
* Add icons
* magix asked
* Fix overflow padding issue
* Fix achievement text overflow
* remove icons from repo
* Change command icon
* Add the wiki page as a submodule
* total magix moment
* fix text overflow in commands
* Fix discord button
* Make text scale on Minicard
* import icons and font from another source
* Add hover effects to siebar buttons
* move font and readme to submodule repo
* Make data folder a submodule
* import icons and font from data submodule
* Update README.md
* total magix moment
* magix moment v2
* submodule change
* Import `.webp` files
* Resize `HomeButton`
* Fix 'Copy Command' reappearing after changing pages
---------
Co-authored-by: KingRainbow44 <kobedo11@gmail.com>
* Lint Code [skip actions]
* Download data for the build, not for the lint
* format imports
this is really just to see if build handbook works kek
* Implement proper handbook authentication (pt. 1)
* Implement proper handbook authentication (pt. 2)
* Format code [skip actions]
* Add quest data dumping for the handbook
* Change colors to fit _something suitable_
* Format code [skip actions]
* Fix force pushing to branches after linting
* Fix logic of `SetPlayerPropReq`
* Move more group loading to `trace`
* Add handbook IP authentication in hybrid mode
* Fix player level up not displaying on the client properly
* Format code [skip actions]
* Fix game time locking
* Format code [skip actions]
* Update player properties
* Format code [skip actions]
* Move `warn`s for groups to `debug`
* Fix player pausing
* Move more logs to `trace`
* Use `removeItemById` for deleting items via quests
* Clean up logger more
* Pause in-game time when the world is paused
* Format code [skip actions]
* More player property documentation
* Multi-threaded resource loading
* Format code [skip actions]
* Add quest widgets
* Add quests page (basic impl.)
* Add/fix colors
also fix tailwind
* Remove banned packets
client modifications already perform the job of blocking malicious packets from being executed, no point in having this if self-windy is wanted
* Re-add `BeginCameraSceneLookNotify`
* Fix being unable to attack (#2157)
* Add `PlayerOpenChestEvent`
* Add methods to get players from the server
* Add static methods to register an event handler
* Add `PlayerEnterDungeonEvent`
* Remove legacy documentation from `PlayerMoveEvent`
* Add `PlayerChatEvent`
* Add defaults to `Position`
* Clean up `.utils`
* Revert `Multi-threaded resource loading`
* Fix changing target UID when talking to the server
* Lint Code [skip actions]
* Format code [skip actions]
* fix NPC talk triggering main quest in 46101 (#2158)
Make it so that only talks where the param matches the talkId are checked.
* Format code [skip actions]
* Partially fix Chasing Shadows (#2159)
* Partially fix Chasing Shadows
* Go ahead and move it before the return before Magix tells me to.
* Format code [skip actions]
* Bring back period lol (#2160)
* Disable SNI for the HTTPS server
* Add `EntityCreationEvent`
* Add initial startup message
this is so the server appears like its preparing to start
* Format code [skip actions]
* Enable debug mode for plugin loggers if enabled for the primary logger
* Add documentation about `WorldAreaConfigData`
* Make more fields in excels accessible
* Remove deprecated fields from `GetShopRsp`
* Run `spotlessApply` on definitions
* Add `PlayerEnterAreaEvent`
* Optimize event calls
* Fix event invokes
* Format code [skip actions]
* Remove manual autofinish for main quests. (#2162)
* Add world areas to the textmap cache
* Format code [skip actions]
* Don't overdefine variables in extended classes (#2163)
* Add dumper for world areas
* Format code [skip actions]
* instantiate personalLineList (#2165)
* Fix protocol definitions
thank you Nazrin! (+ hiro for raw definitions)
* Fix the background color leaking from the character widget
* Change HTML spacing to 2 spaces
* Implement hiding widgets
* Change scrollbar to a vibrant color
* Add _some_ scaling to the home buttons and its text
* Build the handbook with Gradle
* Fix the 'finer details' with the handbook UI
* Lint Code [skip actions]
* Fix target destination for the Gradle-built handbook
* Implement fetching a player across servers & Add a chainable JsonObject
useful for plugins! might be used in grasscutter eventually
* Fix GitHub actions
* Fix event calling & canceling
* Run `spotlessApply`
* Rename fields (might be wrong)
* Add/update all/more protocol definitions
* Add/update all/more protocol definitions
* Remove outdated packet
* Fix protocol definitions
* Format code [skip actions]
* Implement some lua variables for less console spam (#2172)
* Implement some lua variables for less console spam
* Add GetHostQuestState
This fixes some chapter 3 stuff.
* Format code [skip actions]
* Fix merge import
* Format code [skip actions]
* Fully fix fairy clock for real this time (#2167)
* Fully fix fairy clock For real this time
* Make it so relogging keeps the time lock state.
* Refactor out questLockTime
* Per Hartie, the client packet needs to be changed too
* Update src/main/java/emu/grasscutter/game/world/World.java
Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
* Update src/main/java/emu/grasscutter/server/packet/recv/HandlerClientLockGameTimeNotify.java
* Remove all code not needed to get clock working
---------
Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>
* Implement a proper ability system (#2166)
* Apply fix `21dec2fe`
* Apply fix `89d01d5f`
* Apply fix `d900f154`
this one was already implemented; updated to use call from previous commit
* Ability changing commit
TODO: change info to debug
* Remove use of deprecated methods/fields
* Temp commit v2
(Adding LoseHP and some fixes)
* Oopsie
* Probably fix monster battle
* Fix issue with reflecting into fields
* Fix some things
* Fix ability names for 3.6 resources
* Improve logging
---------
Co-authored-by: StartForKiller <jesussanz2003@gmail.com>
* Format code [skip actions]
* Add system for sending messages between servers
* Format some code
* Remove protocol definitions from Spotless
* Default debug to false; enable with `-debug`
* Implement completely useless global value copying
* HACK: Return the avatar which holds the weapon when the weapon is referred to by ID
* Add properties to `AbilityModifier`
* Change the way HTML is served after authentication
* Use thread executors to speed up the database loading process
* Format code [skip actions]
* Add system for setting handbook address and port
* Lint Code [skip actions]
* Format code [skip actions]
* Fix game-related data not saving
* Format code [skip actions]
* Fix handbook server details
* Lint Code [skip actions]
* Format code [skip actions]
* Use the headers provided by a context to get the IP address
should acknowledge #1975
* Format code [skip actions]
* Move more logs to `trace`
* Format code [skip actions]
* more trace
* Fix something and implement weapon entities
* Format code [skip actions]
* Fix `EntityWeapon`
* Remove deprecated API & Fix resource checking
* Fix unnecessary warning for first-time setup
* Implement handbook request limiting
* Format code [skip actions]
* Fix new avatar weapons being null
* Format code [skip actions]
* Fix issue with 35303 being un-completable & Try to fix fulfilled quest conditions being met
* Load activity config on server startup
* Require plugins to specify an API version and match with the server
* Add default open state ignore list
* Format code [skip actions]
* Quick fix for questing, needs more investigation
This would make the questing work again
* Remove existing hack for 35303
* Fix ignored open states from being set
* Format code [skip actions]
* fix the stupidest bug ive ever seen
* Optimize player kicking on server close
* Format code [skip actions]
* Re-add hack to fix 35303
* Update GitHub actions
* Format code [skip actions]
* Potentially fix issues with regions
* Download additional handbook data
* Revert "Potentially fix issues with regions"
This reverts commit 84e3823695.
---------
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: scooterboo <lewasite@yahoo.com>
Co-authored-by: Tesutarin <105267106+Tesutarin@users.noreply.github.com>
Co-authored-by: Scald <104459145+Arikatsu@users.noreply.github.com>
Co-authored-by: StartForKiller <jesussanz2003@gmail.com>
This commit is contained in:
27
src/handbook/.gitignore
vendored
Normal file
27
src/handbook/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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/
|
||||
12
src/handbook/.prettierrc
Normal file
12
src/handbook/.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"jsxSingleQuote": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
||||
25
src/handbook/cfg/postcss.config.js
Normal file
25
src/handbook/cfg/postcss.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
})()
|
||||
}
|
||||
9
src/handbook/cfg/tailwind.config.js
Normal file
9
src/handbook/cfg/tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,tsx,ts}"],
|
||||
mode: "jit",
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: []
|
||||
};
|
||||
34
src/handbook/data/README.md
Normal file
34
src/handbook/data/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Handbook Data
|
||||
Use Grasscutter's dumpers to generate the data to put here.
|
||||
|
||||
## 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
src/handbook/data/assets
Submodule
1
src/handbook/data/assets
Submodule
Submodule src/handbook/data/assets added at 1b9f8b2c0d
22
src/handbook/index.html
Normal file
22
src/handbook/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!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
Normal file
6236
src/handbook/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
src/handbook/package.json
Normal file
52
src/handbook/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
src/handbook/patches/react-virtualized+9.22.3.patch
Normal file
10
src/handbook/patches/react-virtualized+9.22.3.patch
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
55
src/handbook/src/backend/commands.ts
Normal file
55
src/handbook/src/backend/commands.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
245
src/handbook/src/backend/data.ts
Normal file
245
src/handbook/src/backend/data.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
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;
|
||||
})
|
||||
};
|
||||
}
|
||||
103
src/handbook/src/backend/events.ts
Normal file
103
src/handbook/src/backend/events.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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
Normal file
13
src/handbook/src/backend/files.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
198
src/handbook/src/backend/server.ts
Normal file
198
src/handbook/src/backend/server.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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());
|
||||
}
|
||||
177
src/handbook/src/backend/types.ts
Normal file
177
src/handbook/src/backend/types.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
88
src/handbook/src/css/App.scss
Normal file
88
src/handbook/src/css/App.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
@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;
|
||||
}
|
||||
28
src/handbook/src/css/Text.scss
Normal file
28
src/handbook/src/css/Text.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
4
src/handbook/src/css/components/VirtualizedGrid.scss
Normal file
4
src/handbook/src/css/components/VirtualizedGrid.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.GridRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
31
src/handbook/src/css/pages/AvatarsPage.scss
Normal file
31
src/handbook/src/css/pages/AvatarsPage.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.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%;
|
||||
}
|
||||
30
src/handbook/src/css/pages/CommandsPage.scss
Normal file
30
src/handbook/src/css/pages/CommandsPage.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.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;
|
||||
}
|
||||
93
src/handbook/src/css/pages/EntitiesPage.scss
Normal file
93
src/handbook/src/css/pages/EntitiesPage.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
.EntitiesPage {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: var(--background-color);
|
||||
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.EntitiesPage_Content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.EntitiesPage_Header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 30px;
|
||||
align-content: center;
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.EntitiesPage_Title {
|
||||
max-width: 230px;
|
||||
max-height: 60px;
|
||||
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.EntitiesPage_Search {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 465px;
|
||||
max-height: 60px;
|
||||
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
|
||||
background-color: var(--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;
|
||||
}
|
||||
147
src/handbook/src/css/pages/HomePage.scss
Normal file
147
src/handbook/src/css/pages/HomePage.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/handbook/src/css/pages/ItemsPage.scss
Normal file
88
src/handbook/src/css/pages/ItemsPage.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
.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;
|
||||
}
|
||||
14
src/handbook/src/css/pages/QuestsPage.scss
Normal file
14
src/handbook/src/css/pages/QuestsPage.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.QuestsPage {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.QuestsPage_Selector {
|
||||
display: flex;
|
||||
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
56
src/handbook/src/css/pages/ScenesPage.scss
Normal file
56
src/handbook/src/css/pages/ScenesPage.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
.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;
|
||||
}
|
||||
4
src/handbook/src/css/views/Content.scss
Normal file
4
src/handbook/src/css/views/Content.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.Content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
12
src/handbook/src/css/views/Overlay.scss
Normal file
12
src/handbook/src/css/views/Overlay.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.Overlay {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
background-color: rgb(0, 0, 0, 0.35);
|
||||
}
|
||||
9
src/handbook/src/css/views/PlainText.scss
Normal file
9
src/handbook/src/css/views/PlainText.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.PlainText {
|
||||
margin: 12px 5px 0 12px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
|
||||
p {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
80
src/handbook/src/css/views/SideBar.scss
Normal file
80
src/handbook/src/css/views/SideBar.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
.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);
|
||||
}
|
||||
53
src/handbook/src/css/widgets/Card.scss
Normal file
53
src/handbook/src/css/widgets/Card.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.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;
|
||||
}
|
||||
47
src/handbook/src/css/widgets/Character.scss
Normal file
47
src/handbook/src/css/widgets/Character.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
46
src/handbook/src/css/widgets/HomeButton.scss
Normal file
46
src/handbook/src/css/widgets/HomeButton.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.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;
|
||||
}
|
||||
47
src/handbook/src/css/widgets/MiniCard.scss
Normal file
47
src/handbook/src/css/widgets/MiniCard.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.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;
|
||||
}
|
||||
160
src/handbook/src/css/widgets/ObjectCard.scss
Normal file
160
src/handbook/src/css/widgets/ObjectCard.scss
Normal file
@@ -0,0 +1,160 @@
|
||||
.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;
|
||||
}
|
||||
117
src/handbook/src/css/widgets/ServerSettings.scss
Normal file
117
src/handbook/src/css/widgets/ServerSettings.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
.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;
|
||||
}
|
||||
31
src/handbook/src/css/widgets/SideBarButton.scss
Normal file
31
src/handbook/src/css/widgets/SideBarButton.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.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;
|
||||
}
|
||||
54
src/handbook/src/css/widgets/quest/NormalQuest.scss
Normal file
54
src/handbook/src/css/widgets/quest/NormalQuest.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
.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);
|
||||
}
|
||||
65
src/handbook/src/css/widgets/quest/PrimaryQuest.scss
Normal file
65
src/handbook/src/css/widgets/quest/PrimaryQuest.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
.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
src/handbook/src/icons/discord.svg
Normal file
1
src/handbook/src/icons/discord.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 985 B |
20
src/handbook/src/main.tsx
Normal file
20
src/handbook/src/main.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
);
|
||||
56
src/handbook/src/ui/App.tsx
Normal file
56
src/handbook/src/ui/App.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
46
src/handbook/src/ui/components/TextState.tsx
Normal file
46
src/handbook/src/ui/components/TextState.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
102
src/handbook/src/ui/components/VirtualizedGrid.tsx
Normal file
102
src/handbook/src/ui/components/VirtualizedGrid.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
39
src/handbook/src/ui/pages/AvatarsPage.tsx
Normal file
39
src/handbook/src/ui/pages/AvatarsPage.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
32
src/handbook/src/ui/pages/CommandsPage.tsx
Normal file
32
src/handbook/src/ui/pages/CommandsPage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
153
src/handbook/src/ui/pages/EntitiesPage.tsx
Normal file
153
src/handbook/src/ui/pages/EntitiesPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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;
|
||||
75
src/handbook/src/ui/pages/HomePage.tsx
Normal file
75
src/handbook/src/ui/pages/HomePage.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
157
src/handbook/src/ui/pages/ItemsPage.tsx
Normal file
157
src/handbook/src/ui/pages/ItemsPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
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;
|
||||
50
src/handbook/src/ui/pages/QuestsPage.tsx
Normal file
50
src/handbook/src/ui/pages/QuestsPage.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
78
src/handbook/src/ui/pages/ScenesPage.tsx
Normal file
78
src/handbook/src/ui/pages/ScenesPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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;
|
||||
73
src/handbook/src/ui/views/Content.tsx
Normal file
73
src/handbook/src/ui/views/Content.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
57
src/handbook/src/ui/views/Overlay.tsx
Normal file
57
src/handbook/src/ui/views/Overlay.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
171
src/handbook/src/ui/views/PlainText.tsx
Normal file
171
src/handbook/src/ui/views/PlainText.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
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;
|
||||
123
src/handbook/src/ui/views/SideBar.tsx
Normal file
123
src/handbook/src/ui/views/SideBar.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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;
|
||||
67
src/handbook/src/ui/widgets/Card.tsx
Normal file
67
src/handbook/src/ui/widgets/Card.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
56
src/handbook/src/ui/widgets/Character.tsx
Normal file
56
src/handbook/src/ui/widgets/Character.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
199
src/handbook/src/ui/widgets/EntityCard.tsx
Normal file
199
src/handbook/src/ui/widgets/EntityCard.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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;
|
||||
45
src/handbook/src/ui/widgets/HomeButton.tsx
Normal file
45
src/handbook/src/ui/widgets/HomeButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
179
src/handbook/src/ui/widgets/ItemCard.tsx
Normal file
179
src/handbook/src/ui/widgets/ItemCard.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
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;
|
||||
109
src/handbook/src/ui/widgets/MiniCard.tsx
Normal file
109
src/handbook/src/ui/widgets/MiniCard.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
183
src/handbook/src/ui/widgets/ServerSettings.tsx
Normal file
183
src/handbook/src/ui/widgets/ServerSettings.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
45
src/handbook/src/ui/widgets/SideBarButton.tsx
Normal file
45
src/handbook/src/ui/widgets/SideBarButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
37
src/handbook/src/ui/widgets/quest/NormalQuest.tsx
Normal file
37
src/handbook/src/ui/widgets/quest/NormalQuest.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
52
src/handbook/src/ui/widgets/quest/PrimaryQuest.tsx
Normal file
52
src/handbook/src/ui/widgets/quest/PrimaryQuest.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
181
src/handbook/src/utils.ts
Normal file
181
src/handbook/src/utils.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
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"
|
||||
};
|
||||
}
|
||||
36
src/handbook/tsconfig.json
Normal file
36
src/handbook/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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" }]
|
||||
}
|
||||
11
src/handbook/tsconfig.node.json
Normal file
11
src/handbook/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
25
src/handbook/vite.config.ts
Normal file
25
src/handbook/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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"
|
||||
]
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user