Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
|
|
@ -14,7 +14,7 @@ config.json
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
logs/
|
*.log
|
||||||
|
|
||||||
# Git folder
|
# Git folder
|
||||||
.git/
|
.git/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
ACORE_ARMORY_AOWOW_URL="https://wowgaming.altervista.org/aowow"
|
ACORE_ARMORY_AOWOW_URL="https://wotlkdb.com"
|
||||||
ACORE_ARMORY_WEBSITE_URL="https://mywebsite.com"
|
ACORE_ARMORY_WEBSITE_URL="https://mywebsite.com"
|
||||||
ACORE_ARMORY_WEBSITE_NAME="My Website"
|
ACORE_ARMORY_WEBSITE_NAME="My Website"
|
||||||
ACORE_ARMORY_WEBSITE_ROOT=""
|
ACORE_ARMORY_WEBSITE_ROOT=""
|
||||||
|
|
@ -6,8 +6,6 @@ ACORE_ARMORY_IFRAME_MODE__ENABLED=0
|
||||||
ACORE_ARMORY_IFRAME_MODE__URL="https://mywebsite.com/armory"
|
ACORE_ARMORY_IFRAME_MODE__URL="https://mywebsite.com/armory"
|
||||||
ACORE_ARMORY_LOAD_DBCS=1
|
ACORE_ARMORY_LOAD_DBCS=1
|
||||||
ACORE_ARMORY_HIDE_GAME_MASTERS=1
|
ACORE_ARMORY_HIDE_GAME_MASTERS=1
|
||||||
ACORE_ARMORY_TRANSMOG_MODULE=0
|
|
||||||
ACORE_ARMORY_USE_ZAM_CDN=0
|
|
||||||
ACORE_ARMORY_REALMS__0__NAME="AzerothCore"
|
ACORE_ARMORY_REALMS__0__NAME="AzerothCore"
|
||||||
ACORE_ARMORY_REALMS__0__REALM_ID=1
|
ACORE_ARMORY_REALMS__0__REALM_ID=1
|
||||||
ACORE_ARMORY_REALMS__0__AUTH_DATABASE="acore_auth"
|
ACORE_ARMORY_REALMS__0__AUTH_DATABASE="acore_auth"
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"rules": {
|
|
||||||
"semi": ["error", "always"],
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"args": "none"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-duplicate-imports": "warn",
|
|
||||||
"no-promise-executor-return": "warn",
|
|
||||||
"quote-props": ["warn", "as-needed"],
|
|
||||||
"indent": "off",
|
|
||||||
"@typescript-eslint/indent": [
|
|
||||||
"error",
|
|
||||||
"tab",
|
|
||||||
{
|
|
||||||
"ignoredNodes": [
|
|
||||||
"FunctionExpression > .params[decorators.length > 0]",
|
|
||||||
"FunctionExpression > .params > :matches(Decorator, :not(:first-child))",
|
|
||||||
"ClassBody.body > PropertyDefinition[decorators.length > 0] > .key"
|
|
||||||
],
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
||||||
ko_fi: roboto
|
|
||||||
13
.github/workflows/build.yml
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
name: Build
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
13
.github/workflows/lint.yml
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
name: Lint
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
eslint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run lint -- --max-warnings=0
|
|
||||||
3
.gitignore
vendored
|
|
@ -15,6 +15,3 @@ config.json
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# IDEs
|
|
||||||
.vscode/
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
.tmp
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
.npmrc
|
|
||||||
.nvmrc
|
|
||||||
node_modules/
|
|
||||||
.env*
|
|
||||||
/data/
|
|
||||||
build/
|
|
||||||
build-tools/
|
|
||||||
logs/
|
|
||||||
reports/
|
|
||||||
coverage/
|
|
||||||
dist/
|
|
||||||
**/*.md
|
|
||||||
**/*.yml
|
|
||||||
package-lock.json
|
|
||||||
static/**/*.min.js
|
|
||||||
static/**/*.min.css
|
|
||||||
static/**/*.hbs
|
|
||||||
config.json
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 140,
|
|
||||||
"useTabs": true,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "always"
|
|
||||||
}
|
|
||||||
39
README.md
|
|
@ -1,5 +1,4 @@
|
||||||
<!-- SHIELDS -->
|
<!-- SHIELDS -->
|
||||||
[](https://github.com/r-o-b-o-t-o/azerothcore-armory/actions/workflows/build.yml)
|
|
||||||
[![Stargazers][stars-shield]][stars-url]
|
[![Stargazers][stars-shield]][stars-url]
|
||||||
[![Forks][forks-shield]][forks-url]
|
[![Forks][forks-shield]][forks-url]
|
||||||
[![Issues][issues-shield]][issues-url]
|
[![Issues][issues-shield]][issues-url]
|
||||||
|
|
@ -12,13 +11,11 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A website to view your <a href="https://github.com/azerothcore/azerothcore-wotlk">AzerothCore</a> server's characters
|
A website to view your <a href="https://github.com/azerothcore/azerothcore-wotlk">AzerothCore</a> server's characters
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?assignees=&labels=bug&template=bug_report.yml">Report a Bug</a>
|
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?template=bug_report.yml">Report a Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?assignees=&labels=enhancement&template=feature_request.yml">Suggest a Feature</a>
|
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?template=feature_request.yml">Request a Feature</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?labels=question&template=question.yml">Ask a Question</a>
|
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?template=question.yml">Ask a Question</a>
|
||||||
·
|
|
||||||
<a href="https://www.chromiecraft.com/armory">Demo</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -111,14 +108,21 @@ I also noticed that such a tool was frequently requested in the AzerothCore Disc
|
||||||
```sh
|
```sh
|
||||||
git clone git@github.com:r-o-b-o-t-o/azerothcore-armory.git
|
git clone git@github.com:r-o-b-o-t-o/azerothcore-armory.git
|
||||||
```
|
```
|
||||||
2. Install the dependencies:
|
3. Install the dependencies:
|
||||||
```sh
|
```sh
|
||||||
cd azerothcore-armory/
|
cd azerothcore-armory/
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
3. Configure the application: copy `config.default.json` to `config.json` or `.env.example` to `.env` and edit the resulting file.
|
4. Configure the application: copy `config.default.json` to `config.json` or `.env.example` to `.env` and edit the resulting file.
|
||||||
See the [Configuration Reference](#configuration-reference) below for a description of all values.
|
See the [Configuration Reference](#configuration-reference) below for a description of all values.
|
||||||
4. Download the model viewer's data from the [Releases](https://github.com/r-o-b-o-t-o/azerothcore-armory/releases) page and extract it to the `data/` directory.
|
5. Download the model viewer's data:
|
||||||
|
* Download from script:
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
npm run fetchdata
|
||||||
|
```
|
||||||
|
**OR**
|
||||||
|
* Download the latest archive (`data.zip`) from the [Releases](https://github.com/r-o-b-o-t-o/azerothcore-armory/releases) page and extract it to the `data/` directory.
|
||||||
|
|
||||||
### Configuration Reference
|
### Configuration Reference
|
||||||
|
|
||||||
|
|
@ -126,8 +130,8 @@ I also noticed that such a tool was frequently requested in the AzerothCore Disc
|
||||||
<summary>Main configuration</summary>
|
<summary>Main configuration</summary>
|
||||||
|
|
||||||
| config.json | .env | Type | Default value | Description |
|
| config.json | .env | Type | Default value | Description |
|
||||||
|--------------------------------|----------------------------------------------------|-------------------------------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------------|----------------------------------------------------|-------------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `aowowUrl` | `ACORE_ARMORY_AOWOW_URL` | String | `"https://wowgaming.altervista.org/aowow"` | The URL of the AoWoW database to use for tooltips and links |
|
| `aowowUrl` | `ACORE_ARMORY_AOWOW_URL` | String | `"https://wotlkdb.com"` | The URL of the AoWoW database to use for tooltips and links |
|
||||||
| `websiteUrl` | `ACORE_ARMORY_WEBSITE_URL` | String | `"https://mywebsite.com"` | Your website's URL. Used to redirect to the homepage on error pages |
|
| `websiteUrl` | `ACORE_ARMORY_WEBSITE_URL` | String | `"https://mywebsite.com"` | Your website's URL. Used to redirect to the homepage on error pages |
|
||||||
| `websiteName` | `ACORE_ARMORY_WEBSITE_NAME` | String | `"My Website"` | Your website's name. Displayed in the redirect button on error pages |
|
| `websiteName` | `ACORE_ARMORY_WEBSITE_NAME` | String | `"My Website"` | Your website's name. Displayed in the redirect button on error pages |
|
||||||
| `websiteRoot` | `ACORE_ARMORY_WEBSITE_ROOT` | String | `""` | The root of your armory's URL. If your armory is hosted on, for example, `http://mywebsite.com/azerothcore-armory`, the `websiteRoot` value should be `"/azerothcore-armory"` |
|
| `websiteRoot` | `ACORE_ARMORY_WEBSITE_ROOT` | String | `""` | The root of your armory's URL. If your armory is hosted on, for example, `http://mywebsite.com/azerothcore-armory`, the `websiteRoot` value should be `"/azerothcore-armory"` |
|
||||||
|
|
@ -136,8 +140,6 @@ I also noticed that such a tool was frequently requested in the AzerothCore Disc
|
||||||
| `iframeMode.url` | `ACORE_ARMORY_IFRAME_MODE__URL` | String | `"https://mywebsite.com/armory"` | Set this to the URL of the page that hosts the `iframe` |
|
| `iframeMode.url` | `ACORE_ARMORY_IFRAME_MODE__URL` | String | `"https://mywebsite.com/armory"` | Set this to the URL of the page that hosts the `iframe` |
|
||||||
| `loadDbcs` | `ACORE_ARMORY_LOAD_DBCS` | Boolean | `true` | Loads the DBC data from the `data` directory into memory when starting up. It is highly recommended to set this to `true`. Only use `false` to keep memory usage low, on a test server for example |
|
| `loadDbcs` | `ACORE_ARMORY_LOAD_DBCS` | Boolean | `true` | Loads the DBC data from the `data` directory into memory when starting up. It is highly recommended to set this to `true`. Only use `false` to keep memory usage low, on a test server for example |
|
||||||
| `hideGameMasters` | `ACORE_ARMORY_HIDE_GAME_MASTERS` | Boolean | `true` | Hides Game Master characters if set to `true`. They will not be found in the search page, and their character pages will show a 404 error |
|
| `hideGameMasters` | `ACORE_ARMORY_HIDE_GAME_MASTERS` | Boolean | `true` | Hides Game Master characters if set to `true`. They will not be found in the search page, and their character pages will show a 404 error |
|
||||||
| `transmogModule` | `ACORE_ARMORY_TRANSMOG_MODULE` | Boolean | `false` | Set this to `true` if your server uses the [transmogrification module](https://github.com/azerothcore/mod-transmog) and you want to display the transmogrified items on the 3D model |
|
|
||||||
| `useZamCdn` | `ACORE_ARMORY_USE_ZAM_CDN` | Boolean | `false` | Set this to `true` to use the ZAM network CDN for the 3D model viewer instead of the assets in your local `data` folder |
|
|
||||||
| `realms` | `ACORE_ARMORY_REALMS__`... | Array of objects | | An array of realm configurations |
|
| `realms` | `ACORE_ARMORY_REALMS__`... | Array of objects | | An array of realm configurations |
|
||||||
| `realms[0].name` | `ACORE_ARMORY_REALMS__0__NAME` | String | `"AzerothCore"` | The name of the realm. Will be used in the URLs, shown on the character pages and in the search page if you have multiple realms |
|
| `realms[0].name` | `ACORE_ARMORY_REALMS__0__NAME` | String | `"AzerothCore"` | The name of the realm. Will be used in the URLs, shown on the character pages and in the search page if you have multiple realms |
|
||||||
| `realms[0].realmId` | `ACORE_ARMORY_REALMS__0__REALM_ID` | Number | `1` | The realm's ID, this must match the `id` column of the `realmlist` table in the auth database |
|
| `realms[0].realmId` | `ACORE_ARMORY_REALMS__0__REALM_ID` | Number | `1` | The realm's ID, this must match the `id` column of the `realmlist` table in the auth database |
|
||||||
|
|
@ -178,7 +180,8 @@ Open a web browser and navigate to http://localhost:48733
|
||||||
Other useful npm scripts:
|
Other useful npm scripts:
|
||||||
* `npm run clean`: cleans the build directory
|
* `npm run clean`: cleans the build directory
|
||||||
* `npm run watch`: watches for changes and rebuilds automatically, useful for development
|
* `npm run watch`: watches for changes and rebuilds automatically, useful for development
|
||||||
* `npm run cleardata`: clears the data downloaded for the 3D model viewer
|
* `npm run fetchdata`: downloads the data needed by the 3d model viewer
|
||||||
|
* `npm run cleardata`: clears the data downloaded for the 3d model viewer
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
|
|
||||||
|
|
@ -254,7 +257,7 @@ This repository is used in production over at [ChromieCraft](https://www.chromie
|
||||||
- [X] Character page
|
- [X] Character page
|
||||||
- [X] Online/offline status
|
- [X] Online/offline status
|
||||||
- [X] Equipment with tooltips
|
- [X] Equipment with tooltips
|
||||||
- [X] 3D model, including mounts and [transmogrifications](https://github.com/azerothcore/mod-transmog)
|
- [X] 3d model, including mounts
|
||||||
- [X] Talent trees, including glyphs and dual spec support
|
- [X] Talent trees, including glyphs and dual spec support
|
||||||
- [X] Achievements
|
- [X] Achievements
|
||||||
- [X] PvP statistics, including arena teams
|
- [X] PvP statistics, including arena teams
|
||||||
|
|
@ -268,8 +271,6 @@ This repository is used in production over at [ChromieCraft](https://www.chromie
|
||||||
- [X] Multiple realms support
|
- [X] Multiple realms support
|
||||||
- [ ] PvE ladder
|
- [ ] PvE ladder
|
||||||
- [ ] PvP ladder
|
- [ ] PvP ladder
|
||||||
- [X] Arena ladder
|
|
||||||
- [ ] Achievements ladder
|
|
||||||
|
|
||||||
See the [open issues](https://github.com/r-o-b-o-t-o/azerothcore-armory/issues) for a list of suggested features and known issues.
|
See the [open issues](https://github.com/r-o-b-o-t-o/azerothcore-armory/issues) for a list of suggested features and known issues.
|
||||||
|
|
||||||
|
|
@ -307,7 +308,7 @@ Distributed under the MIT License. See the [`LICENSE`][license-url] file for mor
|
||||||
<!-- CONTACT -->
|
<!-- CONTACT -->
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
<img src="https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white"/> Feel free to get in touch with me on Discord: `roboto_`
|
<img src="https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white"/> Feel free to get in touch with me on Discord: `Roboto#9185`
|
||||||
|
|
||||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
@ -323,4 +324,4 @@ Distributed under the MIT License. See the [`LICENSE`][license-url] file for mor
|
||||||
[issues-url]: https://github.com/r-o-b-o-t-o/azerothcore-armory/issues
|
[issues-url]: https://github.com/r-o-b-o-t-o/azerothcore-armory/issues
|
||||||
[license-shield]: https://img.shields.io/github/license/r-o-b-o-t-o/azerothcore-armory.svg?style=flat
|
[license-shield]: https://img.shields.io/github/license/r-o-b-o-t-o/azerothcore-armory.svg?style=flat
|
||||||
[license-url]: https://github.com/r-o-b-o-t-o/azerothcore-armory/blob/master/LICENSE
|
[license-url]: https://github.com/r-o-b-o-t-o/azerothcore-armory/blob/master/LICENSE
|
||||||
[feature-request]: https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?assignees=&labels=enhancement&template=feature_request.yml
|
[feature-request]: https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?template=feature_request.yml
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"aowowUrl": "https://wowgaming.altervista.org/aowow",
|
"aowowUrl": "https://wotlkdb.com",
|
||||||
"websiteUrl": "https://mywebsite.com",
|
"websiteUrl": "https://mywebsite.com",
|
||||||
"websiteName": "My Website",
|
"websiteName": "My Website",
|
||||||
"websiteRoot": "",
|
"websiteRoot": "",
|
||||||
|
|
@ -9,10 +9,6 @@
|
||||||
},
|
},
|
||||||
"loadDbcs": true,
|
"loadDbcs": true,
|
||||||
"hideGameMasters": true,
|
"hideGameMasters": true,
|
||||||
"hideBotAccounts": true,
|
|
||||||
"botAccountPatterns": "RNDBOT%",
|
|
||||||
"transmogModule": false,
|
|
||||||
"useZamCdn": false,
|
|
||||||
"realms": [
|
"realms": [
|
||||||
{
|
{
|
||||||
"name": "AzerothCore",
|
"name": "AzerothCore",
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Portainer stack for the Mythica armory.
|
|
||||||
#
|
|
||||||
# Differs from the upstream docker-compose.yml in three ways, all of them
|
|
||||||
# because Portainer deploys from a git checkout it manages itself:
|
|
||||||
#
|
|
||||||
# 1. Configuration comes from environment variables set in the Portainer
|
|
||||||
# stack UI, not from a .env file in the repo (upstream uses env_file,
|
|
||||||
# which Portainer git stacks do not provide).
|
|
||||||
# 2. Only the four large model-data directories are bind-mounted from the
|
|
||||||
# host. The DBC .csv files stay in the image so they stay versioned with
|
|
||||||
# the code, and Portainer re-cloning the repo cannot wipe the 2 GB of
|
|
||||||
# model data.
|
|
||||||
# 3. The unused ac-network block is dropped.
|
|
||||||
#
|
|
||||||
# See docs/mythica-deploy.md for the full procedure.
|
|
||||||
|
|
||||||
services:
|
|
||||||
armory:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: mythica-armory
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
# --- site identity -------------------------------------------------
|
|
||||||
ACORE_ARMORY_WEBSITE_NAME: ${ARMORY_WEBSITE_NAME:-Mythica}
|
|
||||||
ACORE_ARMORY_WEBSITE_URL: ${ARMORY_WEBSITE_URL:-http://192.168.1.77:48733}
|
|
||||||
ACORE_ARMORY_WEBSITE_ROOT: ${ARMORY_WEBSITE_ROOT:-}
|
|
||||||
|
|
||||||
# Item tooltips and item icons are loaded from this AoWoW instance.
|
|
||||||
# The default is a public third-party site: point this at your own
|
|
||||||
# AoWoW if you do not want that dependency.
|
|
||||||
ACORE_ARMORY_AOWOW_URL: ${ARMORY_AOWOW_URL:-https://wowgaming.altervista.org/aowow}
|
|
||||||
|
|
||||||
# --- embedding -----------------------------------------------------
|
|
||||||
# Set both when embedding the armory in a page on the main site.
|
|
||||||
ACORE_ARMORY_IFRAME_MODE__ENABLED: ${ARMORY_IFRAME_ENABLED:-0}
|
|
||||||
ACORE_ARMORY_IFRAME_MODE__URL: ${ARMORY_IFRAME_URL:-}
|
|
||||||
|
|
||||||
# --- behaviour -----------------------------------------------------
|
|
||||||
ACORE_ARMORY_LOAD_DBCS: ${ARMORY_LOAD_DBCS:-1}
|
|
||||||
# Off by default here: GM accounts on this realm belong to real
|
|
||||||
# players who should appear in the armory like anyone else. Set to
|
|
||||||
# 1 to hide them from search and 404 their character pages.
|
|
||||||
ACORE_ARMORY_HIDE_GAME_MASTERS: ${ARMORY_HIDE_GMS:-0}
|
|
||||||
|
|
||||||
# Playerbots are ordinary accounts, not game masters, so the GM
|
|
||||||
# filter does not touch them. Without this the armory lists ~1350
|
|
||||||
# bots against ~29 real characters.
|
|
||||||
# Comma-separated LIKE patterns. RNDBOT% covers the Playerbots
|
|
||||||
# accounts; ahouse is the auction-house bot, which owns 9 mule
|
|
||||||
# characters. Add further service accounts here as they appear.
|
|
||||||
ACORE_ARMORY_HIDE_BOT_ACCOUNTS: ${ARMORY_HIDE_BOTS:-1}
|
|
||||||
ACORE_ARMORY_BOT_ACCOUNT_PATTERNS: ${ARMORY_BOT_PATTERNS:-RNDBOT%,ahouse}
|
|
||||||
ACORE_ARMORY_TRANSMOG_MODULE: ${ARMORY_TRANSMOG:-0}
|
|
||||||
|
|
||||||
# 0 = serve 3D model assets from the local data directory.
|
|
||||||
# Leave it at 0: the upstream fetch tool can no longer rebuild this
|
|
||||||
# data from the Zam CDN, so the local copy is the only source.
|
|
||||||
ACORE_ARMORY_USE_ZAM_CDN: ${ARMORY_USE_ZAM_CDN:-0}
|
|
||||||
|
|
||||||
# --- realm ---------------------------------------------------------
|
|
||||||
ACORE_ARMORY_REALMS__0__NAME: ${ARMORY_REALM_NAME:-Mythica}
|
|
||||||
ACORE_ARMORY_REALMS__0__REALM_ID: ${ARMORY_REALM_ID:-1}
|
|
||||||
ACORE_ARMORY_REALMS__0__AUTH_DATABASE: ${ARMORY_AUTH_DB:-acore_auth}
|
|
||||||
ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__HOST: ${ARMORY_DB_HOST:-192.168.1.60}
|
|
||||||
ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__PORT: ${ARMORY_DB_PORT:-3306}
|
|
||||||
ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__USER: ${ARMORY_DB_USER:?set ARMORY_DB_USER}
|
|
||||||
ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__PASSWORD: ${ARMORY_DB_PASS:?set ARMORY_DB_PASS}
|
|
||||||
ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__DATABASE: ${ARMORY_CHARACTERS_DB:-acore_characters}
|
|
||||||
|
|
||||||
# --- world database -------------------------------------------------
|
|
||||||
ACORE_ARMORY_WORLD_DATABASE__HOST: ${ARMORY_DB_HOST:-192.168.1.60}
|
|
||||||
ACORE_ARMORY_WORLD_DATABASE__PORT: ${ARMORY_DB_PORT:-3306}
|
|
||||||
ACORE_ARMORY_WORLD_DATABASE__USER: ${ARMORY_DB_USER:?set ARMORY_DB_USER}
|
|
||||||
ACORE_ARMORY_WORLD_DATABASE__PASSWORD: ${ARMORY_DB_PASS:?set ARMORY_DB_PASS}
|
|
||||||
ACORE_ARMORY_WORLD_DATABASE__DATABASE: ${ARMORY_WORLD_DB:-acore_world}
|
|
||||||
|
|
||||||
ACORE_ARMORY_DB_QUERY_TIMEOUT: ${ARMORY_DB_TIMEOUT:-10000}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
# The 3D model payload, extracted from the release archive. These
|
|
||||||
# four directories are exactly the top level of data.tar.gz.
|
|
||||||
- ${ARMORY_DATA_DIR:-/opt/mythica-armory/data}/meta:/data/meta
|
|
||||||
- ${ARMORY_DATA_DIR:-/opt/mythica-armory/data}/bone:/data/bone
|
|
||||||
- ${ARMORY_DATA_DIR:-/opt/mythica-armory/data}/mo3:/data/mo3
|
|
||||||
- ${ARMORY_DATA_DIR:-/opt/mythica-armory/data}/textures:/data/textures
|
|
||||||
- ${ARMORY_LOG_DIR:-/opt/mythica-armory/logs}:/logs
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "${ARMORY_PORT:-48733}:48733"
|
|
||||||
|
|
@ -17,6 +17,3 @@ services:
|
||||||
networks:
|
networks:
|
||||||
local-private-net:
|
local-private-net:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ac-network:
|
|
||||||
name: ${DOCKER_AC_NETWORK_NAME:-azerothcore-wotlk_ac-network} # default network name if you use the docker setup of ac
|
|
||||||
external: ${DOCKER_AC_NETWORK_EXTERNAL:-false}
|
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
# Deploying the armory on Nox
|
|
||||||
|
|
||||||
This fork of [r-o-b-o-t-o/azerothcore-armory](https://github.com/r-o-b-o-t-o/azerothcore-armory)
|
|
||||||
(MIT) is set up to run as a Portainer stack on Nox, alongside the Mythica web
|
|
||||||
page, reading the AzerothCore databases on the game server over the LAN.
|
|
||||||
|
|
||||||
**It must not run on the AzerothCore VM (192.168.1.60).** That box is tuned for
|
|
||||||
the playerbot load; anything extra there means redoing that balancing. Nothing
|
|
||||||
in this procedure touches the game server — the armory only reads its databases.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why the model data is a manual step
|
|
||||||
|
|
||||||
The armory needs about 2 GB of processed 3D model data — bone sets, mo3 meshes,
|
|
||||||
textures, and the per-race character descriptors.
|
|
||||||
|
|
||||||
Upstream ships a `fetchdata` tool that downloads this from Zam's CDN. **It no
|
|
||||||
longer works.** Zam reorganised that part of the CDN, and every character path
|
|
||||||
now 404s (`meta/character/*`, `meta/charactercustomization2/*` — checked against
|
|
||||||
the `live`, `wrath` and `classic` prefixes and several filename variants). The
|
|
||||||
armor and item paths still resolve, so a run appears to succeed while silently
|
|
||||||
producing no character data, and the app then fails at startup on
|
|
||||||
`data/meta/charactercustomization2/1_0.json`.
|
|
||||||
|
|
||||||
The only remaining source is the snapshot in upstream's v1.0.0 release. That
|
|
||||||
snapshot is mirrored into this fork's own release so the fork does not depend on
|
|
||||||
GitHub staying up for the one piece that cannot be regenerated.
|
|
||||||
|
|
||||||
The mirrored copy is **split into five parts** because Nginx Proxy Manager in
|
|
||||||
front of the forge rejects request bodies at that size (500 MB uploads fine,
|
|
||||||
2 GB returns 413). Raising `client_max_body_size` on the forge's proxy host
|
|
||||||
would let it be stored as a single file, if that is ever worth doing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Put the model data on Nox
|
|
||||||
|
|
||||||
Once. Redeploying the stack does not disturb it.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /opt/mythica-armory/data /opt/mythica-armory/logs
|
|
||||||
sudo chown -R "$USER:$USER" /opt/mythica-armory # else the downloads below fail
|
|
||||||
cd /opt/mythica-armory/data
|
|
||||||
|
|
||||||
BASE=https://gitlab.hallsworth.ca/yrtria/azerothcore-armory/releases/download/v1.0.0
|
|
||||||
for i in 0 1 2 3 4; do curl -LO $BASE/data.tar.gz.part-$i; done
|
|
||||||
curl -LO $BASE/SHA256SUMS.txt
|
|
||||||
|
|
||||||
cat data.tar.gz.part-* > data.tar.gz
|
|
||||||
sha256sum -c --ignore-missing SHA256SUMS.txt # data.tar.gz must say OK
|
|
||||||
|
|
||||||
tar xzf data.tar.gz
|
|
||||||
rm -f data.tar.gz data.tar.gz.part-*
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterwards `/opt/mythica-armory/data` holds four directories — `meta`, `bone`,
|
|
||||||
`mo3`, `textures` — totalling roughly 2.5 GB unpacked.
|
|
||||||
|
|
||||||
Ownership by your own user is fine — the container reads these as root either
|
|
||||||
way, and it keeps the download and extract steps out of `sudo`.
|
|
||||||
|
|
||||||
The DBC `.csv` files are **not** part of this. They are committed to the repo and
|
|
||||||
travel in the image, so they stay in step with the code.
|
|
||||||
|
|
||||||
> `data.tar.gz.UPSTREAM-GITHUB-LINK.txt` on the same release is not the archive.
|
|
||||||
> It is the original external reference Forgejo recorded during migration — a
|
|
||||||
> short-lived signed GitHub URL, kept only for provenance.
|
|
||||||
|
|
||||||
## 2. Create the Portainer stack
|
|
||||||
|
|
||||||
Repository stack, same pattern as `wow-web-page`:
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| Repository URL | `https://gitlab.hallsworth.ca/yrtria/azerothcore-armory.git` |
|
|
||||||
| Reference | `refs/heads/master` |
|
|
||||||
| Compose path | `docker-compose.mythica.yml` |
|
|
||||||
|
|
||||||
Environment variables — `ARMORY_DB_USER` and `ARMORY_DB_PASS` are required and
|
|
||||||
the stack refuses to start without them:
|
|
||||||
|
|
||||||
| Variable | Value |
|
|
||||||
|---|---|
|
|
||||||
| `ARMORY_DB_USER` | database user (see §4) |
|
|
||||||
| `ARMORY_DB_PASS` | its password |
|
|
||||||
| `ARMORY_DB_HOST` | `192.168.1.60` |
|
|
||||||
| `ARMORY_WEBSITE_URL` | `https://armory.dionysismedia.ca` once proxied, otherwise `http://192.168.1.77:48733` |
|
|
||||||
| `ARMORY_WEBSITE_NAME` | `Mythica` |
|
|
||||||
| `ARMORY_REALM_ID` | `1` |
|
|
||||||
| `ARMORY_DATA_DIR` | `/opt/mythica-armory/data` |
|
|
||||||
| `ARMORY_LOG_DIR` | `/opt/mythica-armory/logs` |
|
|
||||||
|
|
||||||
The first build compiles TypeScript, so it takes a few minutes. It listens on
|
|
||||||
48733.
|
|
||||||
|
|
||||||
## 3. Reverse proxy
|
|
||||||
|
|
||||||
Add a proxy host in NPM pointing at `192.168.1.77:48733`.
|
|
||||||
|
|
||||||
To embed it in the main site instead of giving it its own subdomain, set
|
|
||||||
`ARMORY_IFRAME_ENABLED=1` and `ARMORY_IFRAME_URL` to the page that hosts the
|
|
||||||
iframe, then use the snippet in the upstream README. That path keeps the site's
|
|
||||||
own header and navigation around it.
|
|
||||||
|
|
||||||
## 4. Database access
|
|
||||||
|
|
||||||
`acore` is already granted from `192.168.1.%` and MySQL on the game server binds
|
|
||||||
`0.0.0.0`, so this works with no changes. But the armory only ever reads, and it
|
|
||||||
is public-facing, so a dedicated read-only user is worth the two minutes:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE USER 'armory'@'192.168.1.%' IDENTIFIED BY '<password>';
|
|
||||||
GRANT SELECT ON acore_characters.* TO 'armory'@'192.168.1.%';
|
|
||||||
GRANT SELECT ON acore_world.* TO 'armory'@'192.168.1.%';
|
|
||||||
GRANT SELECT ON acore_auth.* TO 'armory'@'192.168.1.%';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Things to know
|
|
||||||
|
|
||||||
- **Item icons and tooltips come from a third-party AoWoW instance**
|
|
||||||
(`wowgaming.altervista.org` by default) — they are hotlinked, so they break if
|
|
||||||
that site moves or goes down. Point `ARMORY_AOWOW_URL` at your own AoWoW to
|
|
||||||
remove that dependency. This is the one remaining external asset dependency;
|
|
||||||
the model data is now self-hosted.
|
|
||||||
- **The armory has its own talent view** with dual-spec support, which overlaps
|
|
||||||
the talent page already in `WoW-Web-Page`. Decide which one owns the character
|
|
||||||
page rather than shipping both.
|
|
||||||
- **The Dockerfile builds from `node:16`**, which is end-of-life. It builds and
|
|
||||||
runs fine; bumping it is a separate change from getting this deployed.
|
|
||||||
- **`hideGameMasters` is off** in this stack, unlike upstream. GM accounts on
|
|
||||||
this realm belong to real players — `Spinnaker` among them — so hiding them
|
|
||||||
would hide people who should be listed. Set `ARMORY_HIDE_GMS=1` to restore
|
|
||||||
upstream behaviour.
|
|
||||||
- **Service accounts are hidden** via `hideBotAccounts` / `botAccountPatterns`,
|
|
||||||
a comma-separated list of LIKE patterns defaulting to `RNDBOT%,ahouse` — the
|
|
||||||
Playerbots accounts and the auction-house bot, which owns 9 mule characters.
|
|
||||||
None of them are game masters, so the GM filter never touched them. Add more
|
|
||||||
with `ARMORY_BOT_PATTERNS`, or set `ARMORY_HIDE_BOTS=0` to show them all.
|
|
||||||
3498
package-lock.json
generated
38
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "azerothcore-armory",
|
"name": "azerothcore-armory",
|
||||||
"version": "1.1.1",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
"start": "node --expose-gc build/armory/main.js",
|
"start": "node --expose-gc build/armory/main.js",
|
||||||
"watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"",
|
"watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"",
|
||||||
"nodemon": "nodemon -q -w build -w config.json build/armory/main.js",
|
"nodemon": "nodemon -q -w build -w config.json build/armory/main.js",
|
||||||
"lint": "eslint src/",
|
|
||||||
"prettier": "prettier . -w",
|
|
||||||
"fetchdata": "node build/tools/fetchdata.js",
|
"fetchdata": "node build/tools/fetchdata.js",
|
||||||
"cleardata": "rimraf data/bone data/meta data/mo3 data/textures"
|
"cleardata": "rimraf data/bone data/meta data/mo3 data/textures"
|
||||||
},
|
},
|
||||||
|
|
@ -17,33 +15,31 @@
|
||||||
"homepage": "https://github.com/r-o-b-o-t-o/azerothcore-armory",
|
"homepage": "https://github.com/r-o-b-o-t-o/azerothcore-armory",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.9.2",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/glob": "^7.2.0",
|
"@types/glob": "^7.2.0",
|
||||||
"@types/mkdirp": "^1.0.2",
|
"@types/mkdirp": "^1.0.2",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/node": "^18.0.0",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.1",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^1.0.3",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
"concurrently": "^7.0.0",
|
||||||
"concurrently": "^7.2.2",
|
"nodemon": "^2.0.15",
|
||||||
"eslint": "^8.18.0",
|
|
||||||
"nodemon": "^2.0.18",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.5.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supercharge/promise-pool": "^2.3.0",
|
"@supercharge/promise-pool": "^2.1.0",
|
||||||
"camelcase": "^6.3.0",
|
"camelcase": "^6.3.0",
|
||||||
"cli-progress": "^3.11.2",
|
"cli-progress": "^3.10.0",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.18.1",
|
"express": "^4.17.3",
|
||||||
"express-handlebars": "^6.0.6",
|
"express-handlebars": "^6.0.2",
|
||||||
"glob": "^8.0.3",
|
"glob": "^7.2.0",
|
||||||
|
"glob-promise": "^4.2.2",
|
||||||
"handlebars-helpers": "^0.9.8",
|
"handlebars-helpers": "^0.9.8",
|
||||||
"html-entities": "^2.3.3",
|
"html-entities": "^2.3.2",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
|
|
@ -52,6 +48,6 @@
|
||||||
"pretty-ms": "^7.0.1",
|
"pretty-ms": "^7.0.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"winston": "^3.8.0"
|
"winston": "^3.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { CharacterCustomization } from "./data/CharacterCustomization";
|
||||||
import { IndexController } from "./controllers/IndexController";
|
import { IndexController } from "./controllers/IndexController";
|
||||||
import { CharacterController } from "./controllers/CharacterController";
|
import { CharacterController } from "./controllers/CharacterController";
|
||||||
import { GuildController } from "./controllers/GuildController";
|
import { GuildController } from "./controllers/GuildController";
|
||||||
import { ArenaController } from "./controllers/ArenaController";
|
|
||||||
|
|
||||||
export class Armory {
|
export class Armory {
|
||||||
public characterCustomization: CharacterCustomization;
|
public characterCustomization: CharacterCustomization;
|
||||||
|
|
@ -86,24 +85,22 @@ export class Armory {
|
||||||
websiteRoot: this.config.websiteRoot,
|
websiteRoot: this.config.websiteRoot,
|
||||||
iframeMode: this.config.iframeMode,
|
iframeMode: this.config.iframeMode,
|
||||||
};
|
};
|
||||||
for (const key of Object.keys(locals)) {
|
for (const key in locals) {
|
||||||
|
if (locals.hasOwnProperty(key)) {
|
||||||
app.locals[key] = locals[key];
|
app.locals[key] = locals[key];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
app.locals.locals = locals;
|
app.locals.locals = locals;
|
||||||
|
|
||||||
app.engine(
|
app.engine(".hbs", handlebarsEngine({
|
||||||
".hbs",
|
|
||||||
handlebarsEngine({
|
|
||||||
extname: "hbs",
|
extname: "hbs",
|
||||||
partialsDir: path.join(process.cwd(), "static", "partials"),
|
partialsDir: path.join(process.cwd(), "static", "partials"),
|
||||||
layoutsDir: path.join(process.cwd(), "static"),
|
layoutsDir: path.join(process.cwd(), "static"),
|
||||||
defaultLayout: "layout.hbs",
|
defaultLayout: "layout.hbs",
|
||||||
helpers: {
|
helpers: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
...require("handlebars-helpers")(),
|
...require("handlebars-helpers")(),
|
||||||
},
|
},
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
app.set("view engine", "handlebars");
|
app.set("view engine", "handlebars");
|
||||||
app.set("views", path.join(process.cwd(), "static"));
|
app.set("views", path.join(process.cwd(), "static"));
|
||||||
|
|
||||||
|
|
@ -125,22 +122,20 @@ export class Armory {
|
||||||
}
|
}
|
||||||
return req.socket.remoteAddress;
|
return req.socket.remoteAddress;
|
||||||
});
|
});
|
||||||
app.use(
|
app.use(morgan(":method :url :status - ID :id - IP :ip - :response-time ms", {
|
||||||
morgan(":method :url :status - ID :id - IP :ip - :response-time ms", {
|
|
||||||
stream: {
|
stream: {
|
||||||
write: (msg) => this.logger.http(msg.trim()),
|
write: (msg) => this.logger.http(msg.trim()),
|
||||||
},
|
},
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
app.use("/js", express.static("static/js"));
|
app.use("/js", express.static(`static/js`));
|
||||||
app.use("/css", express.static("static/css"));
|
app.use("/css", express.static(`static/css`));
|
||||||
app.use("/img", express.static("static/img"));
|
app.use("/img", express.static(`static/img`));
|
||||||
app.use("/data/mo3", express.static("data/mo3"));
|
app.use("/data/mo3", express.static(`data/mo3`));
|
||||||
app.use("/data/meta", express.static("data/meta"));
|
app.use("/data/meta", express.static(`data/meta`));
|
||||||
app.use("/data/bone", express.static("data/bone"));
|
app.use("/data/bone", express.static(`data/bone`));
|
||||||
app.use("/data/textures", express.static("data/textures"));
|
app.use("/data/textures", express.static(`data/textures`));
|
||||||
app.use("/data/background.png", express.static("data/modelviewer-background.png"));
|
app.use("/data/background.png", express.static(`data/modelviewer-background.png`));
|
||||||
|
|
||||||
const indexController = new IndexController(this);
|
const indexController = new IndexController(this);
|
||||||
app.get("/", this.wrapRoute(indexController.index.bind(indexController)));
|
app.get("/", this.wrapRoute(indexController.index.bind(indexController)));
|
||||||
|
|
@ -158,11 +153,6 @@ export class Armory {
|
||||||
app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController)));
|
app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController)));
|
||||||
app.get("/guild/:realm/:guild/members", this.wrapRoute(guildsController.members.bind(guildsController)));
|
app.get("/guild/:realm/:guild/members", this.wrapRoute(guildsController.members.bind(guildsController)));
|
||||||
|
|
||||||
const arenaController = new ArenaController(this);
|
|
||||||
app.get("/arena", this.wrapRoute(arenaController.index.bind(arenaController)));
|
|
||||||
app.get("/arena/ladder", this.wrapRoute(arenaController.ladder.bind(arenaController)));
|
|
||||||
app.get("/arena/team/:realm/:name", this.wrapRoute(arenaController.team.bind(arenaController)));
|
|
||||||
|
|
||||||
app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
// Error handler
|
// Error handler
|
||||||
|
|
||||||
|
|
@ -208,14 +198,14 @@ export class Armory {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRealm(realm: string): IRealmConfig {
|
public getRealm(realm: string): IRealmConfig {
|
||||||
return this.config.realms.find((r) => r.name.toLowerCase() === realm.toLowerCase());
|
return this.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDatabaseCharset(realm: string): Promise<string> {
|
public async getDatabaseCharset(realm: string): Promise<string> {
|
||||||
const db = this.getCharactersDb(realm);
|
const db = this.getCharactersDb(realm);
|
||||||
|
|
||||||
if (!(realm in this.charsetCache)) {
|
if (!(realm in this.charsetCache)) {
|
||||||
const [rows] = await db.query({
|
const [rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT CCSA.character_set_name AS charset FROM information_schema.\`TABLES\` T,
|
SELECT CCSA.character_set_name AS charset FROM information_schema.\`TABLES\` T,
|
||||||
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
|
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
|
||||||
|
|
@ -242,7 +232,7 @@ export class Armory {
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void>) {
|
private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<any>) {
|
||||||
// Adds error handling for promise-based controller methods
|
// Adds error handling for promise-based controller methods
|
||||||
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,12 @@ export class Config {
|
||||||
public iframeMode: IIframeModeConfig;
|
public iframeMode: IIframeModeConfig;
|
||||||
public loadDbcs: boolean;
|
public loadDbcs: boolean;
|
||||||
public hideGameMasters: boolean;
|
public hideGameMasters: boolean;
|
||||||
public hideBotAccounts: boolean;
|
|
||||||
public botAccountPatterns: string;
|
|
||||||
public transmogModule: boolean;
|
|
||||||
public useZamCdn: boolean;
|
|
||||||
public realms: IRealmConfig[];
|
public realms: IRealmConfig[];
|
||||||
public worldDatabase: IDatabaseConfig;
|
public worldDatabase: IDatabaseConfig;
|
||||||
public dbQueryTimeout: number;
|
public dbQueryTimeout: number;
|
||||||
|
|
||||||
private static envPrefix = "ACORE_ARMORY";
|
private static envPrefix: string = "ACORE_ARMORY";
|
||||||
private static checkedMissingField = false;
|
private static checkedMissingField: boolean = false;
|
||||||
|
|
||||||
public static async load(logger: winston.Logger): Promise<Config> {
|
public static async load(logger: winston.Logger): Promise<Config> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -74,13 +70,13 @@ export class Config {
|
||||||
return config as Config;
|
return config as Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName = "") {
|
private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName: string = "") {
|
||||||
if (parentName !== "") {
|
if (parentName !== "") {
|
||||||
parentName += ".";
|
parentName += ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const field in model) {
|
for (const field in model) {
|
||||||
if (!Object.hasOwnProperty.call(model, field)) {
|
if (!model.hasOwnProperty(field)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,9 +85,9 @@ export class Config {
|
||||||
} else if (typeof model[field] === "object") {
|
} else if (typeof model[field] === "object") {
|
||||||
obj[field] = {};
|
obj[field] = {};
|
||||||
Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field);
|
Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field);
|
||||||
} else if (!Object.hasOwnProperty.call(obj, field)) {
|
} else if (!obj.hasOwnProperty(field)) {
|
||||||
const key = Config.getEnvKey(parentName + field);
|
const key = Config.getEnvKey(parentName + field);
|
||||||
if (Object.hasOwnProperty.call(process.env, key)) {
|
if (process.env.hasOwnProperty(key)) {
|
||||||
obj[field] = Config.parseEnvValue(process.env[key], model[field]);
|
obj[field] = Config.parseEnvValue(process.env[key], model[field]);
|
||||||
} else if (!Config.checkedMissingField) {
|
} else if (!Config.checkedMissingField) {
|
||||||
logger.warn(`Config field ${key} is missing from .env!`);
|
logger.warn(`Config field ${key} is missing from .env!`);
|
||||||
|
|
@ -100,16 +96,16 @@ export class Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static loadArrayFromEnv(logger: winston.Logger, model, parentName = ""): unknown[] {
|
private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ""): any[] {
|
||||||
if (parentName !== "") {
|
if (parentName !== "") {
|
||||||
parentName += ".";
|
parentName += ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
const arr = [];
|
const arr = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (;;) {
|
while (true) {
|
||||||
const key = Config.getEnvKey(parentName + i);
|
const key = Config.getEnvKey(parentName + i);
|
||||||
const found = Object.keys(process.env).some((k) => k.startsWith(key));
|
const found = Object.keys(process.env).some(k => k.startsWith(key));
|
||||||
if (!found) {
|
if (!found) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -119,10 +115,10 @@ export class Config {
|
||||||
} else if (typeof model === "object") {
|
} else if (typeof model === "object") {
|
||||||
const obj = {};
|
const obj = {};
|
||||||
Config.loadObjFromEnv(logger, obj, model, parentName + i);
|
Config.loadObjFromEnv(logger, obj, model, parentName + i);
|
||||||
if (Object.keys(obj).length) {
|
if (Object.keys(obj).length > 0) {
|
||||||
arr.push(obj);
|
arr.push(obj);
|
||||||
}
|
}
|
||||||
} else if (Object.hasOwnProperty.call(process.env, key)) {
|
} else if (process.env.hasOwnProperty(key)) {
|
||||||
arr.push(Config.parseEnvValue(process.env[key], model));
|
arr.push(Config.parseEnvValue(process.env[key], model));
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|
@ -135,17 +131,13 @@ export class Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getEnvKey(key: string): string {
|
private static getEnvKey(key: string): string {
|
||||||
return (
|
return Config.envPrefix + "_" + key
|
||||||
Config.envPrefix +
|
|
||||||
"_" +
|
|
||||||
key
|
|
||||||
.replace(/\./g, "__")
|
.replace(/\./g, "__")
|
||||||
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
||||||
.toUpperCase()
|
.toUpperCase();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseEnvValue(value: string, model: boolean | number | string): boolean | number | string {
|
private static parseEnvValue(value: string, model: any): any {
|
||||||
const type = typeof model;
|
const type = typeof model;
|
||||||
const lower = value.toLowerCase();
|
const lower = value.toLowerCase();
|
||||||
if (type === "boolean") {
|
if (type === "boolean") {
|
||||||
|
|
@ -157,7 +149,7 @@ export class Config {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName = "") {
|
private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName: string = "") {
|
||||||
const missing = Config.hasMissingFields(obj, model);
|
const missing = Config.hasMissingFields(obj, model);
|
||||||
if (parentName !== "") {
|
if (parentName !== "") {
|
||||||
parentName += ".";
|
parentName += ".";
|
||||||
|
|
@ -165,16 +157,20 @@ export class Config {
|
||||||
for (const field of missing) {
|
for (const field of missing) {
|
||||||
logger.warn(`Field ${parentName}${field} is missing from config.json!`);
|
logger.warn(`Field ${parentName}${field} is missing from config.json!`);
|
||||||
}
|
}
|
||||||
for (const key of Object.keys(model)) {
|
for (const key in model) {
|
||||||
if (typeof model[key] === "object" && Object.hasOwnProperty.call(obj, key)) {
|
if (typeof model[key] === "object" && obj.hasOwnProperty(key)) {
|
||||||
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
|
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static hasMissingFields(obj: object, model: object): string[] {
|
private static hasMissingFields(obj: object, model: object): string[] {
|
||||||
const objProp = Object.keys(obj);
|
const missing = [];
|
||||||
const missingProps = Object.keys(model).filter((key) => !objProp.includes(key));
|
for (const key in model) {
|
||||||
return missingProps;
|
if (!obj.hasOwnProperty(key)) {
|
||||||
|
missing.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { Pool, RowDataPacket } from "mysql2/promise";
|
import { Pool } from "mysql2/promise";
|
||||||
import { Query } from "express-serve-static-core";
|
import { Query } from "express-serve-static-core";
|
||||||
|
|
||||||
export interface IResult {
|
export interface IResult {
|
||||||
recordsTotal: number;
|
recordsTotal: number;
|
||||||
recordsFiltered: number;
|
recordsFiltered: number;
|
||||||
draw: number;
|
draw: number;
|
||||||
data: unknown[][];
|
data: any[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IColumnSettings {
|
export interface IColumnSettings {
|
||||||
name: string;
|
name: string;
|
||||||
collation?: string;
|
collation?: string;
|
||||||
formatter?: (data: string | number | null, row: unknown) => string;
|
formatter?: (data: string | number | null, row: any) => string;
|
||||||
table?: string;
|
table?: string;
|
||||||
database?: string;
|
database?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,6 @@ export interface IColumnJoin {
|
||||||
column2: string;
|
column2: string;
|
||||||
database2?: string;
|
database2?: string;
|
||||||
kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT";
|
kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT";
|
||||||
where?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataTablesSsp {
|
export class DataTablesSsp {
|
||||||
|
|
@ -39,44 +38,41 @@ export class DataTablesSsp {
|
||||||
private start: number;
|
private start: number;
|
||||||
private length: number;
|
private length: number;
|
||||||
private _order: {
|
private _order: {
|
||||||
column: number;
|
column: number,
|
||||||
dir: string;
|
dir: string,
|
||||||
}[];
|
}[];
|
||||||
private columns: {
|
private columns: {
|
||||||
data: number;
|
data: number,
|
||||||
name: string;
|
name: string,
|
||||||
searchable: boolean;
|
searchable: boolean,
|
||||||
orderable: boolean;
|
orderable: boolean,
|
||||||
search: {
|
search: {
|
||||||
value: string;
|
value: string,
|
||||||
regex: boolean;
|
regex: boolean,
|
||||||
};
|
}
|
||||||
}[];
|
}[];
|
||||||
private search: {
|
private search: {
|
||||||
value: string;
|
value: string,
|
||||||
regex: boolean;
|
regex: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
private wheres: string[] = [];
|
private wheres: string[] = [];
|
||||||
private filterBindings: (string | number)[] = [];
|
private filterBindings: (string | number)[] = [];
|
||||||
private customBindings: (string | number)[] = [];
|
private customBindings: (string | number)[] = [];
|
||||||
private filterWhereSql = "1";
|
private filterWhereSql: string = "1";
|
||||||
private customWhereSql = "1";
|
private customWhereSql: string = "1";
|
||||||
private limitSql = "";
|
private limitSql: string = "";
|
||||||
private orderSql = "";
|
private orderSql: string = "";
|
||||||
private joinSql = "";
|
private joinSql: string = "";
|
||||||
|
|
||||||
public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
|
public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
|
||||||
this.start = Math.max(0, parseInt(query.start as string, 10));
|
this.start = parseInt(query.start as string, 10);
|
||||||
this.length = Math.min(100, Math.max(0, parseInt(query.length as string, 10)));
|
this.length = parseInt(query.length as string, 10);
|
||||||
this.draw = parseInt(query.draw as string, 10);
|
this.draw = parseInt(query.draw as string, 10);
|
||||||
this._order = (query.order as { column: string; dir: string }[]).map((order) => {
|
this._order = (query.order as { column: string, dir: string }[])
|
||||||
const dir = order.dir.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
.map(order => { return { column: parseInt(order.column, 10), dir: order.dir, }; });
|
||||||
return { column: parseInt(order.column, 10), dir };
|
this.columns = (query.columns as { data: string, name: string, searchable: string, orderable: string, search: any }[])
|
||||||
});
|
.map(column => {
|
||||||
this.columns = (
|
|
||||||
query.columns as { data: string; name: string; searchable: string; orderable: string; search: { value: string; regex: string } }[]
|
|
||||||
).map((column) => {
|
|
||||||
return {
|
return {
|
||||||
data: parseInt(column.data, 10),
|
data: parseInt(column.data, 10),
|
||||||
name: column.name,
|
name: column.name,
|
||||||
|
|
@ -86,8 +82,8 @@ export class DataTablesSsp {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.search = {
|
this.search = {
|
||||||
value: (query.search as unknown)["value"] as string,
|
value: (query.search as any).value as string,
|
||||||
regex: (query.search as unknown)["regex"] === "true",
|
regex: (query.search as any).regex === "true",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
|
@ -97,7 +93,7 @@ export class DataTablesSsp {
|
||||||
}
|
}
|
||||||
|
|
||||||
private colSettingsToStr(colSettings: IColumnSettings) {
|
private colSettingsToStr(colSettings: IColumnSettings) {
|
||||||
const db = colSettings.database ? "`" + colSettings.database + "`." : "";
|
const db = colSettings.database ? ("`" + colSettings.database + "`.") : "";
|
||||||
return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``;
|
return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,9 +130,8 @@ export class DataTablesSsp {
|
||||||
|
|
||||||
private join() {
|
private join() {
|
||||||
for (const join of this.joins) {
|
for (const join of this.joins) {
|
||||||
const db2 = join.database2 ? `\`${join.database2}\`.` : "";
|
const db2 = join.database2 ? "`" + join.database2 + "`." : "";
|
||||||
const where = join.where ? ` ${join.where}` : "";
|
this.joinSql += `${join.kind} JOIN ${db2}\`${join.table2}\` ON ${db2}\`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`\n`;
|
||||||
this.joinSql += `${join.kind} JOIN ${db2}\`${join.table2}\` ON ${db2}\`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`${where}\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -158,16 +153,19 @@ export class DataTablesSsp {
|
||||||
this.filterBindings.push(`%${this.search.value}%`);
|
this.filterBindings.push(`%${this.search.value}%`);
|
||||||
}
|
}
|
||||||
if (filterWheres.length > 0) {
|
if (filterWheres.length > 0) {
|
||||||
this.filterWhereSql = "(" + filterWheres.map((w) => `(${w})`).join(" OR ") + ")";
|
this.filterWhereSql = "(" + filterWheres.map(w => `(${w})`).join(" OR ") + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.customWhereSql = this.wheres.map((w) => `(${w})`).join(" AND ");
|
this.customWhereSql = this.wheres.map(w => `(${w})`).join(" AND ");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sql(): string {
|
public sql(): string {
|
||||||
const columns = [...this.columnSettings.map((c) => this.colSettingsToStr(c)), ...this.extraDataColumns];
|
const columns = [
|
||||||
|
...this.columnSettings.map(c => this.colSettingsToStr(c)),
|
||||||
|
...this.extraDataColumns,
|
||||||
|
];
|
||||||
return `
|
return `
|
||||||
SELECT ${columns.join(", ")}
|
SELECT ${columns.join(", ")}
|
||||||
FROM ${this.table}
|
FROM ${this.table}
|
||||||
|
|
@ -200,32 +198,35 @@ export class DataTablesSsp {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(queryTimeout = 10_000): Promise<IResult> {
|
public async run(queryTimeout: number = 10_000): Promise<IResult> {
|
||||||
this.limit().order().join().filter();
|
this.limit()
|
||||||
|
.order()
|
||||||
|
.join()
|
||||||
|
.filter();
|
||||||
|
|
||||||
const bindings = [...this.filterBindings, ...this.customBindings];
|
const bindings = [...this.filterBindings, ...this.customBindings];
|
||||||
|
|
||||||
let [rows] = await this.db.query({
|
let [rows, fields] = await this.db.query({
|
||||||
sql: this.buildTotalCountSql(),
|
sql: this.buildTotalCountSql(),
|
||||||
values: this.customBindings,
|
values: this.customBindings,
|
||||||
timeout: queryTimeout,
|
timeout: queryTimeout,
|
||||||
});
|
});
|
||||||
const recordsTotal = rows[0].count;
|
const recordsTotal = rows[0].count;
|
||||||
|
|
||||||
[rows] = await this.db.query({
|
[rows, fields] = await this.db.query({
|
||||||
sql: this.buildFilteredCountSql(),
|
sql: this.buildFilteredCountSql(),
|
||||||
values: bindings,
|
values: bindings,
|
||||||
timeout: queryTimeout,
|
timeout: queryTimeout,
|
||||||
});
|
});
|
||||||
const recordsFiltered = rows[0].count;
|
const recordsFiltered = rows[0].count;
|
||||||
|
|
||||||
[rows] = await this.db.query({
|
[rows, fields] = await this.db.query({
|
||||||
sql: this.sql(),
|
sql: this.sql(),
|
||||||
rowsAsArray: true,
|
rowsAsArray: true,
|
||||||
values: bindings,
|
values: bindings,
|
||||||
timeout: queryTimeout,
|
timeout: queryTimeout,
|
||||||
});
|
});
|
||||||
rows = (rows as RowDataPacket[]).map((row) => {
|
rows = (rows as any[][]).map(row => {
|
||||||
for (let i = 0; i < this.columnSettings.length; ++i) {
|
for (let i = 0; i < this.columnSettings.length; ++i) {
|
||||||
const col = this.columnSettings[i];
|
const col = this.columnSettings[i];
|
||||||
if (col.formatter !== undefined) {
|
if (col.formatter !== undefined) {
|
||||||
|
|
@ -239,7 +240,7 @@ export class DataTablesSsp {
|
||||||
recordsTotal,
|
recordsTotal,
|
||||||
recordsFiltered,
|
recordsFiltered,
|
||||||
draw: this.draw,
|
draw: this.draw,
|
||||||
data: rows as unknown[][],
|
data: rows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,16 +41,7 @@ export class Utils {
|
||||||
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
|
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static makeEmblemObject(
|
public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem {
|
||||||
obj: {
|
|
||||||
background: number;
|
|
||||||
emblemStyle: number;
|
|
||||||
emblemColor: number;
|
|
||||||
borderStyle: number;
|
|
||||||
borderColor: number;
|
|
||||||
},
|
|
||||||
padWithZeroes = true,
|
|
||||||
): IEmblem {
|
|
||||||
const padLength = padWithZeroes ? 2 : 0;
|
const padLength = padWithZeroes ? 2 : 0;
|
||||||
return {
|
return {
|
||||||
icon: obj.emblemStyle.toString().padStart(padLength, "0"),
|
icon: obj.emblemStyle.toString().padStart(padLength, "0"),
|
||||||
|
|
@ -60,37 +51,4 @@ export class Utils {
|
||||||
background: obj.background.toString().padStart(padLength, "0"),
|
background: obj.background.toString().padStart(padLength, "0"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Quote a configuration-supplied string for inline use in SQL.
|
|
||||||
*
|
|
||||||
* The bot-account filter has to be spliced into join clauses that the query
|
|
||||||
* builder emits as raw SQL, so it cannot be passed as a bound parameter the
|
|
||||||
* way a normal value would be. The value comes from the operator's own
|
|
||||||
* config rather than from a request, but quoting it keeps a stray apostrophe
|
|
||||||
* from producing a broken or surprising query.
|
|
||||||
*/
|
|
||||||
public static quoteSqlString(value: string): string {
|
|
||||||
return `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the LIKE test that identifies non-player accounts.
|
|
||||||
*
|
|
||||||
* `patterns` is a comma-separated list, so a realm can name several service
|
|
||||||
* accounts at once — Playerbots plus an auction-house bot, say. Returns an
|
|
||||||
* empty string when nothing is configured, which callers treat as "do not
|
|
||||||
* filter" rather than emitting an always-false clause.
|
|
||||||
*/
|
|
||||||
public static botAccountFilter(column: string, patterns: string): string {
|
|
||||||
const list = String(patterns ?? "")
|
|
||||||
.split(",")
|
|
||||||
.map((p) => p.trim())
|
|
||||||
.filter((p) => p.length > 0);
|
|
||||||
|
|
||||||
if (list.length === 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `(${list.map((p) => `${column} LIKE ${Utils.quoteSqlString(p)}`).join(" OR ")})`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
import * as express from "express";
|
|
||||||
import { RowDataPacket } from "mysql2";
|
|
||||||
|
|
||||||
import { Armory } from "../Armory";
|
|
||||||
import { IRealmConfig } from "../Config";
|
|
||||||
import { IEmblem, Utils } from "../Utils";
|
|
||||||
import { DataTablesSsp } from "../DataTablesSsp";
|
|
||||||
|
|
||||||
interface ITeamMemberData {
|
|
||||||
name: string;
|
|
||||||
weekGames: number;
|
|
||||||
weekWins: number;
|
|
||||||
seasonGames: number;
|
|
||||||
seasonWins: number;
|
|
||||||
personalRating: number;
|
|
||||||
race: number;
|
|
||||||
class: number;
|
|
||||||
gender: string;
|
|
||||||
online: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ITeamData {
|
|
||||||
name: string;
|
|
||||||
captainGuid: number;
|
|
||||||
type: number;
|
|
||||||
rating: number;
|
|
||||||
seasonGames: number;
|
|
||||||
seasonWins: number;
|
|
||||||
weekGames: number;
|
|
||||||
weekWins: number;
|
|
||||||
emblem: IEmblem;
|
|
||||||
members: ITeamMemberData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ArenaController {
|
|
||||||
private armory: Armory;
|
|
||||||
|
|
||||||
public constructor(armory: Armory) {
|
|
||||||
this.armory = armory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async index(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
|
||||||
res.render("ladder-arena.hbs", {
|
|
||||||
title: `Arena Ladder`,
|
|
||||||
realms: this.armory.config.realms.map((r) => r.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async team(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
|
||||||
const realmName = req.params.realm;
|
|
||||||
const teamName = req.params.name;
|
|
||||||
|
|
||||||
const realm = this.armory.getRealm(realmName);
|
|
||||||
if (realm === undefined) {
|
|
||||||
// Could not find realm
|
|
||||||
return next(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamData = await this.getTeamData(realm, teamName);
|
|
||||||
if (teamData === null) {
|
|
||||||
// Could not find team
|
|
||||||
return next(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render("arena-team.hbs", {
|
|
||||||
title: `Armory - ${teamData.name}`,
|
|
||||||
realm: realm.name,
|
|
||||||
...teamData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ladder(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
|
||||||
const realmName = req.query.realm as string;
|
|
||||||
const realm = realmName === undefined ? this.armory.config.realms[0] : this.armory.config.realms.find((r) => r.name === realmName);
|
|
||||||
if (realm === undefined) {
|
|
||||||
return next(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamSize = parseInt(req.query.teamsize as string);
|
|
||||||
if (!(teamSize === 2 || teamSize === 3 || teamSize === 5)) {
|
|
||||||
return next(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
|
||||||
const charSet = await this.armory.getDatabaseCharset(realm.name);
|
|
||||||
|
|
||||||
const ssp = new DataTablesSsp(req.query, db, "arena_team", "arenaTeamId", [
|
|
||||||
{ name: "name", collation: `${charSet}_general_ci` },
|
|
||||||
{ name: "rating" },
|
|
||||||
{ name: "seasonWins" },
|
|
||||||
{ name: "seasonGames" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await ssp.where("`type` = " + teamSize).run(this.armory.config.dbQueryTimeout);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
...result,
|
|
||||||
realm: realm.name,
|
|
||||||
teamSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getTeamData(realm: IRealmConfig, teamName: string): Promise<ITeamData> {
|
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
|
||||||
const [rows] = await db.query({
|
|
||||||
sql: `
|
|
||||||
SELECT arenaTeamId, name, captainGuid, type, rating, seasonGames, seasonWins, weekGames, weekWins, emblemStyle, emblemColor, borderStyle, borderColor, backgroundColor AS background
|
|
||||||
FROM arena_team WHERE name = ?
|
|
||||||
`,
|
|
||||||
values: [teamName],
|
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
|
||||||
});
|
|
||||||
if ((rows as RowDataPacket[]).length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const team = rows[0];
|
|
||||||
|
|
||||||
const [memberRows] = await db.query({
|
|
||||||
sql: `
|
|
||||||
SELECT name, weekGames, weekWins, seasonGames, seasonWins, personalRating, race, class, gender, online
|
|
||||||
FROM arena_team_member
|
|
||||||
LEFT JOIN characters ON arena_team_member.guid = characters.guid
|
|
||||||
WHERE arenaTeamId = ?
|
|
||||||
`,
|
|
||||||
values: [team.arenaTeamId],
|
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
|
||||||
});
|
|
||||||
const members: ITeamMemberData[] = (memberRows as RowDataPacket[]).map((row) => {
|
|
||||||
return {
|
|
||||||
name: row.name,
|
|
||||||
weekGames: row.weekGames,
|
|
||||||
weekWins: row.weekWins,
|
|
||||||
seasonGames: row.seasonGames,
|
|
||||||
seasonWins: row.seasonWins,
|
|
||||||
personalRating: row.personalRating,
|
|
||||||
race: Utils.raceNames[row.race],
|
|
||||||
class: Utils.classNames[row.class],
|
|
||||||
gender: row.gender === 0 ? "male" : "female",
|
|
||||||
online: row.online === 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: team.name,
|
|
||||||
captainGuid: team.captainGuid,
|
|
||||||
type: team.type,
|
|
||||||
rating: team.rating,
|
|
||||||
seasonGames: team.seasonGames,
|
|
||||||
seasonWins: team.seasonWins,
|
|
||||||
weekGames: team.weekGames,
|
|
||||||
weekWins: team.weekWins,
|
|
||||||
emblem: Utils.makeEmblemObject(team, false),
|
|
||||||
members,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import { RowDataPacket } from "mysql2/promise";
|
import { RowDataPacket } from "mysql2/promise";
|
||||||
|
|
||||||
|
import { Utils } from "../Utils";
|
||||||
import { Armory } from "../Armory";
|
import { Armory } from "../Armory";
|
||||||
import { IRealmConfig } from "../Config";
|
import { IRealmConfig } from "../Config";
|
||||||
import { IEmblem, Utils } from "../Utils";
|
import { IAchievement } from "../data/DbcReader";
|
||||||
import { IAchievement as IAchievementDbc } from "../data/DbcReader";
|
|
||||||
|
|
||||||
interface ICharacterData {
|
interface ICharacterData {
|
||||||
guid: number;
|
guid: number;
|
||||||
|
|
@ -27,14 +27,8 @@ interface IEquipmentData {
|
||||||
slot: number;
|
slot: number;
|
||||||
itemEntry: number;
|
itemEntry: number;
|
||||||
flags: number;
|
flags: number;
|
||||||
enchantments: string | number[];
|
enchantments: string;
|
||||||
randomPropertyId: number;
|
randomPropertyId: number;
|
||||||
classId: number;
|
|
||||||
subclassId: number;
|
|
||||||
quality: number;
|
|
||||||
transmog?: number;
|
|
||||||
icon?: number;
|
|
||||||
gems?: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICustomizationOption {
|
interface ICustomizationOption {
|
||||||
|
|
@ -48,30 +42,6 @@ interface IMount {
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAchievement {
|
|
||||||
id: number;
|
|
||||||
category: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
points: number;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IArenaTeam {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
type: number;
|
|
||||||
rating: number;
|
|
||||||
seasonWins: number;
|
|
||||||
seasonGames: number;
|
|
||||||
background: number;
|
|
||||||
emblemStyle: number;
|
|
||||||
emblemColor: number;
|
|
||||||
borderStyle: number;
|
|
||||||
borderColor: number;
|
|
||||||
emblem?: IEmblem;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemClassGem = 3;
|
const ItemClassGem = 3;
|
||||||
const SpellMechanicMounted = 21;
|
const SpellMechanicMounted = 21;
|
||||||
const RaceDisplayName = {
|
const RaceDisplayName = {
|
||||||
|
|
@ -108,7 +78,7 @@ export class CharacterController {
|
||||||
private itemSocketBonuses: { [key: number]: number };
|
private itemSocketBonuses: { [key: number]: number };
|
||||||
private mountSpells: number[];
|
private mountSpells: number[];
|
||||||
private mountBySpellId: { [key: number]: IMount };
|
private mountBySpellId: { [key: number]: IMount };
|
||||||
private achievementById: { [key: number]: IAchievementDbc };
|
private achievementById: { [key: number]: IAchievement };
|
||||||
|
|
||||||
public constructor(armory: Armory) {
|
public constructor(armory: Armory) {
|
||||||
this.armory = armory;
|
this.armory = armory;
|
||||||
|
|
@ -118,7 +88,7 @@ export class CharacterController {
|
||||||
this.itemInventoryTypes = {};
|
this.itemInventoryTypes = {};
|
||||||
const itemsRetail = await this.armory.dbc.itemRetail().toArray();
|
const itemsRetail = await this.armory.dbc.itemRetail().toArray();
|
||||||
for await (const item of this.armory.dbc.item()) {
|
for await (const item of this.armory.dbc.item()) {
|
||||||
const retailItem = itemsRetail.find((row) => row.id === item.id);
|
const retailItem = itemsRetail.find(row => row.id === item.id);
|
||||||
if (retailItem !== undefined) {
|
if (retailItem !== undefined) {
|
||||||
this.itemInventoryTypes[item.id] = retailItem.inventoryType;
|
this.itemInventoryTypes[item.id] = retailItem.inventoryType;
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +107,7 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gemItems = {};
|
this.gemItems = {};
|
||||||
for await (const row of this.armory.dbc.item().filter((item) => item.classId === ItemClassGem)) {
|
for await (const row of this.armory.dbc.item().filter(item => item.classId === ItemClassGem)) {
|
||||||
this.gemItems[row.id] = true;
|
this.gemItems[row.id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +117,7 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.itemSocketBonuses = {};
|
this.itemSocketBonuses = {};
|
||||||
const [rows] = await this.armory.worldDb.query({
|
let [rows, fields] = await this.armory.worldDb.query({
|
||||||
sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0",
|
sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0",
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
|
|
@ -155,17 +125,16 @@ export class CharacterController {
|
||||||
this.itemSocketBonuses[row.entry] = row.socketBonus;
|
this.itemSocketBonuses[row.entry] = row.socketBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mountSpells = await this.armory.dbc
|
const mountSpells = await this.armory.dbc.spell()
|
||||||
.spell()
|
.filter(m => m.mechanic === SpellMechanicMounted)
|
||||||
.filter((m) => m.mechanic === SpellMechanicMounted)
|
|
||||||
.toArray();
|
.toArray();
|
||||||
this.mountSpells = mountSpells.map((spell) => spell.id);
|
this.mountSpells = mountSpells.map(spell => spell.id);
|
||||||
this.mountBySpellId = {};
|
this.mountBySpellId = {};
|
||||||
for (const spell of mountSpells) {
|
for (const spell of mountSpells) {
|
||||||
const mount = await this.armory.dbc.mount().find((m) => m.sourceSpellId === spell.id);
|
const mount = await this.armory.dbc.mount().find(m => m.sourceSpellId === spell.id);
|
||||||
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === spell.spellIconId);
|
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === spell.spellIconId);
|
||||||
if (mount !== undefined) {
|
if (mount !== undefined) {
|
||||||
const display = await this.armory.dbc.mountDisplay().find((d) => d.mountId === mount.id);
|
const display = await this.armory.dbc.mountDisplay().find(d => d.mountId === mount.id);
|
||||||
if (display !== undefined) {
|
if (display !== undefined) {
|
||||||
this.mountBySpellId[spell.id] = {
|
this.mountBySpellId[spell.id] = {
|
||||||
creatureDisplayId: display.creatureDisplayInfoId,
|
creatureDisplayId: display.creatureDisplayInfoId,
|
||||||
|
|
@ -200,27 +169,23 @@ export class CharacterController {
|
||||||
|
|
||||||
const equipmentData = await this.getEquipmentData(realmName, charData.guid);
|
const equipmentData = await this.getEquipmentData(realmName, charData.guid);
|
||||||
const customization = this.getCustomizationOptions(charData);
|
const customization = this.getCustomizationOptions(charData);
|
||||||
const equipment = equipmentData.map((row) => {
|
const equipment = equipmentData.map(row => {
|
||||||
row.icon = this.itemIcons[row.itemEntry];
|
(row as any).icon = this.itemIcons[row.itemEntry];
|
||||||
row.gems = this.getGemsFromEnchantments(row.enchantments as string);
|
(row as any).gems = this.getGemsFromEnchantments(row.enchantments);
|
||||||
row.enchantments = this.filterEnchantments(row.itemEntry, row.enchantments as string);
|
(row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments);
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
const mounts = await this.getMounts(realmName, charData.guid);
|
const mounts = await this.getMounts(realmName, charData.guid);
|
||||||
const transmogs: number[][] = this.armory.config.transmogModule ? [] : undefined;
|
|
||||||
const characterModelItems = await this.getModelViewerItems(equipmentData, charData.class, transmogs);
|
|
||||||
|
|
||||||
res.render("character.hbs", {
|
res.render("character.hbs", {
|
||||||
title: `Armory - ${charData.name}`,
|
title: `Armory - ${charData.name}`,
|
||||||
...this.makeSharedDataObject(realm, charData),
|
...this.makeSharedDataObject(realm, charData),
|
||||||
contentPath: this.armory.config.useZamCdn ? "https://wow.zamimg.com/modelviewer/live/" : this.armory.config.websiteRoot + "/data/",
|
|
||||||
data: {
|
data: {
|
||||||
race: charData.race,
|
race: charData.race,
|
||||||
gender: charData.gender,
|
gender: charData.gender,
|
||||||
class: charData.class,
|
class: charData.class,
|
||||||
flags: charData.playerFlags,
|
flags: charData.playerFlags,
|
||||||
characterModelItems,
|
characterModelItems: await this.getModelViewerItems(equipmentData, charData.class),
|
||||||
characterModelTransmogs: transmogs,
|
|
||||||
customizationOptions: customization,
|
customizationOptions: customization,
|
||||||
equipment,
|
equipment,
|
||||||
mounts,
|
mounts,
|
||||||
|
|
@ -297,7 +262,7 @@ export class CharacterController {
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
categories: await this.armory.dbc.achievementCategory().toArray(),
|
categories: await this.armory.dbc.achievementCategory().toArray(),
|
||||||
...(await this.getAchievements(realm.name, charData)),
|
...await this.getAchievements(realm.name, charData),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +284,6 @@ export class CharacterController {
|
||||||
|
|
||||||
res.render("character-pvp.hbs", {
|
res.render("character-pvp.hbs", {
|
||||||
title: `Armory - ${charData.name} - PvP`,
|
title: `Armory - ${charData.name} - PvP`,
|
||||||
realm: realm.name,
|
|
||||||
...this.makeSharedDataObject(realm, charData),
|
...this.makeSharedDataObject(realm, charData),
|
||||||
faction: Utils.getFactionFromRaceId(charData.race),
|
faction: Utils.getFactionFromRaceId(charData.race),
|
||||||
kills: await this.getPvpKills(realm.name, charData.guid),
|
kills: await this.getPvpKills(realm.name, charData.guid),
|
||||||
|
|
@ -342,32 +306,16 @@ export class CharacterController {
|
||||||
|
|
||||||
private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> {
|
private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> {
|
||||||
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
|
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
|
||||||
|
const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({
|
||||||
// Bot accounts are hidden the same way game masters are: left join the
|
|
||||||
// rows we do not want, then require the join to have missed. Keeping the
|
|
||||||
// pattern inline rather than bound avoids disturbing the positional
|
|
||||||
// values below.
|
|
||||||
const botFilter = this.armory.config.hideBotAccounts
|
|
||||||
? Utils.botAccountFilter("`bot_account`.`username`", this.armory.config.botAccountPatterns)
|
|
||||||
: "";
|
|
||||||
const botJoin =
|
|
||||||
botFilter === ""
|
|
||||||
? ""
|
|
||||||
: `LEFT JOIN \`${realm.authDatabase}\`.\`account\` AS \`bot_account\` ON \`bot_account\`.\`id\` = \`characters\`.\`account\` AND ${botFilter}`;
|
|
||||||
const botWhere = botFilter === "" ? "" : "AND `bot_account`.`id` IS NULL";
|
|
||||||
|
|
||||||
const [rows] = await this.armory.getCharactersDb(realm.name).query({
|
|
||||||
sql: `
|
sql: `
|
||||||
SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`characters\`.\`race\`, \`characters\`.\`class\`, \`characters\`.\`gender\`, \`characters\`.\`level\`, \`characters\`.\`skin\`, \`characters\`.\`face\`, \`characters\`.\`hairStyle\`, \`characters\`.\`hairColor\`, \`characters\`.\`facialStyle\`, \`characters\`.\`playerFlags\`, \`characters\`.\`online\`, \`guild\`.\`name\` AS \`guild\`
|
SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\`
|
||||||
FROM \`characters\`
|
FROM \`characters\`
|
||||||
LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\`
|
LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\`
|
||||||
LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\`
|
LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\`
|
||||||
LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\` AND \`account_access\`.\`RealmID\` IN (-1, ${realm.realmId}) AND \`account_access\`.\`gmlevel\` > 0
|
LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\`
|
||||||
${botJoin}
|
|
||||||
WHERE
|
WHERE
|
||||||
${where}
|
${where}
|
||||||
AND (\`account_access\`.\`id\` IS NULL OR ? = 0)
|
AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0)
|
||||||
${botWhere}
|
|
||||||
`,
|
`,
|
||||||
values: [character, this.armory.config.hideGameMasters ? 1 : 0],
|
values: [character, this.armory.config.hideGameMasters ? 1 : 0],
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
|
|
@ -380,50 +328,21 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
|
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
|
||||||
const transmogSelect = this.armory.config.transmogModule ? ", custom_transmogrification.FakeEntry AS transmog" : "";
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
const transmogJoin = this.armory.config.transmogModule
|
|
||||||
? "LEFT JOIN custom_transmogrification ON custom_transmogrification.GUID = item_instance.guid"
|
|
||||||
: "";
|
|
||||||
let [rows] = await this.armory.getCharactersDb(realm).query({
|
|
||||||
sql: `
|
sql: `
|
||||||
SELECT
|
SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
|
||||||
character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
|
|
||||||
${transmogSelect}
|
|
||||||
FROM character_inventory
|
FROM character_inventory
|
||||||
JOIN item_instance ON item_instance.guid = character_inventory.item
|
JOIN item_instance ON item_instance.guid = character_inventory.item
|
||||||
${transmogJoin}
|
|
||||||
WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)
|
WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)
|
||||||
`,
|
`,
|
||||||
values: [charGuid],
|
values: [charGuid],
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
|
return rows as RowDataPacket[] as IEquipmentData[];
|
||||||
const data = rows as RowDataPacket[] as IEquipmentData[];
|
|
||||||
if (data.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const row of data) {
|
|
||||||
const item = await this.armory.dbc.item().find((item) => item.id === row.itemEntry);
|
|
||||||
row.classId = item.classId;
|
|
||||||
row.subclassId = item.subclassId;
|
|
||||||
}
|
|
||||||
|
|
||||||
[rows] = await this.armory.worldDb.query({
|
|
||||||
sql: "SELECT entry, quality FROM item_template WHERE entry IN (?)",
|
|
||||||
values: [data.map((row) => row.itemEntry)],
|
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
|
||||||
});
|
|
||||||
for (const row of rows as RowDataPacket[]) {
|
|
||||||
const item = data.find((item) => item.itemEntry === row.entry);
|
|
||||||
item.quality = row.quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMounts(realm: string, charGuid: number): Promise<IMount[]> {
|
private async getMounts(realm: string, charGuid: number): Promise<IMount[]> {
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT spell
|
SELECT spell
|
||||||
FROM character_spell
|
FROM character_spell
|
||||||
|
|
@ -433,52 +352,34 @@ export class CharacterController {
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (rows as RowDataPacket[]).map((row) => this.mountBySpellId[row.spell]).filter((m) => m !== undefined);
|
return (rows as RowDataPacket[])
|
||||||
|
.map(row => this.mountBySpellId[row.spell])
|
||||||
|
.filter(m => m !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number, transmogOut?: number[][]): Promise<number[][]> {
|
private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number): Promise<number[][]> {
|
||||||
if (charClass !== 3) {
|
if (charClass !== 3) {
|
||||||
// Keep ranged weapon only if the character is a hunter
|
// Keep ranged weapon only if the character is a hunter
|
||||||
equipmentData = equipmentData.filter((row) => row.slot !== 17);
|
equipmentData = equipmentData.filter(row => row.slot !== 17);
|
||||||
}
|
}
|
||||||
const visibleEquipment = equipmentData.filter(
|
const visibleEquipment = equipmentData.
|
||||||
(item) =>
|
filter(item =>
|
||||||
[0, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 18].includes(item.slot) && // visible slots
|
[0, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 18].includes(item.slot) && // visible slots
|
||||||
item.itemEntry !== 5976, // filter out Guild Tabard (displays blank otherwise)
|
item.itemEntry !== 5976 // filter out Guild Tabard (displays blank otherwise)
|
||||||
);
|
);
|
||||||
|
|
||||||
const items: number[][] = [];
|
const items: number[][] = [];
|
||||||
for (const equipment of visibleEquipment) {
|
for (const equipment of visibleEquipment) {
|
||||||
const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find((row) => row.itemId === equipment.itemEntry);
|
const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find(row => row.itemId === equipment.itemEntry);
|
||||||
if (modifiedAppearance === undefined) {
|
if (modifiedAppearance === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let appearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId);
|
const appearance = await this.armory.dbc.itemAppearance().find(row => row.id === modifiedAppearance.itemAppearanceId);
|
||||||
if (appearance === undefined) {
|
if (appearance === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
appearance = { ...appearance };
|
|
||||||
|
|
||||||
let invType = this.itemInventoryTypes[equipment.itemEntry];
|
items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]);
|
||||||
items.push([invType, appearance.itemDisplayInfoId]);
|
|
||||||
|
|
||||||
if (transmogOut !== undefined) {
|
|
||||||
if (equipment.transmog !== undefined) {
|
|
||||||
if (equipment.transmog === 1) {
|
|
||||||
appearance.itemDisplayInfoId = -1; // Hidden gear piece from transmog
|
|
||||||
} else {
|
|
||||||
const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find((row) => row.itemId === equipment.transmog);
|
|
||||||
if (modifiedAppearance !== undefined) {
|
|
||||||
const tmogAppearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId);
|
|
||||||
if (tmogAppearance !== undefined) {
|
|
||||||
appearance = tmogAppearance;
|
|
||||||
invType = this.itemInventoryTypes[equipment.transmog];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transmogOut.push([invType, appearance.itemDisplayInfoId]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|
@ -488,53 +389,52 @@ export class CharacterController {
|
||||||
return enchantments
|
return enchantments
|
||||||
.trim()
|
.trim()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((enchant) => parseInt(enchant))
|
.map(enchant => parseInt(enchant))
|
||||||
.filter((enchant) => enchant !== 0);
|
.filter(enchant => enchant !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGemsFromEnchantments(enchantments: string): number[] {
|
private getGemsFromEnchantments(enchantments: string): number[] {
|
||||||
return this.parseEnchantmentsString(enchantments)
|
return this.parseEnchantmentsString(enchantments)
|
||||||
.filter((enchant) => enchant in this.enchantSrcItems && this.enchantSrcItems[enchant] in this.gemItems)
|
.filter(enchant => enchant in this.enchantSrcItems && this.enchantSrcItems[enchant] in this.gemItems)
|
||||||
.map((enchant) => this.enchantSrcItems[enchant]);
|
.map(enchant => this.enchantSrcItems[enchant]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterEnchantments(item: number, enchantments: string): number[] {
|
private filterEnchantments(item: number, enchantments: string): number[] {
|
||||||
const socketBonus = this.itemSocketBonuses[item];
|
const socketBonus = this.itemSocketBonuses[item];
|
||||||
return this.parseEnchantmentsString(enchantments).filter(
|
return this.parseEnchantmentsString(enchantments)
|
||||||
(enchant) => enchant in this.enchantSrcItems && !(this.enchantSrcItems[enchant] in this.gemItems) && enchant !== socketBonus,
|
.filter(enchant => enchant in this.enchantSrcItems && !(this.enchantSrcItems[enchant] in this.gemItems) && enchant !== socketBonus);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCustomizationOptions(charData: ICharacterData): ICustomizationOption[] {
|
private getCustomizationOptions(charData: ICharacterData): ICustomizationOption[] {
|
||||||
const data = this.armory.characterCustomization.getCharacterCustomizationData(charData.race, charData.gender);
|
const data = this.armory.characterCustomization.getCharacterCustomizationData(charData.race, charData.gender);
|
||||||
const options = [];
|
const options = [];
|
||||||
const setOptionByChoiceIndex = (optionName: string, choiceIndex: number) => {
|
const setOptionByChoiceIndex = (optionName: string, choiceIndex: number) => {
|
||||||
const option = data["Options"].find((opt) => opt.Name === optionName);
|
const option = data.Options.find(opt => opt.Name === optionName);
|
||||||
if (option !== undefined) {
|
if (option !== undefined) {
|
||||||
const choice = option.Choices.find((choice) => choice.OrderIndex === choiceIndex);
|
const choice = option.Choices.find(choice => choice.OrderIndex === choiceIndex);
|
||||||
if (choice !== undefined) {
|
if (choice !== undefined) {
|
||||||
options.push({ optionId: option.Id, choiceId: choice.Id });
|
options.push({ optionId: option.Id, choiceId: choice.Id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const setOptionByChoiceName = (optionName: string, choiceName: string) => {
|
const setOptionByChoiceName = (optionName: string, choiceName: string) => {
|
||||||
const option = data["Options"].find((opt) => opt.Name === optionName);
|
const option = data.Options.find(opt => opt.Name === optionName);
|
||||||
if (option !== undefined) {
|
if (option !== undefined) {
|
||||||
const choice = option.Choices.find((ch) => ch.Name === choiceName);
|
const choice = option.Choices.find(ch => ch.Name === choiceName);
|
||||||
if (choice !== undefined) {
|
if (choice !== undefined) {
|
||||||
options.push({ optionId: option.Id, choiceId: choice.Id });
|
options.push({ optionId: option.Id, choiceId: choice.Id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const setOptionByChoiceId = (optionName: string, choiceId: number) => {
|
const setOptionByChoiceId = (optionName: string, choiceId: number) => {
|
||||||
const option = data["Options"].find((opt) => opt.Name === optionName);
|
const option = data.Options.find(opt => opt.Name === optionName);
|
||||||
if (option !== undefined) {
|
if (option !== undefined) {
|
||||||
options.push({ optionId: option.Id, choiceId: choiceId });
|
options.push({ optionId: option.Id, choiceId: choiceId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionMapping = {
|
const optionMapping = {
|
||||||
Face: charData.face,
|
"Face": charData.face,
|
||||||
"Skin Color": charData.skin,
|
"Skin Color": charData.skin,
|
||||||
"Hair Style": charData.hairStyle,
|
"Hair Style": charData.hairStyle,
|
||||||
"Hair Color": charData.hairColor,
|
"Hair Color": charData.hairColor,
|
||||||
|
|
@ -547,54 +447,19 @@ export class CharacterController {
|
||||||
switch (charData.race) {
|
switch (charData.race) {
|
||||||
case 1: // Human
|
case 1: // Human
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Mustache", { 0: "Horseshoe", 1: "Brush", 2: "Horseshoe", 3: "None", 4: "Brush", 5: "Brush", 6: "Horseshoe", 7: "Brush", 8: "None" }[charData.facialStyle]);
|
||||||
"Mustache",
|
setOptionByChoiceName("Beard", { 0: "Short", 1: "Chin Puff", 2: "Soul Patch", 3: "Goatee", 4: "Goatee", 5: "None", 6: "Goatee", 7: "None", 8: "None" }[charData.facialStyle]);
|
||||||
{ 0: "Horseshoe", 1: "Brush", 2: "Horseshoe", 3: "None", 4: "Brush", 5: "Brush", 6: "Horseshoe", 7: "Brush", 8: "None" }[
|
setOptionByChoiceName("Sideburns", { 0: "Medium", 1: "None", 2: "None", 3: "Medium", 4: "Long", 5: "Long", 6: "None", 8: "None", 7: "None" }[charData.facialStyle]);
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Beard",
|
|
||||||
{ 0: "Short", 1: "Chin Puff", 2: "Soul Patch", 3: "Goatee", 4: "Goatee", 5: "None", 6: "Goatee", 7: "None", 8: "None" }[
|
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Sideburns",
|
|
||||||
{ 0: "Medium", 1: "None", 2: "None", 3: "Medium", 4: "Long", 5: "Long", 6: "None", 8: "None", 7: "None" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName("Eyebrows", "Natural");
|
setOptionByChoiceName("Eyebrows", "Natural");
|
||||||
setOptionByChoiceName("Face Shape", "Narrow");
|
setOptionByChoiceName("Face Shape", "Narrow");
|
||||||
setOptionByChoiceId(
|
setOptionByChoiceId("Eye Color", { 0: 4138, 1: 4140, 2: 4130, 3: 4136, 4: 4141, 5: 4134, 6: 4130, 7: 4138, 8: 4144, 9: 4135, 10: 4126, 11: 4136 }[charData.face]);
|
||||||
"Eye Color",
|
|
||||||
{ 0: 4138, 1: 4140, 2: 4130, 3: 4136, 4: 4141, 5: 4134, 6: 4130, 7: 4138, 8: 4144, 9: 4135, 10: 4126, 11: 4136 }[charData.face],
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setOptionByChoiceIndex("Piercings", charData.facialStyle);
|
setOptionByChoiceIndex("Piercings", charData.facialStyle);
|
||||||
setOptionByChoiceName("Eyebrows", "Natural");
|
setOptionByChoiceName("Eyebrows", "Natural");
|
||||||
setOptionByChoiceName("Face Shape", "Narrow");
|
setOptionByChoiceName("Face Shape", "Narrow");
|
||||||
setOptionByChoiceName("Makeup", "None");
|
setOptionByChoiceName("Makeup", "None");
|
||||||
setOptionByChoiceName("Necklace", "None");
|
setOptionByChoiceName("Necklace", "None");
|
||||||
setOptionByChoiceId(
|
setOptionByChoiceId("Eye Color", { 0: 4162, 1: 4153, 2: 4161, 3: 4164, 4: 4154, 5: 4160, 6: 4160, 7: 4157, 8: 4152, 9: 4154, 10: 4155, 11: 4165, 12: 4163, 13: 4155, 14: 4151 }[charData.face]);
|
||||||
"Eye Color",
|
|
||||||
{
|
|
||||||
0: 4162,
|
|
||||||
1: 4153,
|
|
||||||
2: 4161,
|
|
||||||
3: 4164,
|
|
||||||
4: 4154,
|
|
||||||
5: 4160,
|
|
||||||
6: 4160,
|
|
||||||
7: 4157,
|
|
||||||
8: 4152,
|
|
||||||
9: 4154,
|
|
||||||
10: 4155,
|
|
||||||
11: 4165,
|
|
||||||
12: 4163,
|
|
||||||
13: 4155,
|
|
||||||
14: 4151,
|
|
||||||
}[charData.face],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (charData.class === 6) {
|
if (charData.class === 6) {
|
||||||
// Death Knight
|
// Death Knight
|
||||||
|
|
@ -606,32 +471,14 @@ export class CharacterController {
|
||||||
setOptionByChoiceIndex("Tattoo Color", 0);
|
setOptionByChoiceIndex("Tattoo Color", 0);
|
||||||
setOptionByChoiceIndex("Eyebrows", 0);
|
setOptionByChoiceIndex("Eyebrows", 0);
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Mustache", { 0: "Trimmed", 1: "Bushy", 2: "Grand", 3: "Thin Braids", 4: "Wise", 5: "Thick Braids", 6: "Fancy", 7: "Bold", 8: "Tied", 9: "None", 10: "None", }[charData.facialStyle]);
|
||||||
"Mustache",
|
|
||||||
{
|
|
||||||
0: "Trimmed",
|
|
||||||
1: "Bushy",
|
|
||||||
2: "Grand",
|
|
||||||
3: "Thin Braids",
|
|
||||||
4: "Wise",
|
|
||||||
5: "Thick Braids",
|
|
||||||
6: "Fancy",
|
|
||||||
7: "Bold",
|
|
||||||
8: "Tied",
|
|
||||||
9: "None",
|
|
||||||
10: "None",
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceIndex("Beard", charData.facialStyle);
|
setOptionByChoiceIndex("Beard", charData.facialStyle);
|
||||||
setOptionByChoiceName("Earrings", "None");
|
setOptionByChoiceName("Earrings", "None");
|
||||||
setOptionByChoiceName("Nose Ring", "None");
|
setOptionByChoiceName("Nose Ring", "None");
|
||||||
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
||||||
} else {
|
} else {
|
||||||
setOptionByChoiceIndex("Earrings", { 0: 0, 1: 1, 2: 2, 3: 3, 4: 0, 5: 4 }[charData.facialStyle]);
|
setOptionByChoiceIndex("Earrings", { 0: 0, 1: 1, 2: 2, 3: 3, 4: 0, 5: 4 }[charData.facialStyle]);
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Piercings", { 0: "None", 1: "None", 2: "None", 3: "None", 4: "Right Nostril", 5: "None" }[charData.facialStyle]);
|
||||||
"Piercings",
|
|
||||||
{ 0: "None", 1: "None", 2: "None", 3: "None", 4: "Right Nostril", 5: "None" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
||||||
}
|
}
|
||||||
if (charData.class === 6) {
|
if (charData.class === 6) {
|
||||||
|
|
@ -661,10 +508,7 @@ export class CharacterController {
|
||||||
setOptionByChoiceName("Ears", "Thin");
|
setOptionByChoiceName("Ears", "Thin");
|
||||||
setOptionByChoiceName("Scars", "None");
|
setOptionByChoiceName("Scars", "None");
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Sideburns", { 0: "None", 1: "Groomed", 2: "None", 3: "Short", 4: "Medium", 5: "Groomed" }[charData.facialStyle]);
|
||||||
"Sideburns",
|
|
||||||
{ 0: "None", 1: "Groomed", 2: "None", 3: "Short", 4: "Medium", 5: "Groomed" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName("Mustache", { 0: "None", 1: "Groomed", 2: "None", 3: "Thin", 4: "None", 5: "None" }[charData.facialStyle]);
|
setOptionByChoiceName("Mustache", { 0: "None", 1: "Groomed", 2: "None", 3: "Thin", 4: "None", 5: "None" }[charData.facialStyle]);
|
||||||
setOptionByChoiceName("Beard", { 0: "None", 1: "Trimmed", 2: "Full", 3: "None", 4: "Short", 5: "Long" }[charData.facialStyle]);
|
setOptionByChoiceName("Beard", { 0: "None", 1: "Trimmed", 2: "Full", 3: "None", 4: "Short", 5: "Long" }[charData.facialStyle]);
|
||||||
setOptionByChoiceName("Eyebrows", { 0: "Shaved", 1: "Short", 2: "Long", 3: "Flat", 4: "Short", 5: "Owl" }[charData.facialStyle]);
|
setOptionByChoiceName("Eyebrows", { 0: "Shaved", 1: "Short", 2: "Long", 3: "Flat", 4: "Short", 5: "Owl" }[charData.facialStyle]);
|
||||||
|
|
@ -694,21 +538,10 @@ export class CharacterController {
|
||||||
setOptionByChoiceName("Horn Decoration", "None");
|
setOptionByChoiceName("Horn Decoration", "None");
|
||||||
setOptionByChoiceName("Tail", charData.gender === 0 ? "Long" : "Short");
|
setOptionByChoiceName("Tail", charData.gender === 0 ? "Long" : "Short");
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Facial Hair", { 0: "Bare", 1: "Bare", 2: "Burns", 3: "Chops", 4: "Mustache", 5: "Soul Patch", 6: "Handlebar", 7: "Bare" }[charData.facialStyle]);
|
||||||
"Facial Hair",
|
setOptionByChoiceName("Tendrils", { 0: "None", 1: "Splayed", 2: "Double", 3: "Fanned", 4: "Single", 5: "Paired", 6: "Uniform", 7: "Twin" }[charData.facialStyle]);
|
||||||
{ 0: "Bare", 1: "Bare", 2: "Burns", 3: "Chops", 4: "Mustache", 5: "Soul Patch", 6: "Handlebar", 7: "Bare" }[
|
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Tendrils",
|
|
||||||
{ 0: "None", 1: "Splayed", 2: "Double", 3: "Fanned", 4: "Single", 5: "Paired", 6: "Uniform", 7: "Twin" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Horns", { 0: "Sweeping", 1: "Curled", 2: "Curved", 3: "Thick", 4: "Wide", 5: "Grand", 6: "Short" }[charData.facialStyle]);
|
||||||
"Horns",
|
|
||||||
{ 0: "Sweeping", 1: "Curled", 2: "Curved", 3: "Thick", 4: "Wide", 5: "Grand", 6: "Short" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (charData.class === 6) {
|
if (charData.class === 6) {
|
||||||
// Death Knight
|
// Death Knight
|
||||||
|
|
@ -724,28 +557,8 @@ export class CharacterController {
|
||||||
setOptionByChoiceName("War Paint", "None");
|
setOptionByChoiceName("War Paint", "None");
|
||||||
setOptionByChoiceName("War Paint Color", "None");
|
setOptionByChoiceName("War Paint Color", "None");
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Beard", { 0: "None", 1: "Stubble", 2: "Thick", 3: "Full", 4: "Tied", 5: "Braid", 6: "Twin Braids", 7: "None", 8: "Ringed", 9: "Split", 10: "Goatee" }[charData.facialStyle]);
|
||||||
"Beard",
|
setOptionByChoiceName("Sideburns", { 0: "None", 1: "None", 2: "Full", 3: "Low", 4: "Full", 5: "None", 6: "None", 7: "Braids", 8: "None", 9: "Full", 10: "Thick" }[charData.facialStyle]);
|
||||||
{
|
|
||||||
0: "None",
|
|
||||||
1: "Stubble",
|
|
||||||
2: "Thick",
|
|
||||||
3: "Full",
|
|
||||||
4: "Tied",
|
|
||||||
5: "Braid",
|
|
||||||
6: "Twin Braids",
|
|
||||||
7: "None",
|
|
||||||
8: "Ringed",
|
|
||||||
9: "Split",
|
|
||||||
10: "Goatee",
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Sideburns",
|
|
||||||
{ 0: "None", 1: "None", 2: "Full", 3: "Low", 4: "Full", 5: "None", 6: "None", 7: "Braids", 8: "None", 9: "Full", 10: "Thick" }[
|
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName("Earrings", "None");
|
setOptionByChoiceName("Earrings", "None");
|
||||||
setOptionByChoiceName("Nose Ring", "None");
|
setOptionByChoiceName("Nose Ring", "None");
|
||||||
setOptionByChoiceName("Tusks", "Natural");
|
setOptionByChoiceName("Tusks", "Natural");
|
||||||
|
|
@ -765,71 +578,13 @@ export class CharacterController {
|
||||||
case 5: // Undead
|
case 5: // Undead
|
||||||
setOptionByChoiceName("Skin Type", "Bony");
|
setOptionByChoiceName("Skin Type", "Bony");
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Jaw Features", { 0: "Intact", 1: "Rot-Kissed", 2: "Intact", 3: "Slackjawed", 4: "Drooler", 5: "Intact", 6: "Slackjawed", 7: "Drooler", 8: "Bonejawed", 9: "Jawsome", 10: "Toothy", 11: "Unhinged", 12: "Cheeky", 13: "Loose", 14: "Intact", 15: "Slackjawed", 16: "Slobber" }[charData.facialStyle]);
|
||||||
"Jaw Features",
|
setOptionByChoiceIndex("Face Features", { 0: 0, 1: 0, 2: 1, 3: 1, 4: 1, 5: 2, 6: 3, 7: 3, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 4, 15: 4, 16: 0 }[charData.facialStyle]);
|
||||||
{
|
setOptionByChoiceId("Eye Color", { 0: 5330, 1: 5330, 2: 6304, 3: 6304, 4: 6304, 5: 5330, 6: 5330, 7: 5330, 8: 5330, 9: 5330, 10: 6304, 11: 6304, 12: 5330, 13: 5330, 14: 5330, 15: 5330, 16: 5330 }[charData.facialStyle]);
|
||||||
0: "Intact",
|
|
||||||
1: "Rot-Kissed",
|
|
||||||
2: "Intact",
|
|
||||||
3: "Slackjawed",
|
|
||||||
4: "Drooler",
|
|
||||||
5: "Intact",
|
|
||||||
6: "Slackjawed",
|
|
||||||
7: "Drooler",
|
|
||||||
8: "Bonejawed",
|
|
||||||
9: "Jawsome",
|
|
||||||
10: "Toothy",
|
|
||||||
11: "Unhinged",
|
|
||||||
12: "Cheeky",
|
|
||||||
13: "Loose",
|
|
||||||
14: "Intact",
|
|
||||||
15: "Slackjawed",
|
|
||||||
16: "Slobber",
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceIndex(
|
|
||||||
"Face Features",
|
|
||||||
{ 0: 0, 1: 0, 2: 1, 3: 1, 4: 1, 5: 2, 6: 3, 7: 3, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 4, 15: 4, 16: 0 }[
|
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceId(
|
|
||||||
"Eye Color",
|
|
||||||
{
|
|
||||||
0: 5330,
|
|
||||||
1: 5330,
|
|
||||||
2: 6304,
|
|
||||||
3: 6304,
|
|
||||||
4: 6304,
|
|
||||||
5: 5330,
|
|
||||||
6: 5330,
|
|
||||||
7: 5330,
|
|
||||||
8: 5330,
|
|
||||||
9: 5330,
|
|
||||||
10: 6304,
|
|
||||||
11: 6304,
|
|
||||||
12: 5330,
|
|
||||||
13: 5330,
|
|
||||||
14: 5330,
|
|
||||||
15: 5330,
|
|
||||||
16: 5330,
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Face Features", { 0: "None", 1: "None", 2: "Strapped", 3: "Rotting", 4: "None", 5: "None", 6: "None", 7: "Putrid" }[charData.facialStyle]);
|
||||||
"Face Features",
|
setOptionByChoiceName("Jaw Features", { 0: "Intact", 1: "Stitched", 2: "Intact", 3: "Intact", 4: "Bonejawed", 5: "Toothy", 6: "Cheeky", 7: "Intact" }[charData.facialStyle]);
|
||||||
{ 0: "None", 1: "None", 2: "Strapped", 3: "Rotting", 4: "None", 5: "None", 6: "None", 7: "Putrid" }[charData.facialStyle],
|
setOptionByChoiceId("Eye Color", { 0: 5337, 1: 5337, 2: 6305, 3: 5337, 4: 5337, 5: 6305, 6: 5337, 7: 5337 }[charData.facialStyle]);
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Jaw Features",
|
|
||||||
{ 0: "Intact", 1: "Stitched", 2: "Intact", 3: "Intact", 4: "Bonejawed", 5: "Toothy", 6: "Cheeky", 7: "Intact" }[
|
|
||||||
charData.facialStyle
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setOptionByChoiceId(
|
|
||||||
"Eye Color",
|
|
||||||
{ 0: 5337, 1: 5337, 2: 6305, 3: 5337, 4: 5337, 5: 6305, 6: 5337, 7: 5337 }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (charData.class === 6) {
|
if (charData.class === 6) {
|
||||||
// Death Knight
|
// Death Knight
|
||||||
|
|
@ -848,18 +603,9 @@ export class CharacterController {
|
||||||
setOptionByChoiceName("Body Paint", "None");
|
setOptionByChoiceName("Body Paint", "None");
|
||||||
setOptionByChoiceIndex("Paint Color", 0);
|
setOptionByChoiceIndex("Paint Color", 0);
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Hair", { 0: "Mane", 1: "Braids", 2: "Chops", 3: "Sideburns", 4: "Mane", 5: "Wrapped", 6: "Braids" }[charData.facialStyle]);
|
||||||
"Hair",
|
setOptionByChoiceName("Facial Hair", { 0: "Clean", 1: "Braid", 2: "Beard", 3: "Wrapped", 4: "Curtain", 5: "Clean", 6: "Split" }[charData.facialStyle]);
|
||||||
{ 0: "Mane", 1: "Braids", 2: "Chops", 3: "Sideburns", 4: "Mane", 5: "Wrapped", 6: "Braids" }[charData.facialStyle],
|
setOptionByChoiceName("Nose Ring", { 0: "None", 1: "Small", 2: "Open", 3: "None", 4: "None", 5: "Bead", 6: "Open" }[charData.facialStyle]);
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Facial Hair",
|
|
||||||
{ 0: "Clean", 1: "Braid", 2: "Beard", 3: "Wrapped", 4: "Curtain", 5: "Clean", 6: "Split" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Nose Ring",
|
|
||||||
{ 0: "None", 1: "Small", 2: "Open", 3: "None", 4: "None", 5: "Bead", 6: "Open" }[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
||||||
} else {
|
} else {
|
||||||
setOptionByChoiceIndex("Hair", charData.facialStyle);
|
setOptionByChoiceIndex("Hair", charData.facialStyle);
|
||||||
|
|
@ -877,38 +623,8 @@ export class CharacterController {
|
||||||
setOptionByChoiceName("Body Paint Color", "None");
|
setOptionByChoiceName("Body Paint Color", "None");
|
||||||
setOptionByChoiceName("Piercing", "None");
|
setOptionByChoiceName("Piercing", "None");
|
||||||
if (charData.gender === 0) {
|
if (charData.gender === 0) {
|
||||||
setOptionByChoiceName(
|
setOptionByChoiceName("Tusks", { 0: "Tusked", 1: "Gougers", 2: "Mammoth", 3: "Spears", 4: "Bridle", 5: "Tusked", 6: "Gougers", 7: "Mammoth", 8: "Spears", 9: "Bridle", 10: "Gougers" }[charData.facialStyle]);
|
||||||
"Tusks",
|
setOptionByChoiceName("Face Paint", { 0: "None", 1: "None", 2: "None", 3: "None", 4: "None", 5: "Berserker", 6: "Fangs", 7: "Mask", 8: "Oni", 9: "Prophet", 10: "War" }[charData.facialStyle]);
|
||||||
{
|
|
||||||
0: "Tusked",
|
|
||||||
1: "Gougers",
|
|
||||||
2: "Mammoth",
|
|
||||||
3: "Spears",
|
|
||||||
4: "Bridle",
|
|
||||||
5: "Tusked",
|
|
||||||
6: "Gougers",
|
|
||||||
7: "Mammoth",
|
|
||||||
8: "Spears",
|
|
||||||
9: "Bridle",
|
|
||||||
10: "Gougers",
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceName(
|
|
||||||
"Face Paint",
|
|
||||||
{
|
|
||||||
0: "None",
|
|
||||||
1: "None",
|
|
||||||
2: "None",
|
|
||||||
3: "None",
|
|
||||||
4: "None",
|
|
||||||
5: "Berserker",
|
|
||||||
6: "Fangs",
|
|
||||||
7: "Mask",
|
|
||||||
8: "Oni",
|
|
||||||
9: "Prophet",
|
|
||||||
10: "War",
|
|
||||||
}[charData.facialStyle],
|
|
||||||
);
|
|
||||||
setOptionByChoiceIndex("Face Paint Color", charData.hairColor + 1);
|
setOptionByChoiceIndex("Face Paint Color", charData.hairColor + 1);
|
||||||
setOptionByChoiceName("Earrings", "None");
|
setOptionByChoiceName("Earrings", "None");
|
||||||
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
setOptionByChoiceIndex("Eye Color", 0); // TODO
|
||||||
|
|
@ -961,7 +677,7 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTalents(realm: string, character: number): Promise<number[][]> {
|
private async getTalents(realm: string, character: number): Promise<number[][]> {
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT spell, specMask
|
SELECT spell, specMask
|
||||||
FROM character_talent
|
FROM character_talent
|
||||||
|
|
@ -985,18 +701,16 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTalentTrees(classId: number) {
|
private async getTalentTrees(classId: number) {
|
||||||
const items = await this.armory.dbc
|
const items = await this.armory.dbc.talentTab()
|
||||||
.talentTab()
|
.filter(tab => tab.classMask === Math.pow(2, classId - 1))
|
||||||
.filter((tab) => tab.classMask === Math.pow(2, classId - 1))
|
.map(async tab => {
|
||||||
.map(async (tab) => {
|
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === tab.spellIconId);
|
||||||
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === tab.spellIconId);
|
const spells = await this.armory.dbc.talent()
|
||||||
const spells = await this.armory.dbc
|
.filter(row => row.tabId === tab.id)
|
||||||
.talent()
|
.map(async row => {
|
||||||
.filter((row) => row.tabId === tab.id)
|
const spell = await this.armory.dbc.spell().find(spell => spell.id === row.spellRank0);
|
||||||
.map(async (row) => {
|
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === spell?.spellIconId);
|
||||||
const spell = await this.armory.dbc.spell().find((spell) => spell.id === row.spellRank0);
|
return { ...row, icon: this.processSpellIconTexture(icon?.textureFilename ?? ""), };
|
||||||
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === spell?.spellIconId);
|
|
||||||
return { ...row, icon: this.processSpellIconTexture(icon?.textureFilename ?? "") };
|
|
||||||
})
|
})
|
||||||
.toArray();
|
.toArray();
|
||||||
return {
|
return {
|
||||||
|
|
@ -1010,11 +724,15 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private processSpellIconTexture(texturePath: string): string {
|
private processSpellIconTexture(texturePath: string): string {
|
||||||
return texturePath.toLowerCase().replace("interface\\icons\\", "").replace("interface\\spellbook\\", "").replace(/\.$/, "");
|
return texturePath
|
||||||
|
.toLowerCase()
|
||||||
|
.replace("interface\\icons\\", "")
|
||||||
|
.replace("interface\\spellbook\\", "")
|
||||||
|
.replace(/\.$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGlyphs(realm: string, character: number): Promise<number[][]> {
|
private async getGlyphs(realm: string, character: number): Promise<any[][]> {
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
|
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
|
||||||
FROM character_glyphs
|
FROM character_glyphs
|
||||||
|
|
@ -1024,11 +742,11 @@ export class CharacterController {
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const glyphs: number[][] = [[], []];
|
const glyphs = [[], []];
|
||||||
for (const row of rows as RowDataPacket[]) {
|
for (const row of rows as RowDataPacket[]) {
|
||||||
const glyphIds = [row.glyph1, row.glyph2, row.glyph3, row.glyph4, row.glyph5, row.glyph6].filter((id) => id !== 0);
|
const glyphIds = [row.glyph1, row.glyph2, row.glyph3, row.glyph4, row.glyph5, row.glyph6].filter(id => id !== 0);
|
||||||
for (const glyphId of glyphIds) {
|
for (const glyphId of glyphIds) {
|
||||||
const glyph = await this.armory.dbc.glyphProperties().find((g) => g.id === glyphId);
|
const glyph = await this.armory.dbc.glyphProperties().find(g => g.id === glyphId);
|
||||||
if (glyph === undefined) {
|
if (glyph === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1039,15 +757,11 @@ export class CharacterController {
|
||||||
return glyphs;
|
return glyphs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAchievements(
|
private async getAchievements(realm: string, charData: ICharacterData): Promise<{ achievements: any[]; earned: { [key: number]: any }; }> {
|
||||||
realm: string,
|
const promises = await this.armory.dbc.achievement()
|
||||||
charData: ICharacterData,
|
.filter(ach => ach.faction === -1 || ach.faction === Utils.getFactionFromRaceId(charData.race))
|
||||||
): Promise<{ achievements: IAchievement[]; earned: { [key: number]: number } }> {
|
|
||||||
const promises = await this.armory.dbc
|
|
||||||
.achievement()
|
|
||||||
.filter((ach) => ach.faction === -1 || ach.faction === Utils.getFactionFromRaceId(charData.race))
|
|
||||||
.map(async (ach) => {
|
.map(async (ach) => {
|
||||||
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === ach.iconId);
|
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === ach.iconId);
|
||||||
return {
|
return {
|
||||||
id: ach.id,
|
id: ach.id,
|
||||||
category: ach.category,
|
category: ach.category,
|
||||||
|
|
@ -1060,7 +774,7 @@ export class CharacterController {
|
||||||
.toArray();
|
.toArray();
|
||||||
const achievements = await Promise.all(promises);
|
const achievements = await Promise.all(promises);
|
||||||
|
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT achievement, date
|
SELECT achievement, date
|
||||||
FROM character_achievement
|
FROM character_achievement
|
||||||
|
|
@ -1069,9 +783,11 @@ export class CharacterController {
|
||||||
values: [charData.guid],
|
values: [charData.guid],
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
const earned: { [key: number]: number } = {};
|
const earned = {};
|
||||||
for (const row of rows as RowDataPacket[]) {
|
for (const row of rows as RowDataPacket[]) {
|
||||||
earned[row.achievement] = row.date;
|
earned[row.achievement] = {
|
||||||
|
date: row.date,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1080,8 +796,8 @@ export class CharacterController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number; today: number; yesterday: number }> {
|
private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number, today: number, yesterday: number }> {
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT totalKills, todayKills, yesterdayKills
|
SELECT totalKills, todayKills, yesterdayKills
|
||||||
FROM characters
|
FROM characters
|
||||||
|
|
@ -1099,8 +815,8 @@ export class CharacterController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getArenaTeams(realm: string, charGuid: number): Promise<IArenaTeam[]> {
|
private async getArenaTeams(realm: string, charGuid: number): Promise<any[]> {
|
||||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT
|
SELECT
|
||||||
arena_team.arenaTeamId AS id, arena_team.name, arena_team.type, arena_team.rating, arena_team.seasonWins, arena_team.seasonGames,
|
arena_team.arenaTeamId AS id, arena_team.name, arena_team.type, arena_team.rating, arena_team.seasonWins, arena_team.seasonGames,
|
||||||
|
|
@ -1114,7 +830,7 @@ export class CharacterController {
|
||||||
timeout: this.armory.config.dbQueryTimeout,
|
timeout: this.armory.config.dbQueryTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (rows as IArenaTeam[]).map((row) => {
|
return (rows as RowDataPacket[]).map(row => {
|
||||||
row.emblem = Utils.makeEmblemObject(row, false);
|
row.emblem = Utils.makeEmblemObject(row, false);
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,16 @@ import * as express from "express";
|
||||||
import { encode } from "html-entities";
|
import { encode } from "html-entities";
|
||||||
import { RowDataPacket } from "mysql2/promise";
|
import { RowDataPacket } from "mysql2/promise";
|
||||||
|
|
||||||
|
import { Utils } from "../Utils";
|
||||||
import { Armory } from "../Armory";
|
import { Armory } from "../Armory";
|
||||||
import { IRealmConfig } from "../Config";
|
import { IRealmConfig } from "../Config";
|
||||||
import { DataTablesSsp } from "../DataTablesSsp";
|
import { DataTablesSsp } from "../DataTablesSsp";
|
||||||
import { Utils, IEmblem, EFaction } from "../Utils";
|
|
||||||
|
|
||||||
interface IGuildRank {
|
interface IGuildRank {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGuildData {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
leader: string;
|
|
||||||
faction: EFaction;
|
|
||||||
emblem: IEmblem;
|
|
||||||
membersCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GuildController {
|
export class GuildController {
|
||||||
private armory: Armory;
|
private armory: Armory;
|
||||||
|
|
||||||
|
|
@ -73,59 +64,37 @@ export class GuildController {
|
||||||
{ name: "name", table: "characters", collation: `${charSet}_general_ci` },
|
{ name: "name", table: "characters", collation: `${charSet}_general_ci` },
|
||||||
{ name: "rank" },
|
{ name: "rank" },
|
||||||
{ name: "level", table: "characters" },
|
{ name: "level", table: "characters" },
|
||||||
{ name: "class", table: "characters", formatter: (cls) => Utils.classNames[cls] },
|
{ name: "class", table: "characters", formatter: cls => Utils.classNames[cls] },
|
||||||
{ name: "race", table: "characters", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
|
{ name: "race", table: "characters", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
|
||||||
{ name: "online", table: "characters", formatter: (online) => online === 1 },
|
{ name: "online", table: "characters", formatter: online => online === 1 },
|
||||||
]);
|
]);
|
||||||
ssp.joins = [{ table1: "guild_member", column1: "guid", table2: "characters", column2: "guid", kind: "LEFT" }];
|
ssp.joins = [
|
||||||
|
{ table1: "guild_member", column1: "guid", table2: "characters", column2: "guid", kind: "LEFT" },
|
||||||
|
];
|
||||||
ssp.extraDataColumns = ["`characters`.`gender`"];
|
ssp.extraDataColumns = ["`characters`.`gender`"];
|
||||||
|
|
||||||
if (this.armory.config.hideGameMasters) {
|
if (this.armory.config.hideGameMasters) {
|
||||||
ssp.joins.push({
|
ssp.joins.push({ table1: "characters", column1: "account", table2: "account_access", column2: "id", database2: realm.authDatabase, kind: "LEFT" });
|
||||||
table1: "characters",
|
ssp = ssp.where(`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`);
|
||||||
column1: "account",
|
|
||||||
table2: "account_access",
|
|
||||||
column2: "id",
|
|
||||||
database2: realm.authDatabase,
|
|
||||||
kind: "LEFT",
|
|
||||||
where: `AND \`account_access\`.\`RealmID\` IN (-1, ${realm.realmId}) AND \`account_access\`.\`gmlevel\` > 0`,
|
|
||||||
});
|
|
||||||
ssp = ssp.where("`account_access`.`id` IS NULL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const botFilter = this.armory.config.hideBotAccounts
|
const result = await ssp
|
||||||
? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns)
|
.where("`guildid` = ?", guildId)
|
||||||
: "";
|
.where("`deleteInfos_Account` IS NULL")
|
||||||
if (botFilter !== "") {
|
.run(this.armory.config.dbQueryTimeout);
|
||||||
ssp.joins.push({
|
|
||||||
table1: "characters",
|
|
||||||
column1: "account",
|
|
||||||
table2: "account",
|
|
||||||
column2: "id",
|
|
||||||
database2: realm.authDatabase,
|
|
||||||
kind: "LEFT",
|
|
||||||
where: `AND ${botFilter}`,
|
|
||||||
});
|
|
||||||
ssp = ssp.where(`\`${realm.authDatabase}\`.\`account\`.\`id\` IS NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ssp.where("`guildid` = ?", guildId).where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout);
|
|
||||||
|
|
||||||
const ranks = await this.getGuildRanks(realm, guildId);
|
const ranks = await this.getGuildRanks(realm, guildId);
|
||||||
const ranksObj: { [key: number]: string } = {};
|
(result as any).ranks = {};
|
||||||
for (const rank of ranks) {
|
for (const rank of ranks) {
|
||||||
ranksObj[rank.id] = encode(rank.name);
|
(result as any).ranks[rank.id] = encode(rank.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json(result);
|
||||||
...result,
|
|
||||||
ranks: ranksObj,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGuildData(realm: IRealmConfig, name: string): Promise<IGuildData> {
|
private async getGuildData(realm: IRealmConfig, name: string): Promise<any> {
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
const db = this.armory.getCharactersDb(realm.name);
|
||||||
let [rows] = await db.query({
|
let [rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background
|
SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background
|
||||||
FROM guild WHERE name = ?
|
FROM guild WHERE name = ?
|
||||||
|
|
@ -138,7 +107,7 @@ export class GuildController {
|
||||||
}
|
}
|
||||||
const guild = rows[0];
|
const guild = rows[0];
|
||||||
|
|
||||||
[rows] = await db.query({
|
[rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT name, race FROM characters
|
SELECT name, race FROM characters
|
||||||
WHERE guid = ?
|
WHERE guid = ?
|
||||||
|
|
@ -148,7 +117,7 @@ export class GuildController {
|
||||||
});
|
});
|
||||||
const leader = rows[0];
|
const leader = rows[0];
|
||||||
|
|
||||||
[rows] = await db.query({
|
[rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT COUNT(guid) AS \`count\` FROM guild_member
|
SELECT COUNT(guid) AS \`count\` FROM guild_member
|
||||||
WHERE guildid = ?
|
WHERE guildid = ?
|
||||||
|
|
@ -170,7 +139,7 @@ export class GuildController {
|
||||||
|
|
||||||
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
|
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
const db = this.armory.getCharactersDb(realm.name);
|
||||||
const [rows] = await db.query({
|
const [rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT guildid
|
SELECT guildid
|
||||||
FROM guild WHERE name = ?
|
FROM guild WHERE name = ?
|
||||||
|
|
@ -187,7 +156,7 @@ export class GuildController {
|
||||||
|
|
||||||
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
|
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
const db = this.armory.getCharactersDb(realm.name);
|
||||||
const [rows] = await db.query({
|
const [rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT guildid
|
SELECT guildid
|
||||||
FROM guild WHERE guildid
|
FROM guild WHERE guildid
|
||||||
|
|
@ -201,7 +170,7 @@ export class GuildController {
|
||||||
|
|
||||||
private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> {
|
private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> {
|
||||||
const db = this.armory.getCharactersDb(realm.name);
|
const db = this.armory.getCharactersDb(realm.name);
|
||||||
const [rows] = await db.query({
|
const [rows, fields] = await db.query({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT rid AS id, rname AS name
|
SELECT rid AS id, rname AS name
|
||||||
FROM guild_rank WHERE guildid = ?
|
FROM guild_rank WHERE guildid = ?
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@ export class IndexController {
|
||||||
public async index(req: express.Request, res: express.Response): Promise<void> {
|
public async index(req: express.Request, res: express.Response): Promise<void> {
|
||||||
res.render("index.hbs", {
|
res.render("index.hbs", {
|
||||||
title: "Armory",
|
title: "Armory",
|
||||||
realms: this.armory.config.realms.map((r) => r.name),
|
realms: this.armory.config.realms.map(r => r.name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||||
const realmName = req.query.realm as string;
|
const realmName = req.query.realm as string;
|
||||||
const realm = realmName === undefined ? this.armory.config.realms[0] : this.armory.config.realms.find((r) => r.name === realmName);
|
const realm = realmName === undefined ?
|
||||||
|
this.armory.config.realms[0] :
|
||||||
|
this.armory.config.realms.find(r => r.name === realmName);
|
||||||
if (realm === undefined) {
|
if (realm === undefined) {
|
||||||
return next(400);
|
return next(400);
|
||||||
}
|
}
|
||||||
|
|
@ -32,9 +34,9 @@ export class IndexController {
|
||||||
{ name: "name", collation: `${charSet}_general_ci` },
|
{ name: "name", collation: `${charSet}_general_ci` },
|
||||||
{ table: "guild", name: "name" },
|
{ table: "guild", name: "name" },
|
||||||
{ name: "level" },
|
{ name: "level" },
|
||||||
{ name: "class", formatter: (cls) => Utils.classNames[cls] },
|
{ name: "class", formatter: cls => Utils.classNames[cls] },
|
||||||
{ name: "race", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
|
{ name: "race", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
|
||||||
{ name: "online", formatter: (online) => online === 1 },
|
{ name: "online", formatter: online => online === 1 },
|
||||||
]);
|
]);
|
||||||
ssp.joins = [
|
ssp.joins = [
|
||||||
{ table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
|
{ table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
|
||||||
|
|
@ -43,39 +45,15 @@ export class IndexController {
|
||||||
ssp.extraDataColumns = ["`characters`.`gender`"];
|
ssp.extraDataColumns = ["`characters`.`gender`"];
|
||||||
|
|
||||||
if (this.armory.config.hideGameMasters) {
|
if (this.armory.config.hideGameMasters) {
|
||||||
ssp.joins.push({
|
ssp.joins.push({ table1: "characters", column1: "account", table2: "account_access", column2: "id", database2: realm.authDatabase, kind: "LEFT" });
|
||||||
table1: "characters",
|
ssp = ssp.where(`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`);
|
||||||
column1: "account",
|
|
||||||
table2: "account_access",
|
|
||||||
column2: "id",
|
|
||||||
database2: realm.authDatabase,
|
|
||||||
kind: "LEFT",
|
|
||||||
where: `AND \`account_access\`.\`RealmID\` IN (-1, ${realm.realmId}) AND \`account_access\`.\`gmlevel\` > 0`,
|
|
||||||
});
|
|
||||||
ssp = ssp.where("`account_access`.`id` IS NULL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const botFilter = this.armory.config.hideBotAccounts
|
const result = await ssp
|
||||||
? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns)
|
.where("`deleteInfos_Account` IS NULL")
|
||||||
: "";
|
.run(this.armory.config.dbQueryTimeout);
|
||||||
if (botFilter !== "") {
|
(result as any).realm = realm.name;
|
||||||
ssp.joins.push({
|
|
||||||
table1: "characters",
|
|
||||||
column1: "account",
|
|
||||||
table2: "account",
|
|
||||||
column2: "id",
|
|
||||||
database2: realm.authDatabase,
|
|
||||||
kind: "LEFT",
|
|
||||||
where: `AND ${botFilter}`,
|
|
||||||
});
|
|
||||||
ssp = ssp.where(`\`${realm.authDatabase}\`.\`account\`.\`id\` IS NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ssp.where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout);
|
res.json(result);
|
||||||
|
|
||||||
res.json({
|
|
||||||
...result,
|
|
||||||
realm: realm.name,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const fsp = fs.promises;
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export class CharacterCustomization {
|
export class CharacterCustomization {
|
||||||
private data: { [key: number]: { [key: number]: unknown } };
|
private data: { [key: number]: { [key: number]: any } };
|
||||||
|
|
||||||
public async loadData(): Promise<void> {
|
public async loadData(): Promise<void> {
|
||||||
this.data = {};
|
this.data = {};
|
||||||
|
|
@ -21,7 +21,7 @@ export class CharacterCustomization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCharacterCustomizationData(race: number, gender: number): unknown {
|
public getCharacterCustomizationData(race: number, gender: number): any {
|
||||||
return this.data[race][gender];
|
return this.data[race][gender];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ export interface IAchievementCategory {
|
||||||
export interface IItemDbc {
|
export interface IItemDbc {
|
||||||
id: number;
|
id: number;
|
||||||
classId: number;
|
classId: number;
|
||||||
subclassId: number;
|
|
||||||
displayInfoId: number;
|
displayInfoId: number;
|
||||||
inventoryType: number;
|
inventoryType: number;
|
||||||
}
|
}
|
||||||
|
|
@ -148,13 +147,11 @@ class AsyncGenWrapper<T> implements IAsyncGeneratorWithArrayMethods<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static from<T>(array: T[]): AsyncGenWrapper<T> {
|
public static from<T>(array: T[]): AsyncGenWrapper<T> {
|
||||||
return new AsyncGenWrapper<T>(
|
return new AsyncGenWrapper<T>(async function* () {
|
||||||
(async function* () {
|
|
||||||
for (const x of array) {
|
for (const x of array) {
|
||||||
yield x;
|
yield x;
|
||||||
}
|
}
|
||||||
})(),
|
}());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
public async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
||||||
|
|
@ -219,15 +216,11 @@ class DbcReader<T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerCols = headerLine.value.map((header) => camelCase(header).replace(/[[\]]/g, ""));
|
const headerCols = headerLine.value
|
||||||
|
.map(header => camelCase(header).replace(/[\[\]]/g, ""));
|
||||||
|
|
||||||
for await (const arr of itr) {
|
for await (const arr of itr) {
|
||||||
const cols = arr.map((value) => {
|
const cols = arr.map(value => isNaN(value as any) ? value : parseInt(value, 10));
|
||||||
if (value === "") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return isNaN(+value) ? value : parseInt(value, 10);
|
|
||||||
});
|
|
||||||
const row = {};
|
const row = {};
|
||||||
headerCols.forEach((header, headerIdx) => {
|
headerCols.forEach((header, headerIdx) => {
|
||||||
if (this.fields.length === 0 || this.fields.includes(header)) {
|
if (this.fields.length === 0 || this.fields.includes(header)) {
|
||||||
|
|
@ -248,8 +241,7 @@ class DbcReader<T> {
|
||||||
const str = chunk.toString();
|
const str = chunk.toString();
|
||||||
// Iterate over each character, keep track of current column (of the returned array)
|
// Iterate over each character, keep track of current column (of the returned array)
|
||||||
for (let c = 0; c < str.length; ++c) {
|
for (let c = 0; c < str.length; ++c) {
|
||||||
const ch = str[c];
|
let ch = str[c], nch = str[c + 1]; // Current character, next character
|
||||||
const nch = str[c + 1]; // Current character, next character
|
|
||||||
if (!(col in arr)) {
|
if (!(col in arr)) {
|
||||||
arr[col] = ""; // Create a new column (start with empty string) if necessary
|
arr[col] = ""; // Create a new column (start with empty string) if necessary
|
||||||
}
|
}
|
||||||
|
|
@ -270,14 +262,14 @@ class DbcReader<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a comma and we're not in a quoted field, move on to the next column
|
// If it's a comma and we're not in a quoted field, move on to the next column
|
||||||
if (ch == "," && !quote) {
|
if (ch == ',' && !quote) {
|
||||||
++col;
|
++col;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
||||||
// and move on to the next row and move to column 0 of that new row
|
// and move on to the next row and move to column 0 of that new row
|
||||||
if (ch == "\r" && nch == "\n" && !quote) {
|
if (ch == '\r' && nch == '\n' && !quote) {
|
||||||
yield arr;
|
yield arr;
|
||||||
arr.length = 0; // Clear the row
|
arr.length = 0; // Clear the row
|
||||||
col = 0;
|
col = 0;
|
||||||
|
|
@ -287,7 +279,7 @@ class DbcReader<T> {
|
||||||
|
|
||||||
// If it's a newline (LF or CR) and we're not in a quoted field,
|
// If it's a newline (LF or CR) and we're not in a quoted field,
|
||||||
// move on to the next row and move to column 0 of that new row
|
// move on to the next row and move to column 0 of that new row
|
||||||
if (!quote && (ch == "\r" || ch == "\n")) {
|
if (!quote && (ch == '\r' || ch == '\n')) {
|
||||||
yield arr;
|
yield arr;
|
||||||
arr.length = 0; // Clear the row
|
arr.length = 0; // Clear the row
|
||||||
col = 0;
|
col = 0;
|
||||||
|
|
@ -324,7 +316,7 @@ const dbcFields = {
|
||||||
achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"],
|
achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"],
|
||||||
achievementCategory: ["id", "parent", "nameLang0"],
|
achievementCategory: ["id", "parent", "nameLang0"],
|
||||||
glyphProperties: ["id", "spellId"],
|
glyphProperties: ["id", "spellId"],
|
||||||
item: ["id", "classId", "subclassId", "displayInfoId", "inventoryType"],
|
item: ["id", "classId", "displayInfoId", "inventoryType"],
|
||||||
itemRetail: ["id", "inventoryType"],
|
itemRetail: ["id", "inventoryType"],
|
||||||
itemAppearance: ["id", "itemDisplayInfoId"],
|
itemAppearance: ["id", "itemDisplayInfoId"],
|
||||||
itemModifiedAppearance: ["id", "itemId", "itemAppearanceId"],
|
itemModifiedAppearance: ["id", "itemId", "itemAppearanceId"],
|
||||||
|
|
@ -334,19 +326,7 @@ const dbcFields = {
|
||||||
spell: ["id", "mechanic", "spellIconId"],
|
spell: ["id", "mechanic", "spellIconId"],
|
||||||
spellItemEnchantment: ["id", "srcItemId"],
|
spellItemEnchantment: ["id", "srcItemId"],
|
||||||
spellIcon: ["id", "textureFilename"],
|
spellIcon: ["id", "textureFilename"],
|
||||||
talent: [
|
talent: ["id", "tabId", "tierId", "columnIndex", "spellRank0", "spellRank1", "spellRank2", "spellRank3", "spellRank4", "prereqTalent0", "prereqRank0"],
|
||||||
"id",
|
|
||||||
"tabId",
|
|
||||||
"tierId",
|
|
||||||
"columnIndex",
|
|
||||||
"spellRank0",
|
|
||||||
"spellRank1",
|
|
||||||
"spellRank2",
|
|
||||||
"spellRank3",
|
|
||||||
"spellRank4",
|
|
||||||
"prereqTalent0",
|
|
||||||
"prereqRank0",
|
|
||||||
],
|
|
||||||
talentTab: ["id", "nameLang0", "spellIconId", "classMask"],
|
talentTab: ["id", "nameLang0", "spellIconId", "classMask"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -369,26 +349,17 @@ export class DbcManager {
|
||||||
|
|
||||||
public async loadAllFiles(): Promise<void> {
|
public async loadAllFiles(): Promise<void> {
|
||||||
this._achievement = await this.read<IAchievement>(DbcFiles.achievement, dbcFields.achievement).toArray();
|
this._achievement = await this.read<IAchievement>(DbcFiles.achievement, dbcFields.achievement).toArray();
|
||||||
this._achievementCategory = await this.read<IAchievementCategory>(
|
this._achievementCategory = await this.read<IAchievementCategory>(DbcFiles.achievementCategory, dbcFields.achievementCategory).toArray();
|
||||||
DbcFiles.achievementCategory,
|
|
||||||
dbcFields.achievementCategory,
|
|
||||||
).toArray();
|
|
||||||
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
||||||
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
||||||
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
||||||
this._itemAppearance = await this.read<IItemAppearanceDbc>(DbcFiles.itemAppearance, dbcFields.itemAppearance).toArray();
|
this._itemAppearance = await this.read<IItemAppearanceDbc>(DbcFiles.itemAppearance, dbcFields.itemAppearance).toArray();
|
||||||
this._itemModifiedAppearance = await this.read<IItemModifiedAppearanceDbc>(
|
this._itemModifiedAppearance = await this.read<IItemModifiedAppearanceDbc>(DbcFiles.itemModifiedAppearance, dbcFields.itemModifiedAppearance).toArray();
|
||||||
DbcFiles.itemModifiedAppearance,
|
|
||||||
dbcFields.itemModifiedAppearance,
|
|
||||||
).toArray();
|
|
||||||
this._itemDisplayInfo = await this.read<IItemDisplayInfoDbc>(DbcFiles.itemDisplayInfo, dbcFields.itemDisplayInfo).toArray();
|
this._itemDisplayInfo = await this.read<IItemDisplayInfoDbc>(DbcFiles.itemDisplayInfo, dbcFields.itemDisplayInfo).toArray();
|
||||||
this._mount = await this.read<IMountDbc>(DbcFiles.mount, dbcFields.mount).toArray();
|
this._mount = await this.read<IMountDbc>(DbcFiles.mount, dbcFields.mount).toArray();
|
||||||
this._mountDisplay = await this.read<IMountXDisplayDbc>(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray();
|
this._mountDisplay = await this.read<IMountXDisplayDbc>(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray();
|
||||||
this._spell = await this.read<ISpellDbc>(DbcFiles.spell, dbcFields.spell).toArray();
|
this._spell = await this.read<ISpellDbc>(DbcFiles.spell, dbcFields.spell).toArray();
|
||||||
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(
|
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(DbcFiles.spellItemEnchantment, dbcFields.spellItemEnchantment).toArray();
|
||||||
DbcFiles.spellItemEnchantment,
|
|
||||||
dbcFields.spellItemEnchantment,
|
|
||||||
).toArray();
|
|
||||||
this._spellIcon = await this.read<ISpellIcon>(DbcFiles.spellIcon, dbcFields.spellIcon).toArray();
|
this._spellIcon = await this.read<ISpellIcon>(DbcFiles.spellIcon, dbcFields.spellIcon).toArray();
|
||||||
this._talent = await this.read<ITalent>(DbcFiles.talent, dbcFields.talent).toArray();
|
this._talent = await this.read<ITalent>(DbcFiles.talent, dbcFields.talent).toArray();
|
||||||
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();
|
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import "source-map-support/register";
|
|
||||||
|
|
||||||
import { Armory } from "./Armory";
|
import { Armory } from "./Armory";
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
require("source-map-support").install();
|
||||||
|
|
||||||
const armory = new Armory();
|
const armory = new Armory();
|
||||||
await armory.start();
|
await armory.start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
src/index.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
declare namespace Express {
|
declare module Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,19 @@ const fsp = fs.promises;
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import * as pako from "pako";
|
import * as pako from "pako";
|
||||||
|
import fetch from "node-fetch";
|
||||||
import * as mkdirp from "mkdirp";
|
import * as mkdirp from "mkdirp";
|
||||||
import * as glob from "glob";
|
import * as glob from "glob-promise";
|
||||||
import "source-map-support/register";
|
import { Response } from "node-fetch";
|
||||||
import * as prettyMs from "pretty-ms";
|
import * as prettyMs from "pretty-ms";
|
||||||
import * as cliProgress from "cli-progress";
|
import * as cliProgress from "cli-progress";
|
||||||
import fetch, { Response } from "node-fetch";
|
|
||||||
import promisepool = require("@supercharge/promise-pool");
|
import promisepool = require("@supercharge/promise-pool");
|
||||||
|
|
||||||
import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/DbcReader";
|
import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/DbcReader";
|
||||||
|
|
||||||
const baseUrl = "https://wow.zamimg.com/modelviewer/live";
|
require("source-map-support").install();
|
||||||
|
|
||||||
const globAsync = (pattern: string, options?: glob.IOptions) => {
|
const baseUrl = "https://wow.zamimg.com/modelviewer/live";
|
||||||
return new Promise<string[]>((res, rej) => {
|
|
||||||
glob(pattern, options, (err, matches) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err);
|
|
||||||
} else {
|
|
||||||
res(matches);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
class Stopwatch {
|
class Stopwatch {
|
||||||
private startTime: number;
|
private startTime: number;
|
||||||
|
|
@ -64,12 +54,9 @@ class Progress {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createProgressBar(text: string, total: number): cliProgress.SingleBar {
|
private createProgressBar(text: string, total: number): cliProgress.SingleBar {
|
||||||
const progress = new cliProgress.SingleBar(
|
const progress = new cliProgress.SingleBar({
|
||||||
{
|
|
||||||
format: `${text} {bar} {percentage}% ({value} / {total})`,
|
format: `${text} {bar} {percentage}% ({value} / {total})`,
|
||||||
},
|
}, cliProgress.Presets.shades_classic);
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
progress.start(total, 0);
|
progress.start(total, 0);
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
@ -102,10 +89,11 @@ const modelsDownloadQueue = new Set<number>();
|
||||||
const texturesDownloadQueue = new Set<number>();
|
const texturesDownloadQueue = new Set<number>();
|
||||||
const bonesDownloadQueue = new Set<number>();
|
const bonesDownloadQueue = new Set<number>();
|
||||||
|
|
||||||
async function download(dir: string, file: string): Promise<string | unknown> {
|
async function download(dir: string, file: string): Promise<string | any> {
|
||||||
const dataDir = path.join(process.cwd(), "data");
|
const dataDir = path.join(process.cwd(), "data");
|
||||||
const fullPath = `${dir}/${file}`;
|
const fullPath = `${dir}/${file}`;
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await fetch(`${baseUrl}/${fullPath}`);
|
const res = await fetch(`${baseUrl}/${fullPath}`);
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new HttpRequestError(res);
|
throw new HttpRequestError(res);
|
||||||
|
|
@ -127,21 +115,28 @@ async function download(dir: string, file: string): Promise<string | unknown> {
|
||||||
|
|
||||||
return fileStream.path.toString();
|
return fileStream.path.toString();
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueTexturesAndModels(item): void {
|
function queueTexturesAndModels(item: any): void {
|
||||||
if (item.TextureFiles !== null) {
|
if (item.TextureFiles !== null) {
|
||||||
for (const file in Object.values(item.TextureFiles)) {
|
for (const key in item.TextureFiles) {
|
||||||
if (file["FileDataId"] !== 0) {
|
for (const file of item.TextureFiles[key]) {
|
||||||
texturesDownloadQueue.add(file["FileDataId"]);
|
if (file.FileDataId !== 0) {
|
||||||
|
texturesDownloadQueue.add(file.FileDataId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.ModelFiles !== null) {
|
if (item.ModelFiles !== null) {
|
||||||
for (const file of Object.values(item.ModelFiles)) {
|
for (const key in item.ModelFiles) {
|
||||||
if (file["FileDataId"] !== 0) {
|
for (const file of item.ModelFiles[key]) {
|
||||||
modelsDownloadQueue.add(file["FileDataId"]);
|
if (file.FileDataId !== 0) {
|
||||||
|
modelsDownloadQueue.add(file.FileDataId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +146,7 @@ function queueTexturesAndModels(item): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Textures !== null) {
|
if (item.Textures !== null) {
|
||||||
for (const key of Object.keys(item.Textures)) {
|
for (const key in item.Textures) {
|
||||||
if (item.Textures[key] !== 0) {
|
if (item.Textures[key] !== 0) {
|
||||||
texturesDownloadQueue.add(item.Textures[key]);
|
texturesDownloadQueue.add(item.Textures[key]);
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +154,7 @@ function queueTexturesAndModels(item): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Textures2 !== null) {
|
if (item.Textures2 !== null) {
|
||||||
for (const key of Object.keys(item.Textures2)) {
|
for (const key in item.Textures2) {
|
||||||
if (item.Textures2[key] !== 0) {
|
if (item.Textures2[key] !== 0) {
|
||||||
texturesDownloadQueue.add(item.Textures2[key]);
|
texturesDownloadQueue.add(item.Textures2[key]);
|
||||||
}
|
}
|
||||||
|
|
@ -168,27 +163,35 @@ function queueTexturesAndModels(item): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadRaces(): Promise<void> {
|
async function downloadRaces(): Promise<void> {
|
||||||
const races = ["human", "nightelf", "dwarf", "gnome", "draenei", "orc", "troll", "tauren", "bloodelf", "scourge"];
|
const races = [
|
||||||
|
"human",
|
||||||
|
"nightelf",
|
||||||
|
"dwarf",
|
||||||
|
"gnome",
|
||||||
|
"draenei",
|
||||||
|
"orc",
|
||||||
|
"troll",
|
||||||
|
"tauren",
|
||||||
|
"bloodelf",
|
||||||
|
"scourge",
|
||||||
|
];
|
||||||
const genders = ["male", "female"];
|
const genders = ["male", "female"];
|
||||||
const raceGenderCombo = races.map((race) => [race + genders[0], race + genders[1]]).flat();
|
const raceGenderCombo = races.map((race) => [race + genders[0], race + genders[1]]).flat();
|
||||||
|
|
||||||
const progress = new Progress("Downloading races data...", raceGenderCombo.length);
|
const progress = new Progress("Downloading races data...", raceGenderCombo.length);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(raceGenderCombo)
|
await promisepool.PromisePool
|
||||||
|
.for(raceGenderCombo)
|
||||||
.withConcurrency(4)
|
.withConcurrency(4)
|
||||||
.process(async (race) => {
|
.process(async (race) => {
|
||||||
const characterJson = await download("meta/character", `${race}.json`);
|
const characterJson = await download("meta/character", `${race}.json`);
|
||||||
modelsDownloadQueue.add(characterJson["Model"]);
|
modelsDownloadQueue.add(characterJson.Model);
|
||||||
|
|
||||||
const customizationJson = await download("meta/charactercustomization2", `${characterJson["Race"]}_${characterJson["Gender"]}.json`);
|
const customizationJson = await download("meta/charactercustomization2", `${characterJson.Race}_${characterJson.Gender}.json`);
|
||||||
for (const option of customizationJson["Options"]) {
|
for (const option of customizationJson.Options) {
|
||||||
for (const choice of option.Choices) {
|
for (const choice of option.Choices) {
|
||||||
for (const element of choice.Elements) {
|
for (const element of choice.Elements) {
|
||||||
if (
|
if (element.SkinnedModel !== null && typeof element.SkinnedModel.CollectionFileDataID === "number" && element.SkinnedModel.CollectionFileDataID !== 0) {
|
||||||
element.SkinnedModel !== null &&
|
|
||||||
typeof element.SkinnedModel.CollectionFileDataID === "number" &&
|
|
||||||
element.SkinnedModel.CollectionFileDataID !== 0
|
|
||||||
) {
|
|
||||||
modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID);
|
modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID);
|
||||||
}
|
}
|
||||||
if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === "number" && element.BoneSet.BoneFileDataID !== 0) {
|
if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === "number" && element.BoneSet.BoneFileDataID !== 0) {
|
||||||
|
|
@ -198,9 +201,11 @@ async function downloadRaces(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const textureFiles = Object.values(customizationJson["TextureFiles"]).flat();
|
const textureFiles = Object.keys(customizationJson.TextureFiles)
|
||||||
|
.map(key => customizationJson.TextureFiles[key])
|
||||||
|
.flat();
|
||||||
for (const file of textureFiles) {
|
for (const file of textureFiles) {
|
||||||
texturesDownloadQueue.add(file["FileDataId"]);
|
texturesDownloadQueue.add(file.FileDataId);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.increment();
|
progress.increment();
|
||||||
|
|
@ -210,14 +215,12 @@ async function downloadRaces(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadArmors(): Promise<void> {
|
async function downloadArmors(): Promise<void> {
|
||||||
const rows = await dbc
|
const rows = await dbc.item().filter((row) => row.classId === classIdArmor).toArray();
|
||||||
.item()
|
|
||||||
.filter((row) => row.classId === classIdArmor)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
const progress = new Progress("Downloading armor data...", rows.length);
|
const progress = new Progress("Downloading armor data...", rows.length);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(rows)
|
await promisepool.PromisePool
|
||||||
|
.for(rows)
|
||||||
.withConcurrency(50)
|
.withConcurrency(50)
|
||||||
.process(async (row) => {
|
.process(async (row) => {
|
||||||
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
|
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
|
||||||
|
|
@ -244,14 +247,12 @@ async function downloadArmors(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadWeapons(): Promise<void> {
|
async function downloadWeapons(): Promise<void> {
|
||||||
const rows = await dbc
|
const rows = await dbc.item().filter((row) => row.classId === classIdWeapon).toArray();
|
||||||
.item()
|
|
||||||
.filter((row) => row.classId === classIdWeapon)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
const progress = new Progress("Downloading weapon data...", rows.length);
|
const progress = new Progress("Downloading weapon data...", rows.length);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(rows)
|
await promisepool.PromisePool
|
||||||
|
.for(rows)
|
||||||
.withConcurrency(50)
|
.withConcurrency(50)
|
||||||
.process(async (row) => {
|
.process(async (row) => {
|
||||||
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
|
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
|
||||||
|
|
@ -261,7 +262,7 @@ async function downloadWeapons(): Promise<void> {
|
||||||
}
|
}
|
||||||
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
|
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
|
||||||
try {
|
try {
|
||||||
const itemJson = await download("meta/item", `${appearance.itemDisplayInfoId}.json`);
|
const itemJson = await download(`meta/item`, `${appearance.itemDisplayInfoId}.json`);
|
||||||
queueTexturesAndModels(itemJson);
|
queueTexturesAndModels(itemJson);
|
||||||
progress.increment();
|
progress.increment();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -306,13 +307,11 @@ async function readDbcData(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadMounts(): Promise<void> {
|
async function downloadMounts(): Promise<void> {
|
||||||
const mountSpells = await dbc
|
const mountSpells = await dbc.spell().filter(spell => spell.mechanic === spellMechanicMounted).toArray();
|
||||||
.spell()
|
|
||||||
.filter((spell) => spell.mechanic === spellMechanicMounted)
|
|
||||||
.toArray();
|
|
||||||
const progress = new Progress("Downloading mount data...", mountSpells.length);
|
const progress = new Progress("Downloading mount data...", mountSpells.length);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(mountSpells)
|
await promisepool.PromisePool
|
||||||
|
.for(mountSpells)
|
||||||
.withConcurrency(50)
|
.withConcurrency(50)
|
||||||
.process(async (spell) => {
|
.process(async (spell) => {
|
||||||
const mount = dbcMountBySourceSpellId[spell.id];
|
const mount = dbcMountBySourceSpellId[spell.id];
|
||||||
|
|
@ -334,7 +333,8 @@ async function downloadMounts(): Promise<void> {
|
||||||
async function downloadTextures(): Promise<void> {
|
async function downloadTextures(): Promise<void> {
|
||||||
const progress = new Progress("Downloading textures...", texturesDownloadQueue.size);
|
const progress = new Progress("Downloading textures...", texturesDownloadQueue.size);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(Array.from(texturesDownloadQueue))
|
await promisepool.PromisePool
|
||||||
|
.for(Array.from(texturesDownloadQueue))
|
||||||
.withConcurrency(25)
|
.withConcurrency(25)
|
||||||
.process(async (fileDataId) => {
|
.process(async (fileDataId) => {
|
||||||
await download("textures", `${fileDataId}.png`);
|
await download("textures", `${fileDataId}.png`);
|
||||||
|
|
@ -347,7 +347,8 @@ async function downloadTextures(): Promise<void> {
|
||||||
async function downloadModels(): Promise<void> {
|
async function downloadModels(): Promise<void> {
|
||||||
const progress = new Progress("Downloading models...", modelsDownloadQueue.size);
|
const progress = new Progress("Downloading models...", modelsDownloadQueue.size);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(Array.from(modelsDownloadQueue))
|
await promisepool.PromisePool
|
||||||
|
.for(Array.from(modelsDownloadQueue))
|
||||||
.withConcurrency(25)
|
.withConcurrency(25)
|
||||||
.process(async (fileDataId) => {
|
.process(async (fileDataId) => {
|
||||||
await download("mo3", `${fileDataId}.mo3`);
|
await download("mo3", `${fileDataId}.mo3`);
|
||||||
|
|
@ -360,7 +361,8 @@ async function downloadModels(): Promise<void> {
|
||||||
async function downloadBones(): Promise<void> {
|
async function downloadBones(): Promise<void> {
|
||||||
const progress = new Progress("Downloading bones...", bonesDownloadQueue.size);
|
const progress = new Progress("Downloading bones...", bonesDownloadQueue.size);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(Array.from(bonesDownloadQueue))
|
await promisepool.PromisePool
|
||||||
|
.for(Array.from(bonesDownloadQueue))
|
||||||
.withConcurrency(25)
|
.withConcurrency(25)
|
||||||
.process(async (fileDataId) => {
|
.process(async (fileDataId) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -379,10 +381,11 @@ async function downloadBones(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseModels(): Promise<void> {
|
async function parseModels(): Promise<void> {
|
||||||
const files = await globAsync("data/mo3/*.mo3");
|
const files = await glob("data/mo3/*.mo3");
|
||||||
const progress = new Progress("Reading model files for texture references...", files.length);
|
const progress = new Progress("Reading model files for texture references...", files.length);
|
||||||
|
|
||||||
await promisepool.PromisePool.for(files)
|
await promisepool.PromisePool
|
||||||
|
.for(files)
|
||||||
.withConcurrency(20)
|
.withConcurrency(20)
|
||||||
.process(async (file) => {
|
.process(async (file) => {
|
||||||
const buffer = await fsp.readFile(file);
|
const buffer = await fsp.readFile(file);
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/arena-team.css">
|
|
||||||
{{> datatables}}
|
|
||||||
{{> emblems}}
|
|
||||||
|
|
||||||
<a href="{{websiteRoot}}/arena">Back to Arena Ladder</a>
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
<div id="arena-header">
|
|
||||||
<div class="emblem-container">
|
|
||||||
<div class="arena-emblem">
|
|
||||||
<canvas width="74" height="128"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info">
|
|
||||||
<div class="team-name is-size-3">{{name}}</div>
|
|
||||||
<div class="is-size-5">
|
|
||||||
{{type}}v{{type}} Team, {{realm}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div class="is-size-5">Rating: {{rating}}</div>
|
|
||||||
<table id="stats">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Games</th>
|
|
||||||
<th>Wins</th>
|
|
||||||
<th>Losses</th>
|
|
||||||
<th>Win Rate</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Week</td>
|
|
||||||
<td>{{weekGames}}</td>
|
|
||||||
<td>{{weekWins}}</td>
|
|
||||||
<td>{{subtract weekGames weekWins}}</td>
|
|
||||||
<td>
|
|
||||||
{{#if (isnt weekGames 0)}}
|
|
||||||
{{toPrecision (multiply (divide weekWins weekGames) 100) 3}} %
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Season</td>
|
|
||||||
<td>{{seasonGames}}</td>
|
|
||||||
<td>{{seasonWins}}</td>
|
|
||||||
<td>{{subtract seasonGames seasonWins}}</td>
|
|
||||||
<td>
|
|
||||||
{{#if (isnt seasonGames 0)}}
|
|
||||||
{{toPrecision (multiply (divide seasonWins seasonGames) 100) 3}} %
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<div class="is-size-5">Members:</div>
|
|
||||||
<table id="members" class="stripe hover row-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Class</th>
|
|
||||||
<th>Race</th>
|
|
||||||
<th>Week Wins</th>
|
|
||||||
<th>Week Losses</th>
|
|
||||||
<th>Season Wins</th>
|
|
||||||
<th>Season Losses</th>
|
|
||||||
<th>Rating</th>
|
|
||||||
<th>Online</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each members}}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{{../websiteRoot}}/character/{{../realm}}/{{name}}/pvp">
|
|
||||||
{{name}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<img src="{{../aowow}}/static/images/wow/icons/medium/class_{{class}}.jpg">
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<img src="{{../aowow}}/static/images/wow/icons/medium/race_{{race}}_{{gender}}.jpg">
|
|
||||||
</td>
|
|
||||||
<td>{{weekWins}}</td>
|
|
||||||
<td>{{subtract weekGames weekWins}}</td>
|
|
||||||
<td>{{seasonWins}}</td>
|
|
||||||
<td>{{subtract seasonGames seasonWins}}</td>
|
|
||||||
<td>{{personalRating}}</td>
|
|
||||||
<td>{{#if online}}🟢{{else}}🔴{{/if}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script type="application/javascript">
|
|
||||||
$(window).on("load", () => {
|
|
||||||
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-emblem")[0]);
|
|
||||||
|
|
||||||
const dt = $("#members").DataTable({
|
|
||||||
searching: false,
|
|
||||||
paging: false,
|
|
||||||
responsive: {
|
|
||||||
details: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -69,7 +69,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
let achievementsData = {};
|
let achievementsData = {};
|
||||||
let $openCategory;
|
let $openCategory;
|
||||||
const $achievementTemplate = $("#achievement-template").detach().removeAttr("id");
|
const $achievementTemplate = $("#achievement-template").detach().removeAttr("id");
|
||||||
|
|
@ -118,7 +117,8 @@
|
||||||
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
|
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
|
||||||
|
|
||||||
if (isEarned) {
|
if (isEarned) {
|
||||||
const date = new Date(achievementsData.earned[achievement.id] * 1000);
|
const earned = achievementsData.earned[achievement.id];
|
||||||
|
const date = new Date(earned.date * 1000);
|
||||||
$achievement.find(".earned-date").text(date.toLocaleDateString());
|
$achievement.find(".earned-date").text(date.toLocaleDateString());
|
||||||
$achievement.addClass("earned");
|
$achievement.addClass("earned");
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,7 @@
|
||||||
.map(achievement => {
|
.map(achievement => {
|
||||||
return {
|
return {
|
||||||
achievement,
|
achievement,
|
||||||
date: achievementsData.earned[achievement],
|
...achievementsData.earned[achievement],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const mostRecent = achievements.sort((a, b) => b.date - a.date).slice(0, 4);
|
const mostRecent = achievements.sort((a, b) => b.date - a.date).slice(0, 4);
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
$totalRow.prependTo($("#progress-bars")); // Move the total bar to the top
|
$totalRow.prependTo($("#progress-bars")); // Move the total bar to the top
|
||||||
}
|
}
|
||||||
|
|
||||||
$.getJSON(`{{websiteRoot}}/character/{{{realm}}}/{{guid}}/achievements/data`, (data) => {
|
$.getJSON(`{{websiteRoot}}/character/{{realm}}/{{guid}}/achievements/data`, (data) => {
|
||||||
achievementsData = data;
|
achievementsData = data;
|
||||||
|
|
||||||
const $summaryLink = $("<a>")
|
const $summaryLink = $("<a>")
|
||||||
|
|
@ -258,5 +258,4 @@
|
||||||
makeProgressBars();
|
makeProgressBars();
|
||||||
openSummary();
|
openSummary();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@
|
||||||
<div class="column is-full-touch is-one-quarter-desktop">
|
<div class="column is-full-touch is-one-quarter-desktop">
|
||||||
<div class="box arena-team" data-team-id="{{this.id}}">
|
<div class="box arena-team" data-team-id="{{this.id}}">
|
||||||
<div class="info has-text-centered">
|
<div class="info has-text-centered">
|
||||||
<div class="is-size-4">
|
<div class="is-size-4">{{this.name}}</div>
|
||||||
<a href="{{../websiteRoot}}/arena/team/{{../realm}}/{{this.name}}">{{this.name}}</a>
|
|
||||||
</div>
|
|
||||||
<div class="is-size-5">{{this.type}}v{{this.type}} Team</div>
|
<div class="is-size-5">{{this.type}}v{{this.type}} Team</div>
|
||||||
<div>Rating: {{this.rating}}</div>
|
<div>Rating: {{this.rating}}</div>
|
||||||
<div>{{this.seasonWins}} W / {{subtract this.seasonGames this.seasonWins}} L</div>
|
<div>{{this.seasonWins}} W / {{subtract this.seasonGames this.seasonWins}} L</div>
|
||||||
|
|
@ -38,9 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
{{#each arenaTeams}}
|
{{#each arenaTeams}}
|
||||||
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-team[data-team-id={{this.id}}] .arena-emblem")[0]);
|
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-team[data-team-id={{this.id}}] .arena-emblem")[0]);
|
||||||
{{/each}}
|
{{/each}}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
const aowow_tooltips = { "renamelinks": true, };
|
const aowow_tooltips = { "renamelinks": true, };
|
||||||
</script>
|
</script>
|
||||||
|
<script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script>
|
||||||
|
|
||||||
{{> character-header }}
|
{{> character-header }}
|
||||||
|
|
||||||
|
|
@ -46,10 +47,62 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: none;">
|
||||||
|
<!-- Hackfix for aowow's tooltip opening when the page loads -->
|
||||||
|
<a href="{{aowow}}/?spell=10"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
const talentsData = {{{JSONstringify data}}};
|
const talentsData = {{{JSONstringify data}}};
|
||||||
|
|
||||||
|
for (let spec = 0; spec < 2; ++spec) {
|
||||||
|
const $spec = $(`#talents-spec-${spec}`);
|
||||||
|
|
||||||
|
let nbLearned = 0;
|
||||||
|
for (const tree of talentsData.trees) {
|
||||||
|
const learned = {};
|
||||||
|
for (const talent of talentsData.talents[spec]) {
|
||||||
|
for (const s of tree.spells) {
|
||||||
|
const ranks = [s.spellRank0, s.spellRank1, s.spellRank2, s.spellRank3, s.spellRank4];
|
||||||
|
const idx = ranks.indexOf(talent);
|
||||||
|
if (idx === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
learned[s.id] = {
|
||||||
|
spell: s,
|
||||||
|
rank: idx + 1,
|
||||||
|
};
|
||||||
|
++nbLearned;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $tree = $("#talent-tree-template")
|
||||||
|
.clone(false)
|
||||||
|
.removeAttr("id")
|
||||||
|
.appendTo($spec);
|
||||||
|
$tree.find(".header .image").attr("src", `{{aowow}}/static/images/wow/icons/medium/${tree.icon}.jpg`);
|
||||||
|
$tree.find(".header .name").text(tree.name);
|
||||||
|
|
||||||
|
createTree(tree, learned, $tree[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbLearned === 0 && spec !== 0) {
|
||||||
|
$spec.addClass("hidden");
|
||||||
|
$("#spec-links").hide();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $glyphs = $spec.find(".glyphs");
|
||||||
|
for (const spell of talentsData.glyphs[spec]) {
|
||||||
|
const $a = $("<a>");
|
||||||
|
$a.attr("href", `{{aowow}}?spell=${spell}`);
|
||||||
|
$glyphs.append($a);
|
||||||
|
$glyphs.append("<br>");
|
||||||
|
}
|
||||||
|
$glyphs.parent().appendTo($spec);
|
||||||
|
}
|
||||||
|
|
||||||
function createTree(data, learnedTalents, container) {
|
function createTree(data, learnedTalents, container) {
|
||||||
const $columns = $(container).find(".col");
|
const $columns = $(container).find(".col");
|
||||||
let points = 0;
|
let points = 0;
|
||||||
|
|
@ -166,55 +219,6 @@
|
||||||
$(container).append($arrow);
|
$(container).append($arrow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
|
||||||
for (let spec = 0; spec < 2; ++spec) {
|
|
||||||
const $spec = $(`#talents-spec-${spec}`);
|
|
||||||
|
|
||||||
let nbLearned = 0;
|
|
||||||
for (const tree of talentsData.trees) {
|
|
||||||
const learned = {};
|
|
||||||
for (const talent of talentsData.talents[spec]) {
|
|
||||||
for (const s of tree.spells) {
|
|
||||||
const ranks = [s.spellRank0, s.spellRank1, s.spellRank2, s.spellRank3, s.spellRank4];
|
|
||||||
const idx = ranks.indexOf(talent);
|
|
||||||
if (idx === -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
learned[s.id] = {
|
|
||||||
spell: s,
|
|
||||||
rank: idx + 1,
|
|
||||||
};
|
|
||||||
++nbLearned;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $tree = $("#talent-tree-template")
|
|
||||||
.clone(false)
|
|
||||||
.removeAttr("id")
|
|
||||||
.appendTo($spec);
|
|
||||||
$tree.find(".header .image").attr("src", `{{aowow}}/static/images/wow/icons/medium/${tree.icon}.jpg`);
|
|
||||||
$tree.find(".header .name").text(tree.name);
|
|
||||||
|
|
||||||
createTree(tree, learned, $tree[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nbLearned === 0 && spec !== 0) {
|
|
||||||
$spec.addClass("hidden");
|
|
||||||
$("#spec-links").hide();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $glyphs = $spec.find(".glyphs");
|
|
||||||
for (const spell of talentsData.glyphs[spec]) {
|
|
||||||
const $a = $("<a>");
|
|
||||||
$a.attr("href", `{{aowow}}?spell=${spell}`);
|
|
||||||
$glyphs.append($a);
|
|
||||||
$glyphs.append("<br>");
|
|
||||||
}
|
|
||||||
$glyphs.parent().appendTo($spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#link-spec-0").on("click", () => {
|
$("#link-spec-0").on("click", () => {
|
||||||
$("#talents-spec-0").removeClass("hidden");
|
$("#talents-spec-0").removeClass("hidden");
|
||||||
$("#talents-spec-1").addClass("hidden");
|
$("#talents-spec-1").addClass("hidden");
|
||||||
|
|
@ -228,11 +232,4 @@
|
||||||
if (location.hash === "#1") {
|
if (location.hash === "#1") {
|
||||||
$("#link-spec-1").click();
|
$("#link-spec-1").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("<script>")
|
|
||||||
.attr("src", "{{aowow}}/static/widgets/power.js")
|
|
||||||
.appendTo("body");
|
|
||||||
}
|
|
||||||
main();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/character.css">
|
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/character.css">
|
||||||
{{> icons }}
|
{{> icons }}
|
||||||
<script type="application/javascript">
|
|
||||||
const aowow_tooltips = {
|
|
||||||
renamelinks: true,
|
|
||||||
colorlinks: true,
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="application/javascript" src="{{websiteRoot}}/js/viewer.min.js"></script>
|
<script type="application/javascript" src="{{websiteRoot}}/js/viewer.min.js"></script>
|
||||||
|
<script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script>
|
||||||
|
|
||||||
{{> character-header }}
|
{{> character-header }}
|
||||||
|
|
||||||
|
|
@ -21,16 +16,8 @@
|
||||||
<span data-icon-size="large" class="icon-size is-hidden-mobile"></span>
|
<span data-icon-size="large" class="icon-size is-hidden-mobile"></span>
|
||||||
<span data-icon-size="medium" class="icon-size is-hidden-tablet"></span>
|
<span data-icon-size="medium" class="icon-size is-hidden-tablet"></span>
|
||||||
|
|
||||||
<div class="columns">
|
<br>
|
||||||
<div class="column">
|
<div id="char-options" class="columns">
|
||||||
<label class="checkbox">
|
|
||||||
<input id="cb-enable-3d-viewer" type="checkbox">
|
|
||||||
Enable 3D viewer
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="3d-viewer-options" class="columns">
|
|
||||||
<div class="column is-one-quarter">
|
<div class="column is-one-quarter">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input id="cb-hide-helmet" type="checkbox">
|
<input id="cb-hide-helmet" type="checkbox">
|
||||||
|
|
@ -48,26 +35,12 @@
|
||||||
<input id="cb-hide-tabard" type="checkbox">
|
<input id="cb-hide-tabard" type="checkbox">
|
||||||
Hide tabard
|
Hide tabard
|
||||||
</label>
|
</label>
|
||||||
<br>
|
|
||||||
|
|
||||||
<label id="lbl-hide-transmogs" class="checkbox">
|
|
||||||
<input id="cb-hide-transmogs" type="checkbox">
|
|
||||||
Hide transmogrifications
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="list-transmogs-container" class="column is-one-quarter">
|
|
||||||
<button id="btn-transmogs" class="button is-primary">Transmogrifications</button>
|
|
||||||
|
|
||||||
<div id="list-transmogs" class="box" style="display: none;">
|
|
||||||
<ul></ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="mounts-container" class="column">
|
<div id="mounts-container" class="column">
|
||||||
<button id="btn-mounts" class="button is-primary">Mounts</button>
|
<button id="btn-mounts" class="button is-primary">Mounts</button>
|
||||||
|
|
||||||
<div id="mount-template" class="iconmedium no-link-text">
|
<div id="mount-template" class="iconmedium">
|
||||||
<div class="icon"></div>
|
<div class="icon"></div>
|
||||||
<div class="border"></div>
|
<div class="border"></div>
|
||||||
<a target="_blank"></a>
|
<a target="_blank"></a>
|
||||||
|
|
@ -76,20 +49,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="item-slot-template" class="item-slot no-link-text">
|
<div id="item-slot-template" class="item-slot">
|
||||||
<div class="inventory-slot"></div>
|
<div class="inventory-slot"></div>
|
||||||
<div class="icon"></div>
|
<div class="icon"></div>
|
||||||
<div class="border"></div>
|
<div class="border"></div>
|
||||||
<a target="_blank"></a>
|
<a target="_blank"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: none;">
|
|
||||||
<!-- Hackfix for aowow's tooltip opening when the page loads -->
|
|
||||||
<a href="{{aowow}}/?spell=10"></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
const $model = $("#model");
|
const $model = $("#model");
|
||||||
const $itemSlotTemplate = $("#item-slot-template").detach().removeAttr("id");
|
const $itemSlotTemplate = $("#item-slot-template").detach().removeAttr("id");
|
||||||
const $mountTemplate = $("#mount-template").detach().removeAttr("id");
|
const $mountTemplate = $("#mount-template").detach().removeAttr("id");
|
||||||
|
|
@ -99,7 +66,7 @@
|
||||||
debug: () => { },
|
debug: () => { },
|
||||||
};
|
};
|
||||||
|
|
||||||
const charData = {{{ JSONstringify data }}};
|
const charData = {{{JSONstringify data}}};
|
||||||
const races = {
|
const races = {
|
||||||
1: "human",
|
1: "human",
|
||||||
2: "orc",
|
2: "orc",
|
||||||
|
|
@ -114,25 +81,6 @@
|
||||||
};
|
};
|
||||||
const genders = ["male", "female"];
|
const genders = ["male", "female"];
|
||||||
|
|
||||||
if (Cookies.get("disable-3d-viewer") === "1") {
|
|
||||||
$("#3d-viewer-options").hide();
|
|
||||||
} else {
|
|
||||||
$("#cb-enable-3d-viewer").prop("checked", true);
|
|
||||||
}
|
|
||||||
$("#cb-enable-3d-viewer").change(() => {
|
|
||||||
const enabled = $("#cb-enable-3d-viewer").prop("checked");
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
Cookies.remove("disable-3d-viewer");
|
|
||||||
createViewer();
|
|
||||||
$("#3d-viewer-options").slideDown("fast");
|
|
||||||
} else {
|
|
||||||
Cookies.set("disable-3d-viewer", "1");
|
|
||||||
viewer?.destroy();
|
|
||||||
$("#3d-viewer-options").slideUp("fast");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hideHelm = (charData.flags & 0x0400) === 0x0400;
|
const hideHelm = (charData.flags & 0x0400) === 0x0400;
|
||||||
$("#cb-hide-helmet").prop("checked", hideHelm);
|
$("#cb-hide-helmet").prop("checked", hideHelm);
|
||||||
const hideCloak = (charData.flags & 0x0800) === 0x0800;
|
const hideCloak = (charData.flags & 0x0800) === 0x0800;
|
||||||
|
|
@ -150,60 +98,19 @@
|
||||||
const hide = $("#cb-hide-tabard").prop("checked");
|
const hide = $("#cb-hide-tabard").prop("checked");
|
||||||
setSlotVisible(19, !hide);
|
setSlotVisible(19, !hide);
|
||||||
});
|
});
|
||||||
if (charData.characterModelTransmogs !== undefined && charData.equipment.some((item) => item.transmog !== null)) {
|
|
||||||
$("#lbl-hide-transmogs").show();
|
|
||||||
$("#list-transmogs-container").show();
|
|
||||||
|
|
||||||
for (const item of charData.equipment) {
|
|
||||||
if (item.transmog !== undefined && item.transmog !== 1) {
|
|
||||||
const $li = $("<li>").appendTo("#list-transmogs ul");
|
|
||||||
$("<a>")
|
|
||||||
.attr("href", `{{aowow}}/?item=${item.transmog}`)
|
|
||||||
.appendTo($li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$("#cb-hide-transmogs").change(() => {
|
|
||||||
const hide = $("#cb-hide-transmogs").prop("checked");
|
|
||||||
const items = hide ? charData.characterModelItems : charData.characterModelTransmogs;
|
|
||||||
for (const slot of (items.map((item) => item[0]))) {
|
|
||||||
clearSlots([slot]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideHelm = $("#cb-hide-helmet").prop("checked");
|
|
||||||
const hideCloak = $("#cb-hide-cloak").prop("checked");
|
|
||||||
const hideTabard = $("#cb-hide-tabard").prop("checked");
|
|
||||||
setItems(items.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak) || (item[0] === 19 && hideTabard)) && item[1] !== -1));
|
|
||||||
});
|
|
||||||
|
|
||||||
let viewer;
|
let viewer;
|
||||||
function createViewer() {
|
function createViewer() {
|
||||||
if (Cookies.get("disable-3d-viewer") === "1") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewer?.destroy();
|
viewer?.destroy();
|
||||||
viewer = new ZamModelViewer(characterModel);
|
viewer = new ZamModelViewer(characterModel);
|
||||||
const isLoadedInterval = setInterval(() => {
|
|
||||||
if (viewer.method("isLoaded")) {
|
|
||||||
clearInterval(isLoadedInterval);
|
|
||||||
setBackground("background.png");
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSlotVisible(slot, visible) {
|
function setSlotVisible(slot, visible) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
const transmogged = !$("#cb-hide-transmogs").prop("checked");
|
const item = charData.characterModelItems.find(([sl, appearance]) => sl === slot);
|
||||||
const items = transmogged ? charData.characterModelTransmogs : charData.characterModelItems;
|
|
||||||
const item = items.find(([sl, appearance]) => sl === slot);
|
|
||||||
if (item !== undefined) {
|
if (item !== undefined) {
|
||||||
if (item[1] === -1) {
|
|
||||||
clearSlots([slot]);
|
|
||||||
} else {
|
|
||||||
setItems([item]);
|
setItems([item]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
clearSlots([slot]);
|
clearSlots([slot]);
|
||||||
}
|
}
|
||||||
|
|
@ -227,11 +134,10 @@
|
||||||
characterModel.items.push(...items);
|
characterModel.items.push(...items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createItemSlot(item, invSlot, icon, quality, rel, container) {
|
function createItemSlot(item, invSlot, icon, rel, container) {
|
||||||
const $item = $itemSlotTemplate.clone();
|
const $item = $itemSlotTemplate.clone();
|
||||||
$item
|
$item
|
||||||
.data("icon", icon)
|
.data("icon", icon)
|
||||||
.data("quality", quality)
|
|
||||||
.find(".inventory-slot")
|
.find(".inventory-slot")
|
||||||
.css("background-image", `url("{{websiteRoot}}/img/inventory-slot/${invSlot}.png")`);
|
.css("background-image", `url("{{websiteRoot}}/img/inventory-slot/${invSlot}.png")`);
|
||||||
if (item !== undefined) {
|
if (item !== undefined) {
|
||||||
|
|
@ -262,7 +168,7 @@
|
||||||
rel.push("rand=" + item.randomPropertyId);
|
rel.push("rand=" + item.randomPropertyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createItemSlot(item?.itemEntry, slot, item?.icon?.toLowerCase(), item?.quality, rel.join("&"), side.element);
|
createItemSlot(item?.itemEntry, slot, item?.icon?.toLowerCase(), rel.join("&"), side.element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nbItemRows = Math.max($("#equipment-col-left").children().length, $("#equipment-col-right").children().length);
|
const nbItemRows = Math.max($("#equipment-col-left").children().length, $("#equipment-col-right").children().length);
|
||||||
|
|
@ -280,13 +186,9 @@
|
||||||
const $item = $(el);
|
const $item = $(el);
|
||||||
const item = $item.data("item");
|
const item = $item.data("item");
|
||||||
const icon = $item.data("icon");
|
const icon = $item.data("icon");
|
||||||
const quality = $item.data("quality");
|
|
||||||
if (item !== undefined) {
|
if (item !== undefined) {
|
||||||
$item.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/${iconSize}/${icon}.jpg")`);
|
$item.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/${iconSize}/${icon}.jpg")`);
|
||||||
}
|
}
|
||||||
if (quality !== undefined) {
|
|
||||||
$item.find(".border").css("background-image", `url("{{websiteRoot}}/img/icon-border/${iconSize}/q${quality}.png")`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!init) {
|
if (!init) {
|
||||||
|
|
@ -297,50 +199,10 @@
|
||||||
onResize(true);
|
onResize(true);
|
||||||
|
|
||||||
function setMount(mountId) {
|
function setMount(mountId) {
|
||||||
if (characterModel.mount.id === mountId) {
|
if (characterModel.mount.id !== mountId) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mountId !== 0) {
|
|
||||||
const mainHand = charData.equipment.find(e => e.slot === 15);
|
|
||||||
const offHand = charData.equipment.find(e => e.slot === 16);
|
|
||||||
const ranged = charData.equipment.find(e => e.slot === 17);
|
|
||||||
|
|
||||||
characterModel.charCustomization.sheathMain = mainHand !== undefined ? sheathTypes[mainHand.classId][mainHand.subclassId] : undefined;
|
|
||||||
if (offHand !== undefined) {
|
|
||||||
characterModel.charCustomization.sheathOff = sheathTypes[offHand.classId][offHand.subclassId];
|
|
||||||
} else if (ranged !== undefined) {
|
|
||||||
characterModel.charCustomization.sheathOff = sheathTypes[ranged.classId][ranged.subclassId];
|
|
||||||
} else {
|
|
||||||
characterModel.charCustomization.sheathOff = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (characterModel.charCustomization.sheathMain === undefined) {
|
|
||||||
characterModel.charCustomization.sheathMain = 0;
|
|
||||||
}
|
|
||||||
if (characterModel.charCustomization.sheathOff === undefined) {
|
|
||||||
characterModel.charCustomization.sheathOff = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
characterModel.charCustomization.sheathMain = -1;
|
|
||||||
characterModel.charCustomization.sheathOff = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
characterModel.mount.id = mountId;
|
characterModel.mount.id = mountId;
|
||||||
createViewer();
|
createViewer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBackground(file) {
|
|
||||||
if (!viewer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalContentPath = viewer.options.contentPath;
|
|
||||||
viewer.options.contentPath = "{{websiteRoot}}/data/";
|
|
||||||
viewer.options.background = file;
|
|
||||||
viewer.renderer.loadBackground();
|
|
||||||
viewer.options.contentPath = originalContentPath;
|
|
||||||
viewer.options.background = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charData.mounts.length === 0) {
|
if (charData.mounts.length === 0) {
|
||||||
|
|
@ -370,45 +232,11 @@
|
||||||
$("#btn-mounts").on("click", () => {
|
$("#btn-mounts").on("click", () => {
|
||||||
$("#mounts").slideToggle("fast");
|
$("#mounts").slideToggle("fast");
|
||||||
});
|
});
|
||||||
$("#btn-transmogs").on("click", () => {
|
|
||||||
$("#list-transmogs").slideToggle("fast");
|
|
||||||
});
|
|
||||||
|
|
||||||
const sheathTypes = {
|
|
||||||
2: {
|
|
||||||
// One handed weapons
|
|
||||||
0: 3,
|
|
||||||
4: 3,
|
|
||||||
7: 3,
|
|
||||||
14: 3,
|
|
||||||
15: 3,
|
|
||||||
17: 3,
|
|
||||||
20: 3,
|
|
||||||
|
|
||||||
// Two handed weapons
|
|
||||||
1: 1,
|
|
||||||
5: 1,
|
|
||||||
8: 1,
|
|
||||||
|
|
||||||
// Others
|
|
||||||
10: 2, // Staff
|
|
||||||
6: 1, // Polearm
|
|
||||||
2: 4, // Bow
|
|
||||||
3: 4, // Gun
|
|
||||||
18: 4, // Crossbow
|
|
||||||
13: 0, // Fist weapons
|
|
||||||
19: 3, // Wand
|
|
||||||
20: 1, // Fishing pole
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
0: 3, // Held in off-hand
|
|
||||||
6: 9, // Shield
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const characterModel = {
|
const characterModel = {
|
||||||
type: ZamModelViewer.WOW,
|
type: ZamModelViewer.WOW,
|
||||||
contentPath: "{{ contentPath }}",
|
contentPath: "{{websiteRoot}}/data/",
|
||||||
|
background: "background.png",
|
||||||
container: $model,
|
container: $model,
|
||||||
hd: true,
|
hd: true,
|
||||||
aspect: $model.outerWidth() / $model.outerHeight(),
|
aspect: $model.outerWidth() / $model.outerHeight(),
|
||||||
|
|
@ -420,8 +248,7 @@
|
||||||
sheathOff: -1,
|
sheathOff: -1,
|
||||||
},
|
},
|
||||||
cls: charData.class,
|
cls: charData.class,
|
||||||
items: (charData.characterModelTransmogs ?? charData.characterModelItems)
|
items: charData.characterModelItems.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak))),
|
||||||
.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak)) && item[1] !== -1),
|
|
||||||
models: {
|
models: {
|
||||||
type: ZamModelViewer.Wow.Types.CHARACTER,
|
type: ZamModelViewer.Wow.Types.CHARACTER,
|
||||||
id: `${races[charData.race]}${genders[charData.gender]}`,
|
id: `${races[charData.race]}${genders[charData.gender]}`,
|
||||||
|
|
@ -452,9 +279,4 @@
|
||||||
windowWidth = newWidth;
|
windowWidth = newWidth;
|
||||||
onResize();
|
onResize();
|
||||||
}, 500));
|
}, 500));
|
||||||
|
|
||||||
$("<script>")
|
|
||||||
.attr("src", "{{aowow}}/static/widgets/power.js")
|
|
||||||
.appendTo("body");
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#arena-header {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#arena-header .emblem-container {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#arena-header .info {
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#arena-header .info .team-name {
|
|
||||||
margin-top: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#stats td,
|
|
||||||
#stats th {
|
|
||||||
border: 1px solid #dddddd;
|
|
||||||
text-align: left;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataTables_info {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
@ -24,12 +24,3 @@
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-link-text a span {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#list-transmogs-container,
|
|
||||||
#lbl-hide-transmogs {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-size: 36px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconlarge {
|
.iconlarge {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
#select-realm {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
#results_processing {
|
#results_processing {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#select-arena-type-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-arena-type-container .arena-type-label {
|
|
||||||
align-self: center;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
@ -59,7 +59,6 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
createGuildEmblem({{{ JSONstringify emblem }}}, $(".emblem-container .guild-emblem")[0]);
|
createGuildEmblem({{{ JSONstringify emblem }}}, $(".emblem-container .guild-emblem")[0]);
|
||||||
|
|
||||||
dt = $("#members").DataTable({
|
dt = $("#members").DataTable({
|
||||||
|
|
@ -67,7 +66,7 @@
|
||||||
serverSide: true,
|
serverSide: true,
|
||||||
searchDelay: 800,
|
searchDelay: 800,
|
||||||
ajax: {
|
ajax: {
|
||||||
url: `{{websiteRoot}}/guild/{{{realm}}}/{{id}}/members`,
|
url: `{{websiteRoot}}/guild/{{realm}}/{{id}}/members`,
|
||||||
},
|
},
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{
|
{
|
||||||
|
|
@ -99,5 +98,4 @@
|
||||||
},
|
},
|
||||||
order: [[1, "asc"]],
|
order: [[1, "asc"]],
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 784 B |
|
Before Width: | Height: | Size: 901 B |
|
Before Width: | Height: | Size: 946 B |
|
Before Width: | Height: | Size: 858 B |
|
Before Width: | Height: | Size: 851 B |
|
Before Width: | Height: | Size: 883 B |
|
Before Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 884 B |
|
|
@ -3,11 +3,8 @@
|
||||||
|
|
||||||
<h1 class="title is-size-1">Armory</h1>
|
<h1 class="title is-size-1">Armory</h1>
|
||||||
|
|
||||||
<a href="{{websiteRoot}}/arena">Arena Ladder</a> 
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
{{#if (not (equalsLength realms 1))}}
|
{{#if (not (equalsLength realms 1))}}
|
||||||
|
|
||||||
<div id="select-realm-container">
|
<div id="select-realm-container">
|
||||||
<span class="realm-label">Realm:</span>
|
<span class="realm-label">Realm:</span>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
|
|
@ -37,7 +34,6 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
$(window).on("load", () => {
|
|
||||||
let dt;
|
let dt;
|
||||||
|
|
||||||
$("#select-realm").on("change", () => {
|
$("#select-realm").on("change", () => {
|
||||||
|
|
@ -83,5 +79,4 @@
|
||||||
details: true,
|
details: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
function waitForEmblemImages($emblem) {
|
function waitForEmblemImages($emblem) {
|
||||||
return Promise.all(
|
return Promise.all($emblem.find(".images img").map((idx, img) => {
|
||||||
$emblem.find(".images img").map((idx, img) => {
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
if (img.complete) {
|
if (img.complete) {
|
||||||
res();
|
res();
|
||||||
|
|
@ -9,8 +8,7 @@ function waitForEmblemImages($emblem) {
|
||||||
img.addEventListener("error", rej);
|
img.addEventListener("error", rej);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGuildEmblem(emblem, el) {
|
function createGuildEmblem(emblem, el) {
|
||||||
|
|
@ -18,8 +16,7 @@ function createGuildEmblem(emblem, el) {
|
||||||
const canvas = $emblem.find("canvas")[0];
|
const canvas = $emblem.find("canvas")[0];
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
const imgUrl = (type, section, value, value2) =>
|
const imgUrl = (type, section, value, value2) => `${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? ("_" + value2) : ""}_T${section}_U.PNG`;
|
||||||
`${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? "_" + value2 : ""}_T${section}_U.PNG`;
|
|
||||||
|
|
||||||
const $images = $("<div>").addClass("images").appendTo($emblem);
|
const $images = $("<div>").addClass("images").appendTo($emblem);
|
||||||
const bgUpper = $("<img>").attr("src", imgUrl("Background", "U", emblem.background)).appendTo($images)[0];
|
const bgUpper = $("<img>").attr("src", imgUrl("Background", "U", emblem.background)).appendTo($images)[0];
|
||||||
|
|
@ -67,10 +64,7 @@ function createArenaEmblem(teamSize, emblem, el) {
|
||||||
const canvas = $emblem.find("canvas")[0];
|
const canvas = $emblem.find("canvas")[0];
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
const imgUrl = (teamSize, type, value) =>
|
const imgUrl = (teamSize, type, value) => `${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? ("-" + teamSize) : ""}${type ? ("-" + type) : ""}${value ? ("-" + value) : ""}.PNG`;
|
||||||
`${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? "-" + teamSize : ""}${type ? "-" + type : ""}${
|
|
||||||
value ? "-" + value : ""
|
|
||||||
}.PNG`;
|
|
||||||
|
|
||||||
const $images = $("<div>").addClass("images").appendTo($emblem);
|
const $images = $("<div>").addClass("images").appendTo($emblem);
|
||||||
const banner = $("<img>").attr("src", imgUrl(teamSize)).appendTo($images)[0];
|
const banner = $("<img>").attr("src", imgUrl(teamSize)).appendTo($images)[0];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
window.parent.postMessage(
|
window.parent.postMessage({
|
||||||
{
|
|
||||||
url: window.location.pathname.replace(handlebarsData.websiteRoot, ""),
|
url: window.location.pathname.replace(handlebarsData.websiteRoot, ""),
|
||||||
},
|
}, "*");
|
||||||
"*",
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/ladder-arena.css">
|
|
||||||
{{> datatables}}
|
|
||||||
|
|
||||||
<h1 class="title is-size-1">Arena Ladder</h1>
|
|
||||||
|
|
||||||
<a href="{{websiteRoot}}/">Armory</a> 
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
{{#if (not (equalsLength realms 1))}}
|
|
||||||
<div id="select-realm-container">
|
|
||||||
<span class="realm-label">Realm:</span>
|
|
||||||
<div class="select">
|
|
||||||
<select id="select-realm">
|
|
||||||
{{#each realms}}
|
|
||||||
<option>{{this}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
 
|
|
||||||
<div class="select">
|
|
||||||
<select id="select-arena-type">
|
|
||||||
<option value="2">2v2</option>
|
|
||||||
<option value="3">3v3</option>
|
|
||||||
<option value="5">5v5</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div id="select-arena-type-container">
|
|
||||||
<span class="arena-type-label">Type:</span>
|
|
||||||
<div class="select">
|
|
||||||
<select id="select-arena-type">
|
|
||||||
<option value="2">2v2</option>
|
|
||||||
<option value="3">3v3</option>
|
|
||||||
<option value="5">5v5</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<table id="results" class="stripe hover row-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Rating</th>
|
|
||||||
<th>Wins</th>
|
|
||||||
<th>Losses</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script type="application/javascript">
|
|
||||||
$(window).on("load", () => {
|
|
||||||
let dt;
|
|
||||||
|
|
||||||
$("#select-realm, #select-arena-type").on("change", () => {
|
|
||||||
dt.draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
dt = $("#results").DataTable({
|
|
||||||
processing: true,
|
|
||||||
serverSide: true,
|
|
||||||
searchDelay: 800,
|
|
||||||
ajax: {
|
|
||||||
url: `{{websiteRoot}}/arena/ladder`,
|
|
||||||
data: d => {
|
|
||||||
d.realm = $("#select-realm").val();
|
|
||||||
d.teamsize = $("#select-arena-type").val();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columnDefs: [
|
|
||||||
{
|
|
||||||
targets: 0,
|
|
||||||
render: (name, type, row, meta) => `<a href="{{websiteRoot}}/arena/team/${meta.settings.json.realm}/${name}">${name}</a>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targets: 1,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targets: 2,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targets: 3,
|
|
||||||
searchable: false,
|
|
||||||
render: (gamesPlayed, type, row, meta) => gamesPlayed - row[2],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
responsive: {
|
|
||||||
details: true,
|
|
||||||
},
|
|
||||||
order: [[1, "desc"], [0, "asc"]],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
const handlebarsData = {{{JSONstringify locals}}};
|
const handlebarsData = {{{JSONstringify locals}}};
|
||||||
</script>
|
</script>
|
||||||
<script type="application/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script type="application/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script type="application/javascript" src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"></script>
|
|
||||||
|
|
||||||
{{#if iframeMode.enabled}}
|
{{#if iframeMode.enabled}}
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
|
|
@ -37,5 +36,3 @@
|
||||||
{{{ body }}}
|
{{{ body }}}
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<a href="{{websiteRoot}}/">Back to Armory</a>
|
<a href="{{websiteRoot}}/">Back to Armory</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a> 
|
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a> 
|
||||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a> 
|
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a> 
|
||||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a> 
|
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a>
|
||||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a> 
|
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/icons.css">
|
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/icons.css">
|
||||||
<style>
|
<style>
|
||||||
.iconlarge .border {
|
.iconlarge .border {
|
||||||
background-image: url("{{websiteRoot}}/img/icon-border/large/default.png");
|
background-image: url("{{aowow}}/static/images/Icon/large/border/default.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconlarge a {
|
.iconlarge a {
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconmedium .border {
|
.iconmedium .border {
|
||||||
background-image: url("{{websiteRoot}}/img/icon-border/medium/default.png");
|
background-image: url("{{aowow}}/static/images/Icon/medium/border/default.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconmedium a {
|
.iconmedium a {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"moduleResolution": "node"
|
"moduleResolution": "node"
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||