Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

70 changed files with 1422 additions and 5577 deletions

View file

@ -14,7 +14,7 @@ config.json
.env
# Log files
logs/
*.log
# Git folder
.git/

View file

@ -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_NAME="My Website"
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_LOAD_DBCS=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__REALM_ID=1
ACORE_ARMORY_REALMS__0__AUTH_DATABASE="acore_auth"

View file

@ -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
View file

@ -1 +0,0 @@
ko_fi: roboto

View file

@ -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

View file

@ -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
View file

@ -15,6 +15,3 @@ config.json
# Log files
*.log
# IDEs
.vscode/

View file

@ -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

View file

@ -1,9 +0,0 @@
{
"printWidth": 140,
"useTabs": true,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}

View file

@ -1,5 +1,4 @@
<!-- SHIELDS -->
[![Build](https://github.com/r-o-b-o-t-o/azerothcore-armory/actions/workflows/build.yml/badge.svg)](https://github.com/r-o-b-o-t-o/azerothcore-armory/actions/workflows/build.yml)
[![Stargazers][stars-shield]][stars-url]
[![Forks][forks-shield]][forks-url]
[![Issues][issues-shield]][issues-url]
@ -12,13 +11,11 @@
<p align="center">
A website to view your <a href="https://github.com/azerothcore/azerothcore-wotlk">AzerothCore</a> server's characters
<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://www.chromiecraft.com/armory">Demo</a>
<a href="https://github.com/r-o-b-o-t-o/azerothcore-armory/issues/new?template=question.yml">Ask a Question</a>
</p>
</div>
@ -111,40 +108,45 @@ I also noticed that such a tool was frequently requested in the AzerothCore Disc
```sh
git clone git@github.com:r-o-b-o-t-o/azerothcore-armory.git
```
2. Install the dependencies:
3. Install the dependencies:
```sh
cd azerothcore-armory/
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.
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
<details>
<summary>Main configuration</summary>
| 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 |
| `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 |
| `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"` |
| `iframeMode` | `ACORE_ARMORY_IFRAME_MODE__`... | Object | | Settings for the iframe mode |
| `iframeMode.enabled` | `ACORE_ARMORY_IFRAME_MODE__ENABLED` | Boolean | `false` | Set to `true` if you want to embed the armory in an 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 |
| `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[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].authDatabase` | `ACORE_ARMORY_REALMS__0__AUTH_DATABASE` | String | `"acore_auth"` | The name of the auth database |
| `realms[0].charactersDatabase` | `ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__`... | Database configuration object | | Configuration for the characters database. See "Database configuration" below |
| `worldDatabase` | `ACORE_ARMORY_WORLD_DATABASE__`... | Database configuration object | | Configuration for the world database. This is shared between all realms at the moment. See "Database configuration" below |
| `dbQueryTimeout` | `ACORE_ARMORY_DB_QUERY_TIMEOUT` | Number | `10000` | The maximum duration in milliseconds of a database query before it times out |
| config.json | .env | Type | Default value | Description |
|--------------------------------|----------------------------------------------------|-------------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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 |
| `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"` |
| `iframeMode` | `ACORE_ARMORY_IFRAME_MODE__`... | Object | | Settings for the iframe mode |
| `iframeMode.enabled` | `ACORE_ARMORY_IFRAME_MODE__ENABLED` | Boolean | `false` | Set to `true` if you want to embed the armory in an 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 |
| `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 |
| `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].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].authDatabase` | `ACORE_ARMORY_REALMS__0__AUTH_DATABASE` | String | `"acore_auth"` | The name of the auth database |
| `realms[0].charactersDatabase` | `ACORE_ARMORY_REALMS__0__CHARACTERS_DATABASE__`... | Database configuration object | | Configuration for the characters database. See "Database configuration" below |
| `worldDatabase` | `ACORE_ARMORY_WORLD_DATABASE__`... | Database configuration object | | Configuration for the world database. This is shared between all realms at the moment. See "Database configuration" below |
| `dbQueryTimeout` | `ACORE_ARMORY_DB_QUERY_TIMEOUT` | Number | `10000` | The maximum duration in milliseconds of a database query before it times out |
</details>
<details>
@ -178,7 +180,8 @@ Open a web browser and navigate to http://localhost:48733
Other useful npm scripts:
* `npm run clean`: cleans the build directory
* `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
@ -254,7 +257,7 @@ This repository is used in production over at [ChromieCraft](https://www.chromie
- [X] Character page
- [X] Online/offline status
- [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] Achievements
- [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
- [ ] PvE 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.
@ -307,7 +308,7 @@ Distributed under the MIT License. See the [`LICENSE`][license-url] file for mor
<!-- 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>
@ -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
[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
[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

View file

@ -1,5 +1,5 @@
{
"aowowUrl": "https://wowgaming.altervista.org/aowow",
"aowowUrl": "https://wotlkdb.com",
"websiteUrl": "https://mywebsite.com",
"websiteName": "My Website",
"websiteRoot": "",
@ -9,10 +9,6 @@
},
"loadDbcs": true,
"hideGameMasters": true,
"hideBotAccounts": true,
"botAccountPatterns": "RNDBOT%",
"transmogModule": false,
"useZamCdn": false,
"realms": [
{
"name": "AzerothCore",

View file

@ -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"

View file

@ -17,6 +17,3 @@ services:
networks:
local-private-net:
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}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "azerothcore-armory",
"version": "1.1.1",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "tsc -p tsconfig.json",
@ -8,8 +8,6 @@
"start": "node --expose-gc build/armory/main.js",
"watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"",
"nodemon": "nodemon -q -w build -w config.json build/armory/main.js",
"lint": "eslint src/",
"prettier": "prettier . -w",
"fetchdata": "node build/tools/fetchdata.js",
"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",
"license": "MIT",
"devDependencies": {
"@types/cli-progress": "^3.11.0",
"@types/cli-progress": "^3.9.2",
"@types/express": "^4.17.13",
"@types/glob": "^7.2.0",
"@types/mkdirp": "^1.0.2",
"@types/morgan": "^1.9.3",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/node": "^17.0.21",
"@types/node-fetch": "^2.6.1",
"@types/pako": "^1.0.3",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"concurrently": "^7.2.2",
"eslint": "^8.18.0",
"nodemon": "^2.0.18",
"prettier": "^2.7.1",
"concurrently": "^7.0.0",
"nodemon": "^2.0.15",
"rimraf": "^3.0.2",
"typescript": "^4.7.4"
"typescript": "^4.5.5"
},
"dependencies": {
"@supercharge/promise-pool": "^2.3.0",
"@supercharge/promise-pool": "^2.1.0",
"camelcase": "^6.3.0",
"cli-progress": "^3.11.2",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-handlebars": "^6.0.6",
"glob": "^8.0.3",
"cli-progress": "^3.10.0",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-handlebars": "^6.0.2",
"glob": "^7.2.0",
"glob-promise": "^4.2.2",
"handlebars-helpers": "^0.9.8",
"html-entities": "^2.3.3",
"html-entities": "^2.3.2",
"mkdirp": "^1.0.4",
"morgan": "^1.10.0",
"mysql2": "^2.3.3",
@ -52,6 +48,6 @@
"pretty-ms": "^7.0.1",
"source-map-support": "^0.5.21",
"uuid": "^8.3.2",
"winston": "^3.8.0"
"winston": "^3.6.0"
}
}

View file

@ -13,7 +13,6 @@ import { CharacterCustomization } from "./data/CharacterCustomization";
import { IndexController } from "./controllers/IndexController";
import { CharacterController } from "./controllers/CharacterController";
import { GuildController } from "./controllers/GuildController";
import { ArenaController } from "./controllers/ArenaController";
export class Armory {
public characterCustomization: CharacterCustomization;
@ -86,24 +85,22 @@ export class Armory {
websiteRoot: this.config.websiteRoot,
iframeMode: this.config.iframeMode,
};
for (const key of Object.keys(locals)) {
app.locals[key] = locals[key];
for (const key in locals) {
if (locals.hasOwnProperty(key)) {
app.locals[key] = locals[key];
}
}
app.locals.locals = locals;
app.engine(
".hbs",
handlebarsEngine({
extname: "hbs",
partialsDir: path.join(process.cwd(), "static", "partials"),
layoutsDir: path.join(process.cwd(), "static"),
defaultLayout: "layout.hbs",
helpers: {
// eslint-disable-next-line @typescript-eslint/no-var-requires
...require("handlebars-helpers")(),
},
}),
);
app.engine(".hbs", handlebarsEngine({
extname: "hbs",
partialsDir: path.join(process.cwd(), "static", "partials"),
layoutsDir: path.join(process.cwd(), "static"),
defaultLayout: "layout.hbs",
helpers: {
...require("handlebars-helpers")(),
},
}));
app.set("view engine", "handlebars");
app.set("views", path.join(process.cwd(), "static"));
@ -125,22 +122,20 @@ export class Armory {
}
return req.socket.remoteAddress;
});
app.use(
morgan(":method :url :status - ID :id - IP :ip - :response-time ms", {
stream: {
write: (msg) => this.logger.http(msg.trim()),
},
}),
);
app.use(morgan(":method :url :status - ID :id - IP :ip - :response-time ms", {
stream: {
write: (msg) => this.logger.http(msg.trim()),
},
}));
app.use("/js", express.static("static/js"));
app.use("/css", express.static("static/css"));
app.use("/img", express.static("static/img"));
app.use("/data/mo3", express.static("data/mo3"));
app.use("/data/meta", express.static("data/meta"));
app.use("/data/bone", express.static("data/bone"));
app.use("/data/textures", express.static("data/textures"));
app.use("/data/background.png", express.static("data/modelviewer-background.png"));
app.use("/js", express.static(`static/js`));
app.use("/css", express.static(`static/css`));
app.use("/img", express.static(`static/img`));
app.use("/data/mo3", express.static(`data/mo3`));
app.use("/data/meta", express.static(`data/meta`));
app.use("/data/bone", express.static(`data/bone`));
app.use("/data/textures", express.static(`data/textures`));
app.use("/data/background.png", express.static(`data/modelviewer-background.png`));
const indexController = new IndexController(this);
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/: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) => {
// Error handler
@ -208,14 +198,14 @@ export class Armory {
}
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> {
const db = this.getCharactersDb(realm);
if (!(realm in this.charsetCache)) {
const [rows] = await db.query({
const [rows, fields] = await db.query({
sql: `
SELECT CCSA.character_set_name AS charset FROM information_schema.\`TABLES\` T,
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
@ -242,7 +232,7 @@ export class Armory {
}, 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
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {

View file

@ -31,16 +31,12 @@ export class Config {
public iframeMode: IIframeModeConfig;
public loadDbcs: boolean;
public hideGameMasters: boolean;
public hideBotAccounts: boolean;
public botAccountPatterns: string;
public transmogModule: boolean;
public useZamCdn: boolean;
public realms: IRealmConfig[];
public worldDatabase: IDatabaseConfig;
public dbQueryTimeout: number;
private static envPrefix = "ACORE_ARMORY";
private static checkedMissingField = false;
private static envPrefix: string = "ACORE_ARMORY";
private static checkedMissingField: boolean = false;
public static async load(logger: winston.Logger): Promise<Config> {
try {
@ -74,13 +70,13 @@ export class 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 !== "") {
parentName += ".";
}
for (const field in model) {
if (!Object.hasOwnProperty.call(model, field)) {
if (!model.hasOwnProperty(field)) {
continue;
}
@ -89,9 +85,9 @@ export class Config {
} else if (typeof model[field] === "object") {
obj[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);
if (Object.hasOwnProperty.call(process.env, key)) {
if (process.env.hasOwnProperty(key)) {
obj[field] = Config.parseEnvValue(process.env[key], model[field]);
} else if (!Config.checkedMissingField) {
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 !== "") {
parentName += ".";
}
const arr = [];
let i = 0;
for (;;) {
while (true) {
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) {
break;
}
@ -119,10 +115,10 @@ export class Config {
} else if (typeof model === "object") {
const obj = {};
Config.loadObjFromEnv(logger, obj, model, parentName + i);
if (Object.keys(obj).length) {
if (Object.keys(obj).length > 0) {
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));
} else {
break;
@ -135,17 +131,13 @@ export class Config {
}
private static getEnvKey(key: string): string {
return (
Config.envPrefix +
"_" +
key
.replace(/\./g, "__")
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
.toUpperCase()
);
return Config.envPrefix + "_" + key
.replace(/\./g, "__")
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
.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 lower = value.toLowerCase();
if (type === "boolean") {
@ -157,7 +149,7 @@ export class Config {
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);
if (parentName !== "") {
parentName += ".";
@ -165,16 +157,20 @@ export class Config {
for (const field of missing) {
logger.warn(`Field ${parentName}${field} is missing from config.json!`);
}
for (const key of Object.keys(model)) {
if (typeof model[key] === "object" && Object.hasOwnProperty.call(obj, key)) {
for (const key in model) {
if (typeof model[key] === "object" && obj.hasOwnProperty(key)) {
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
}
}
}
private static hasMissingFields(obj: object, model: object): string[] {
const objProp = Object.keys(obj);
const missingProps = Object.keys(model).filter((key) => !objProp.includes(key));
return missingProps;
const missing = [];
for (const key in model) {
if (!obj.hasOwnProperty(key)) {
missing.push(key);
}
}
return missing;
}
}
};

View file

@ -1,17 +1,17 @@
import { Pool, RowDataPacket } from "mysql2/promise";
import { Pool } from "mysql2/promise";
import { Query } from "express-serve-static-core";
export interface IResult {
recordsTotal: number;
recordsFiltered: number;
draw: number;
data: unknown[][];
data: any[][];
}
export interface IColumnSettings {
name: string;
collation?: string;
formatter?: (data: string | number | null, row: unknown) => string;
formatter?: (data: string | number | null, row: any) => string;
table?: string;
database?: string;
}
@ -23,7 +23,6 @@ export interface IColumnJoin {
column2: string;
database2?: string;
kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT";
where?: string;
}
export class DataTablesSsp {
@ -39,55 +38,52 @@ export class DataTablesSsp {
private start: number;
private length: number;
private _order: {
column: number;
dir: string;
column: number,
dir: string,
}[];
private columns: {
data: number;
name: string;
searchable: boolean;
orderable: boolean;
data: number,
name: string,
searchable: boolean,
orderable: boolean,
search: {
value: string;
regex: boolean;
};
value: string,
regex: boolean,
}
}[];
private search: {
value: string;
regex: boolean;
value: string,
regex: boolean,
};
private wheres: string[] = [];
private filterBindings: (string | number)[] = [];
private customBindings: (string | number)[] = [];
private filterWhereSql = "1";
private customWhereSql = "1";
private limitSql = "";
private orderSql = "";
private joinSql = "";
private filterWhereSql: string = "1";
private customWhereSql: string = "1";
private limitSql: string = "";
private orderSql: string = "";
private joinSql: string = "";
public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
this.start = Math.max(0, parseInt(query.start as string, 10));
this.length = Math.min(100, Math.max(0, parseInt(query.length as string, 10)));
this.start = parseInt(query.start as string, 10);
this.length = parseInt(query.length as string, 10);
this.draw = parseInt(query.draw as string, 10);
this._order = (query.order as { column: string; dir: string }[]).map((order) => {
const dir = order.dir.toUpperCase() === "DESC" ? "DESC" : "ASC";
return { column: parseInt(order.column, 10), dir };
});
this.columns = (
query.columns as { data: string; name: string; searchable: string; orderable: string; search: { value: string; regex: string } }[]
).map((column) => {
return {
data: parseInt(column.data, 10),
name: column.name,
searchable: column.searchable === "true",
orderable: column.orderable === "true",
search: { value: column.search.value, regex: column.search.regex === "true" },
};
});
this._order = (query.order as { column: string, dir: string }[])
.map(order => { return { column: parseInt(order.column, 10), dir: order.dir, }; });
this.columns = (query.columns as { data: string, name: string, searchable: string, orderable: string, search: any }[])
.map(column => {
return {
data: parseInt(column.data, 10),
name: column.name,
searchable: column.searchable === "true",
orderable: column.orderable === "true",
search: { value: column.search.value, regex: column.search.regex === "true" },
};
});
this.search = {
value: (query.search as unknown)["value"] as string,
regex: (query.search as unknown)["regex"] === "true",
value: (query.search as any).value as string,
regex: (query.search as any).regex === "true",
};
this.db = db;
@ -97,7 +93,7 @@ export class DataTablesSsp {
}
private colSettingsToStr(colSettings: IColumnSettings) {
const db = colSettings.database ? "`" + colSettings.database + "`." : "";
const db = colSettings.database ? ("`" + colSettings.database + "`.") : "";
return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``;
}
@ -134,9 +130,8 @@ export class DataTablesSsp {
private join() {
for (const join of this.joins) {
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}\`${where}\n`;
const db2 = join.database2 ? "`" + join.database2 + "`." : "";
this.joinSql += `${join.kind} JOIN ${db2}\`${join.table2}\` ON ${db2}\`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`\n`;
}
return this;
@ -158,16 +153,19 @@ export class DataTablesSsp {
this.filterBindings.push(`%${this.search.value}%`);
}
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;
}
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 `
SELECT ${columns.join(", ")}
FROM ${this.table}
@ -200,32 +198,35 @@ export class DataTablesSsp {
`;
}
public async run(queryTimeout = 10_000): Promise<IResult> {
this.limit().order().join().filter();
public async run(queryTimeout: number = 10_000): Promise<IResult> {
this.limit()
.order()
.join()
.filter();
const bindings = [...this.filterBindings, ...this.customBindings];
let [rows] = await this.db.query({
let [rows, fields] = await this.db.query({
sql: this.buildTotalCountSql(),
values: this.customBindings,
timeout: queryTimeout,
});
const recordsTotal = rows[0].count;
[rows] = await this.db.query({
[rows, fields] = await this.db.query({
sql: this.buildFilteredCountSql(),
values: bindings,
timeout: queryTimeout,
});
const recordsFiltered = rows[0].count;
[rows] = await this.db.query({
[rows, fields] = await this.db.query({
sql: this.sql(),
rowsAsArray: true,
values: bindings,
timeout: queryTimeout,
});
rows = (rows as RowDataPacket[]).map((row) => {
rows = (rows as any[][]).map(row => {
for (let i = 0; i < this.columnSettings.length; ++i) {
const col = this.columnSettings[i];
if (col.formatter !== undefined) {
@ -239,7 +240,7 @@ export class DataTablesSsp {
recordsTotal,
recordsFiltered,
draw: this.draw,
data: rows as unknown[][],
data: rows,
};
}

View file

@ -41,16 +41,7 @@ export class Utils {
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
}
public static makeEmblemObject(
obj: {
background: number;
emblemStyle: number;
emblemColor: number;
borderStyle: number;
borderColor: number;
},
padWithZeroes = true,
): IEmblem {
public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem {
const padLength = padWithZeroes ? 2 : 0;
return {
icon: obj.emblemStyle.toString().padStart(padLength, "0"),
@ -60,37 +51,4 @@ export class Utils {
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 ")})`;
}
}

View file

@ -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,
};
}
}

View file

@ -1,10 +1,10 @@
import * as express from "express";
import { RowDataPacket } from "mysql2/promise";
import { Utils } from "../Utils";
import { Armory } from "../Armory";
import { IRealmConfig } from "../Config";
import { IEmblem, Utils } from "../Utils";
import { IAchievement as IAchievementDbc } from "../data/DbcReader";
import { IAchievement } from "../data/DbcReader";
interface ICharacterData {
guid: number;
@ -27,14 +27,8 @@ interface IEquipmentData {
slot: number;
itemEntry: number;
flags: number;
enchantments: string | number[];
enchantments: string;
randomPropertyId: number;
classId: number;
subclassId: number;
quality: number;
transmog?: number;
icon?: number;
gems?: number[];
}
interface ICustomizationOption {
@ -48,30 +42,6 @@ interface IMount {
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 SpellMechanicMounted = 21;
const RaceDisplayName = {
@ -108,7 +78,7 @@ export class CharacterController {
private itemSocketBonuses: { [key: number]: number };
private mountSpells: number[];
private mountBySpellId: { [key: number]: IMount };
private achievementById: { [key: number]: IAchievementDbc };
private achievementById: { [key: number]: IAchievement };
public constructor(armory: Armory) {
this.armory = armory;
@ -118,7 +88,7 @@ export class CharacterController {
this.itemInventoryTypes = {};
const itemsRetail = await this.armory.dbc.itemRetail().toArray();
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) {
this.itemInventoryTypes[item.id] = retailItem.inventoryType;
}
@ -137,7 +107,7 @@ export class CharacterController {
}
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;
}
@ -147,7 +117,7 @@ export class CharacterController {
}
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",
timeout: this.armory.config.dbQueryTimeout,
});
@ -155,17 +125,16 @@ export class CharacterController {
this.itemSocketBonuses[row.entry] = row.socketBonus;
}
const mountSpells = await this.armory.dbc
.spell()
.filter((m) => m.mechanic === SpellMechanicMounted)
const mountSpells = await this.armory.dbc.spell()
.filter(m => m.mechanic === SpellMechanicMounted)
.toArray();
this.mountSpells = mountSpells.map((spell) => spell.id);
this.mountSpells = mountSpells.map(spell => spell.id);
this.mountBySpellId = {};
for (const spell of mountSpells) {
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 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);
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) {
this.mountBySpellId[spell.id] = {
creatureDisplayId: display.creatureDisplayInfoId,
@ -200,27 +169,23 @@ export class CharacterController {
const equipmentData = await this.getEquipmentData(realmName, charData.guid);
const customization = this.getCustomizationOptions(charData);
const equipment = equipmentData.map((row) => {
row.icon = this.itemIcons[row.itemEntry];
row.gems = this.getGemsFromEnchantments(row.enchantments as string);
row.enchantments = this.filterEnchantments(row.itemEntry, row.enchantments as string);
const equipment = equipmentData.map(row => {
(row as any).icon = this.itemIcons[row.itemEntry];
(row as any).gems = this.getGemsFromEnchantments(row.enchantments);
(row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments);
return row;
});
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", {
title: `Armory - ${charData.name}`,
...this.makeSharedDataObject(realm, charData),
contentPath: this.armory.config.useZamCdn ? "https://wow.zamimg.com/modelviewer/live/" : this.armory.config.websiteRoot + "/data/",
data: {
race: charData.race,
gender: charData.gender,
class: charData.class,
flags: charData.playerFlags,
characterModelItems,
characterModelTransmogs: transmogs,
characterModelItems: await this.getModelViewerItems(equipmentData, charData.class),
customizationOptions: customization,
equipment,
mounts,
@ -297,7 +262,7 @@ export class CharacterController {
res.json({
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", {
title: `Armory - ${charData.name} - PvP`,
realm: realm.name,
...this.makeSharedDataObject(realm, charData),
faction: Utils.getFactionFromRaceId(charData.race),
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> {
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
// 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({
const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({
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\`
LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\`
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
${botJoin}
LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\`
WHERE
${where}
AND (\`account_access\`.\`id\` IS NULL OR ? = 0)
${botWhere}
AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0)
`,
values: [character, this.armory.config.hideGameMasters ? 1 : 0],
timeout: this.armory.config.dbQueryTimeout,
@ -380,50 +328,21 @@ export class CharacterController {
}
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
const transmogSelect = this.armory.config.transmogModule ? ", custom_transmogrification.FakeEntry AS transmog" : "";
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({
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT
character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
${transmogSelect}
SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
FROM character_inventory
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)
`,
values: [charGuid],
timeout: this.armory.config.dbQueryTimeout,
});
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;
return rows as RowDataPacket[] as IEquipmentData[];
}
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: `
SELECT spell
FROM character_spell
@ -433,52 +352,34 @@ export class CharacterController {
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) {
// 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(
(item) =>
const visibleEquipment = equipmentData.
filter(item =>
[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[][] = [];
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) {
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) {
continue;
}
appearance = { ...appearance };
let invType = this.itemInventoryTypes[equipment.itemEntry];
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]);
}
items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]);
}
return items;
@ -488,53 +389,52 @@ export class CharacterController {
return enchantments
.trim()
.split(" ")
.map((enchant) => parseInt(enchant))
.filter((enchant) => enchant !== 0);
.map(enchant => parseInt(enchant))
.filter(enchant => enchant !== 0);
}
private getGemsFromEnchantments(enchantments: string): number[] {
return this.parseEnchantmentsString(enchantments)
.filter((enchant) => enchant in this.enchantSrcItems && this.enchantSrcItems[enchant] in this.gemItems)
.map((enchant) => this.enchantSrcItems[enchant]);
.filter(enchant => enchant in this.enchantSrcItems && this.enchantSrcItems[enchant] in this.gemItems)
.map(enchant => this.enchantSrcItems[enchant]);
}
private filterEnchantments(item: number, enchantments: string): number[] {
const socketBonus = this.itemSocketBonuses[item];
return this.parseEnchantmentsString(enchantments).filter(
(enchant) => enchant in this.enchantSrcItems && !(this.enchantSrcItems[enchant] in this.gemItems) && enchant !== socketBonus,
);
return this.parseEnchantmentsString(enchantments)
.filter(enchant => enchant in this.enchantSrcItems && !(this.enchantSrcItems[enchant] in this.gemItems) && enchant !== socketBonus);
}
private getCustomizationOptions(charData: ICharacterData): ICustomizationOption[] {
const data = this.armory.characterCustomization.getCharacterCustomizationData(charData.race, charData.gender);
const options = [];
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) {
const choice = option.Choices.find((choice) => choice.OrderIndex === choiceIndex);
const choice = option.Choices.find(choice => choice.OrderIndex === choiceIndex);
if (choice !== undefined) {
options.push({ optionId: option.Id, choiceId: choice.Id });
}
}
};
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) {
const choice = option.Choices.find((ch) => ch.Name === choiceName);
const choice = option.Choices.find(ch => ch.Name === choiceName);
if (choice !== undefined) {
options.push({ optionId: option.Id, choiceId: choice.Id });
}
}
};
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) {
options.push({ optionId: option.Id, choiceId: choiceId });
}
};
const optionMapping = {
Face: charData.face,
"Face": charData.face,
"Skin Color": charData.skin,
"Hair Style": charData.hairStyle,
"Hair Color": charData.hairColor,
@ -547,54 +447,19 @@ export class CharacterController {
switch (charData.race) {
case 1: // Human
if (charData.gender === 0) {
setOptionByChoiceName(
"Mustache",
{ 0: "Horseshoe", 1: "Brush", 2: "Horseshoe", 3: "None", 4: "Brush", 5: "Brush", 6: "Horseshoe", 7: "Brush", 8: "None" }[
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("Mustache", { 0: "Horseshoe", 1: "Brush", 2: "Horseshoe", 3: "None", 4: "Brush", 5: "Brush", 6: "Horseshoe", 7: "Brush", 8: "None" }[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("Face Shape", "Narrow");
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],
);
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]);
} else {
setOptionByChoiceIndex("Piercings", charData.facialStyle);
setOptionByChoiceName("Eyebrows", "Natural");
setOptionByChoiceName("Face Shape", "Narrow");
setOptionByChoiceName("Makeup", "None");
setOptionByChoiceName("Necklace", "None");
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],
);
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]);
}
if (charData.class === 6) {
// Death Knight
@ -606,32 +471,14 @@ export class CharacterController {
setOptionByChoiceIndex("Tattoo Color", 0);
setOptionByChoiceIndex("Eyebrows", 0);
if (charData.gender === 0) {
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],
);
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]);
setOptionByChoiceIndex("Beard", charData.facialStyle);
setOptionByChoiceName("Earrings", "None");
setOptionByChoiceName("Nose Ring", "None");
setOptionByChoiceIndex("Eye Color", 0); // TODO
} else {
setOptionByChoiceIndex("Earrings", { 0: 0, 1: 1, 2: 2, 3: 3, 4: 0, 5: 4 }[charData.facialStyle]);
setOptionByChoiceName(
"Piercings",
{ 0: "None", 1: "None", 2: "None", 3: "None", 4: "Right Nostril", 5: "None" }[charData.facialStyle],
);
setOptionByChoiceName("Piercings", { 0: "None", 1: "None", 2: "None", 3: "None", 4: "Right Nostril", 5: "None" }[charData.facialStyle]);
setOptionByChoiceIndex("Eye Color", 0); // TODO
}
if (charData.class === 6) {
@ -661,10 +508,7 @@ export class CharacterController {
setOptionByChoiceName("Ears", "Thin");
setOptionByChoiceName("Scars", "None");
if (charData.gender === 0) {
setOptionByChoiceName(
"Sideburns",
{ 0: "None", 1: "Groomed", 2: "None", 3: "Short", 4: "Medium", 5: "Groomed" }[charData.facialStyle],
);
setOptionByChoiceName("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("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]);
@ -694,21 +538,10 @@ export class CharacterController {
setOptionByChoiceName("Horn Decoration", "None");
setOptionByChoiceName("Tail", charData.gender === 0 ? "Long" : "Short");
if (charData.gender === 0) {
setOptionByChoiceName(
"Facial Hair",
{ 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],
);
setOptionByChoiceName("Facial Hair", { 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 {
setOptionByChoiceName(
"Horns",
{ 0: "Sweeping", 1: "Curled", 2: "Curved", 3: "Thick", 4: "Wide", 5: "Grand", 6: "Short" }[charData.facialStyle],
);
setOptionByChoiceName("Horns", { 0: "Sweeping", 1: "Curled", 2: "Curved", 3: "Thick", 4: "Wide", 5: "Grand", 6: "Short" }[charData.facialStyle]);
}
if (charData.class === 6) {
// Death Knight
@ -724,28 +557,8 @@ export class CharacterController {
setOptionByChoiceName("War Paint", "None");
setOptionByChoiceName("War Paint Color", "None");
if (charData.gender === 0) {
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],
);
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("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]);
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("Nose Ring", "None");
setOptionByChoiceName("Tusks", "Natural");
@ -765,71 +578,13 @@ export class CharacterController {
case 5: // Undead
setOptionByChoiceName("Skin Type", "Bony");
if (charData.gender === 0) {
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],
);
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],
);
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]);
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 {
setOptionByChoiceName(
"Face Features",
{ 0: "None", 1: "None", 2: "Strapped", 3: "Rotting", 4: "None", 5: "None", 6: "None", 7: "Putrid" }[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],
);
setOptionByChoiceName("Face Features", { 0: "None", 1: "None", 2: "Strapped", 3: "Rotting", 4: "None", 5: "None", 6: "None", 7: "Putrid" }[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) {
// Death Knight
@ -848,18 +603,9 @@ export class CharacterController {
setOptionByChoiceName("Body Paint", "None");
setOptionByChoiceIndex("Paint Color", 0);
if (charData.gender === 0) {
setOptionByChoiceName(
"Hair",
{ 0: "Mane", 1: "Braids", 2: "Chops", 3: "Sideburns", 4: "Mane", 5: "Wrapped", 6: "Braids" }[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],
);
setOptionByChoiceName("Hair", { 0: "Mane", 1: "Braids", 2: "Chops", 3: "Sideburns", 4: "Mane", 5: "Wrapped", 6: "Braids" }[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
} else {
setOptionByChoiceIndex("Hair", charData.facialStyle);
@ -877,38 +623,8 @@ export class CharacterController {
setOptionByChoiceName("Body Paint Color", "None");
setOptionByChoiceName("Piercing", "None");
if (charData.gender === 0) {
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],
);
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],
);
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]);
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);
setOptionByChoiceName("Earrings", "None");
setOptionByChoiceIndex("Eye Color", 0); // TODO
@ -961,7 +677,7 @@ export class CharacterController {
}
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: `
SELECT spell, specMask
FROM character_talent
@ -985,18 +701,16 @@ export class CharacterController {
}
private async getTalentTrees(classId: number) {
const items = await this.armory.dbc
.talentTab()
.filter((tab) => tab.classMask === Math.pow(2, classId - 1))
.map(async (tab) => {
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === tab.spellIconId);
const spells = await this.armory.dbc
.talent()
.filter((row) => row.tabId === tab.id)
.map(async (row) => {
const spell = await this.armory.dbc.spell().find((spell) => spell.id === row.spellRank0);
const icon = await this.armory.dbc.spellIcon().find((icon) => icon.id === spell?.spellIconId);
return { ...row, icon: this.processSpellIconTexture(icon?.textureFilename ?? "") };
const items = await this.armory.dbc.talentTab()
.filter(tab => tab.classMask === Math.pow(2, classId - 1))
.map(async tab => {
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === tab.spellIconId);
const spells = await this.armory.dbc.talent()
.filter(row => row.tabId === tab.id)
.map(async row => {
const spell = await this.armory.dbc.spell().find(spell => spell.id === row.spellRank0);
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === spell?.spellIconId);
return { ...row, icon: this.processSpellIconTexture(icon?.textureFilename ?? ""), };
})
.toArray();
return {
@ -1010,11 +724,15 @@ export class CharacterController {
}
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[][]> {
const [rows] = await this.armory.getCharactersDb(realm).query({
private async getGlyphs(realm: string, character: number): Promise<any[][]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
FROM character_glyphs
@ -1024,11 +742,11 @@ export class CharacterController {
timeout: this.armory.config.dbQueryTimeout,
});
const glyphs: number[][] = [[], []];
const glyphs = [[], []];
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) {
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) {
continue;
}
@ -1039,15 +757,11 @@ export class CharacterController {
return glyphs;
}
private async getAchievements(
realm: string,
charData: ICharacterData,
): 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))
private async getAchievements(realm: string, charData: ICharacterData): Promise<{ achievements: any[]; earned: { [key: number]: any }; }> {
const promises = await this.armory.dbc.achievement()
.filter(ach => ach.faction === -1 || ach.faction === Utils.getFactionFromRaceId(charData.race))
.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 {
id: ach.id,
category: ach.category,
@ -1060,7 +774,7 @@ export class CharacterController {
.toArray();
const achievements = await Promise.all(promises);
const [rows] = await this.armory.getCharactersDb(realm).query({
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT achievement, date
FROM character_achievement
@ -1069,9 +783,11 @@ export class CharacterController {
values: [charData.guid],
timeout: this.armory.config.dbQueryTimeout,
});
const earned: { [key: number]: number } = {};
const earned = {};
for (const row of rows as RowDataPacket[]) {
earned[row.achievement] = row.date;
earned[row.achievement] = {
date: row.date,
};
}
return {
@ -1080,8 +796,8 @@ export class CharacterController {
};
}
private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number; today: number; yesterday: number }> {
const [rows] = await this.armory.getCharactersDb(realm).query({
private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number, today: number, yesterday: number }> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT totalKills, todayKills, yesterdayKills
FROM characters
@ -1099,8 +815,8 @@ export class CharacterController {
};
}
private async getArenaTeams(realm: string, charGuid: number): Promise<IArenaTeam[]> {
const [rows] = await this.armory.getCharactersDb(realm).query({
private async getArenaTeams(realm: string, charGuid: number): Promise<any[]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT
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,
});
return (rows as IArenaTeam[]).map((row) => {
return (rows as RowDataPacket[]).map(row => {
row.emblem = Utils.makeEmblemObject(row, false);
return row;
});

View file

@ -2,25 +2,16 @@ import * as express from "express";
import { encode } from "html-entities";
import { RowDataPacket } from "mysql2/promise";
import { Utils } from "../Utils";
import { Armory } from "../Armory";
import { IRealmConfig } from "../Config";
import { DataTablesSsp } from "../DataTablesSsp";
import { Utils, IEmblem, EFaction } from "../Utils";
interface IGuildRank {
id: number;
name: string;
}
interface IGuildData {
id: number;
name: string;
leader: string;
faction: EFaction;
emblem: IEmblem;
membersCount: number;
}
export class GuildController {
private armory: Armory;
@ -73,59 +64,37 @@ export class GuildController {
{ name: "name", table: "characters", collation: `${charSet}_general_ci` },
{ name: "rank" },
{ 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: "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`"];
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: "characters",
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");
ssp.joins.push({ table1: "characters", column1: "account", table2: "account_access", column2: "id", database2: realm.authDatabase, kind: "LEFT" });
ssp = ssp.where(`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`);
}
const botFilter = this.armory.config.hideBotAccounts
? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns)
: "";
if (botFilter !== "") {
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 result = await ssp
.where("`guildid` = ?", guildId)
.where("`deleteInfos_Account` IS NULL")
.run(this.armory.config.dbQueryTimeout);
const ranks = await this.getGuildRanks(realm, guildId);
const ranksObj: { [key: number]: string } = {};
(result as any).ranks = {};
for (const rank of ranks) {
ranksObj[rank.id] = encode(rank.name);
(result as any).ranks[rank.id] = encode(rank.name);
}
res.json({
...result,
ranks: ranksObj,
});
res.json(result);
}
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);
let [rows] = await db.query({
let [rows, fields] = await db.query({
sql: `
SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background
FROM guild WHERE name = ?
@ -138,7 +107,7 @@ export class GuildController {
}
const guild = rows[0];
[rows] = await db.query({
[rows, fields] = await db.query({
sql: `
SELECT name, race FROM characters
WHERE guid = ?
@ -148,7 +117,7 @@ export class GuildController {
});
const leader = rows[0];
[rows] = await db.query({
[rows, fields] = await db.query({
sql: `
SELECT COUNT(guid) AS \`count\` FROM guild_member
WHERE guildid = ?
@ -170,7 +139,7 @@ export class GuildController {
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
const db = this.armory.getCharactersDb(realm.name);
const [rows] = await db.query({
const [rows, fields] = await db.query({
sql: `
SELECT guildid
FROM guild WHERE name = ?
@ -187,7 +156,7 @@ export class GuildController {
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
const db = this.armory.getCharactersDb(realm.name);
const [rows] = await db.query({
const [rows, fields] = await db.query({
sql: `
SELECT guildid
FROM guild WHERE guildid
@ -201,7 +170,7 @@ export class GuildController {
private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> {
const db = this.armory.getCharactersDb(realm.name);
const [rows] = await db.query({
const [rows, fields] = await db.query({
sql: `
SELECT rid AS id, rname AS name
FROM guild_rank WHERE guildid = ?

View file

@ -14,13 +14,15 @@ export class IndexController {
public async index(req: express.Request, res: express.Response): Promise<void> {
res.render("index.hbs", {
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> {
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) {
return next(400);
}
@ -32,9 +34,9 @@ export class IndexController {
{ name: "name", collation: `${charSet}_general_ci` },
{ table: "guild", name: "name" },
{ 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: "online", formatter: (online) => online === 1 },
{ name: "online", formatter: online => online === 1 },
]);
ssp.joins = [
{ table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
@ -43,39 +45,15 @@ export class IndexController {
ssp.extraDataColumns = ["`characters`.`gender`"];
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: "characters",
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");
ssp.joins.push({ table1: "characters", column1: "account", table2: "account_access", column2: "id", database2: realm.authDatabase, kind: "LEFT" });
ssp = ssp.where(`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`);
}
const botFilter = this.armory.config.hideBotAccounts
? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns)
: "";
if (botFilter !== "") {
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);
(result as any).realm = realm.name;
const result = await ssp.where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout);
res.json({
...result,
realm: realm.name,
});
res.json(result);
}
}

View file

@ -3,7 +3,7 @@ const fsp = fs.promises;
import * as path from "path";
export class CharacterCustomization {
private data: { [key: number]: { [key: number]: unknown } };
private data: { [key: number]: { [key: number]: any } };
public async loadData(): Promise<void> {
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];
}
}

View file

@ -28,7 +28,6 @@ export interface IAchievementCategory {
export interface IItemDbc {
id: number;
classId: number;
subclassId: number;
displayInfoId: number;
inventoryType: number;
}
@ -148,13 +147,11 @@ class AsyncGenWrapper<T> implements IAsyncGeneratorWithArrayMethods<T> {
}
public static from<T>(array: T[]): AsyncGenWrapper<T> {
return new AsyncGenWrapper<T>(
(async function* () {
for (const x of array) {
yield x;
}
})(),
);
return new AsyncGenWrapper<T>(async function* () {
for (const x of array) {
yield x;
}
}());
}
public async *[Symbol.asyncIterator](): AsyncGenerator<T> {
@ -219,15 +216,11 @@ class DbcReader<T> {
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) {
const cols = arr.map((value) => {
if (value === "") {
return value;
}
return isNaN(+value) ? value : parseInt(value, 10);
});
const cols = arr.map(value => isNaN(value as any) ? value : parseInt(value, 10));
const row = {};
headerCols.forEach((header, headerIdx) => {
if (this.fields.length === 0 || this.fields.includes(header)) {
@ -248,8 +241,7 @@ class DbcReader<T> {
const str = chunk.toString();
// Iterate over each character, keep track of current column (of the returned array)
for (let c = 0; c < str.length; ++c) {
const ch = str[c];
const nch = str[c + 1]; // Current character, next character
let ch = str[c], nch = str[c + 1]; // Current character, next character
if (!(col in arr)) {
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 (ch == "," && !quote) {
if (ch == ',' && !quote) {
++col;
continue;
}
// 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
if (ch == "\r" && nch == "\n" && !quote) {
if (ch == '\r' && nch == '\n' && !quote) {
yield arr;
arr.length = 0; // Clear the row
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,
// 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;
arr.length = 0; // Clear the row
col = 0;
@ -324,7 +316,7 @@ const dbcFields = {
achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"],
achievementCategory: ["id", "parent", "nameLang0"],
glyphProperties: ["id", "spellId"],
item: ["id", "classId", "subclassId", "displayInfoId", "inventoryType"],
item: ["id", "classId", "displayInfoId", "inventoryType"],
itemRetail: ["id", "inventoryType"],
itemAppearance: ["id", "itemDisplayInfoId"],
itemModifiedAppearance: ["id", "itemId", "itemAppearanceId"],
@ -334,19 +326,7 @@ const dbcFields = {
spell: ["id", "mechanic", "spellIconId"],
spellItemEnchantment: ["id", "srcItemId"],
spellIcon: ["id", "textureFilename"],
talent: [
"id",
"tabId",
"tierId",
"columnIndex",
"spellRank0",
"spellRank1",
"spellRank2",
"spellRank3",
"spellRank4",
"prereqTalent0",
"prereqRank0",
],
talent: ["id", "tabId", "tierId", "columnIndex", "spellRank0", "spellRank1", "spellRank2", "spellRank3", "spellRank4", "prereqTalent0", "prereqRank0"],
talentTab: ["id", "nameLang0", "spellIconId", "classMask"],
};
@ -369,26 +349,17 @@ export class DbcManager {
public async loadAllFiles(): Promise<void> {
this._achievement = await this.read<IAchievement>(DbcFiles.achievement, dbcFields.achievement).toArray();
this._achievementCategory = await this.read<IAchievementCategory>(
DbcFiles.achievementCategory,
dbcFields.achievementCategory,
).toArray();
this._achievementCategory = await this.read<IAchievementCategory>(DbcFiles.achievementCategory, dbcFields.achievementCategory).toArray();
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
this._itemAppearance = await this.read<IItemAppearanceDbc>(DbcFiles.itemAppearance, dbcFields.itemAppearance).toArray();
this._itemModifiedAppearance = await this.read<IItemModifiedAppearanceDbc>(
DbcFiles.itemModifiedAppearance,
dbcFields.itemModifiedAppearance,
).toArray();
this._itemModifiedAppearance = await this.read<IItemModifiedAppearanceDbc>(DbcFiles.itemModifiedAppearance, dbcFields.itemModifiedAppearance).toArray();
this._itemDisplayInfo = await this.read<IItemDisplayInfoDbc>(DbcFiles.itemDisplayInfo, dbcFields.itemDisplayInfo).toArray();
this._mount = await this.read<IMountDbc>(DbcFiles.mount, dbcFields.mount).toArray();
this._mountDisplay = await this.read<IMountXDisplayDbc>(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray();
this._spell = await this.read<ISpellDbc>(DbcFiles.spell, dbcFields.spell).toArray();
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(
DbcFiles.spellItemEnchantment,
dbcFields.spellItemEnchantment,
).toArray();
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(DbcFiles.spellItemEnchantment, dbcFields.spellItemEnchantment).toArray();
this._spellIcon = await this.read<ISpellIcon>(DbcFiles.spellIcon, dbcFields.spellIcon).toArray();
this._talent = await this.read<ITalent>(DbcFiles.talent, dbcFields.talent).toArray();
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();

View file

@ -1,9 +1,9 @@
import "dotenv/config";
import "source-map-support/register";
import { Armory } from "./Armory";
async function main(): Promise<void> {
require("source-map-support").install();
const armory = new Armory();
await armory.start();
}

2
src/index.d.ts vendored
View file

@ -1,4 +1,4 @@
declare namespace Express {
declare module Express {
export interface Request {
id: string;
}

View file

@ -3,29 +3,19 @@ const fsp = fs.promises;
import * as path from "path";
import * as pako from "pako";
import fetch from "node-fetch";
import * as mkdirp from "mkdirp";
import * as glob from "glob";
import "source-map-support/register";
import * as glob from "glob-promise";
import { Response } from "node-fetch";
import * as prettyMs from "pretty-ms";
import * as cliProgress from "cli-progress";
import fetch, { Response } from "node-fetch";
import promisepool = require("@supercharge/promise-pool");
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) => {
return new Promise<string[]>((res, rej) => {
glob(pattern, options, (err, matches) => {
if (err) {
rej(err);
} else {
res(matches);
}
});
});
};
const baseUrl = "https://wow.zamimg.com/modelviewer/live";
class Stopwatch {
private startTime: number;
@ -64,12 +54,9 @@ class Progress {
}
private createProgressBar(text: string, total: number): cliProgress.SingleBar {
const progress = new cliProgress.SingleBar(
{
format: `${text} {bar} {percentage}% ({value} / {total})`,
},
cliProgress.Presets.shades_classic,
);
const progress = new cliProgress.SingleBar({
format: `${text} {bar} {percentage}% ({value} / {total})`,
}, cliProgress.Presets.shades_classic);
progress.start(total, 0);
return progress;
}
@ -102,46 +89,54 @@ const modelsDownloadQueue = new Set<number>();
const texturesDownloadQueue = 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 fullPath = `${dir}/${file}`;
const res = await fetch(`${baseUrl}/${fullPath}`);
if (res.status !== 200) {
throw new HttpRequestError(res);
}
try {
const res = await fetch(`${baseUrl}/${fullPath}`);
if (res.status !== 200) {
throw new HttpRequestError(res);
}
await mkdirp(path.join(dataDir, dir));
await mkdirp(path.join(dataDir, dir));
if (res.headers.get("Content-Type") === "application/json") {
const json = await res.json();
fsp.writeFile(path.join(dataDir, fullPath), JSON.stringify(json));
return json;
} else {
const fileStream = fs.createWriteStream(path.join(dataDir, fullPath));
await new Promise((resolve, rej) => {
res.body.pipe(fileStream);
res.body.on("error", rej);
fileStream.on("finish", resolve);
});
if (res.headers.get("Content-Type") === "application/json") {
const json = await res.json();
fsp.writeFile(path.join(dataDir, fullPath), JSON.stringify(json));
return json;
} else {
const fileStream = fs.createWriteStream(path.join(dataDir, fullPath));
await new Promise((resolve, rej) => {
res.body.pipe(fileStream);
res.body.on("error", rej);
fileStream.on("finish", resolve);
});
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) {
for (const file in Object.values(item.TextureFiles)) {
if (file["FileDataId"] !== 0) {
texturesDownloadQueue.add(file["FileDataId"]);
for (const key in item.TextureFiles) {
for (const file of item.TextureFiles[key]) {
if (file.FileDataId !== 0) {
texturesDownloadQueue.add(file.FileDataId);
}
}
}
}
if (item.ModelFiles !== null) {
for (const file of Object.values(item.ModelFiles)) {
if (file["FileDataId"] !== 0) {
modelsDownloadQueue.add(file["FileDataId"]);
for (const key in item.ModelFiles) {
for (const file of item.ModelFiles[key]) {
if (file.FileDataId !== 0) {
modelsDownloadQueue.add(file.FileDataId);
}
}
}
}
@ -151,7 +146,7 @@ function queueTexturesAndModels(item): void {
}
if (item.Textures !== null) {
for (const key of Object.keys(item.Textures)) {
for (const key in item.Textures) {
if (item.Textures[key] !== 0) {
texturesDownloadQueue.add(item.Textures[key]);
}
@ -159,7 +154,7 @@ function queueTexturesAndModels(item): void {
}
if (item.Textures2 !== null) {
for (const key of Object.keys(item.Textures2)) {
for (const key in item.Textures2) {
if (item.Textures2[key] !== 0) {
texturesDownloadQueue.add(item.Textures2[key]);
}
@ -168,27 +163,35 @@ function queueTexturesAndModels(item): 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 raceGenderCombo = races.map((race) => [race + genders[0], race + genders[1]]).flat();
const progress = new Progress("Downloading races data...", raceGenderCombo.length);
await promisepool.PromisePool.for(raceGenderCombo)
await promisepool.PromisePool
.for(raceGenderCombo)
.withConcurrency(4)
.process(async (race) => {
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`);
for (const option of customizationJson["Options"]) {
const customizationJson = await download("meta/charactercustomization2", `${characterJson.Race}_${characterJson.Gender}.json`);
for (const option of customizationJson.Options) {
for (const choice of option.Choices) {
for (const element of choice.Elements) {
if (
element.SkinnedModel !== null &&
typeof element.SkinnedModel.CollectionFileDataID === "number" &&
element.SkinnedModel.CollectionFileDataID !== 0
) {
if (element.SkinnedModel !== null && typeof element.SkinnedModel.CollectionFileDataID === "number" && element.SkinnedModel.CollectionFileDataID !== 0) {
modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID);
}
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) {
texturesDownloadQueue.add(file["FileDataId"]);
texturesDownloadQueue.add(file.FileDataId);
}
progress.increment();
@ -210,14 +215,12 @@ async function downloadRaces(): Promise<void> {
}
async function downloadArmors(): Promise<void> {
const rows = await dbc
.item()
.filter((row) => row.classId === classIdArmor)
.toArray();
const rows = await dbc.item().filter((row) => row.classId === classIdArmor).toArray();
const progress = new Progress("Downloading armor data...", rows.length);
await promisepool.PromisePool.for(rows)
await promisepool.PromisePool
.for(rows)
.withConcurrency(50)
.process(async (row) => {
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
@ -244,14 +247,12 @@ async function downloadArmors(): Promise<void> {
}
async function downloadWeapons(): Promise<void> {
const rows = await dbc
.item()
.filter((row) => row.classId === classIdWeapon)
.toArray();
const rows = await dbc.item().filter((row) => row.classId === classIdWeapon).toArray();
const progress = new Progress("Downloading weapon data...", rows.length);
await promisepool.PromisePool.for(rows)
await promisepool.PromisePool
.for(rows)
.withConcurrency(50)
.process(async (row) => {
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
@ -261,7 +262,7 @@ async function downloadWeapons(): Promise<void> {
}
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
try {
const itemJson = await download("meta/item", `${appearance.itemDisplayInfoId}.json`);
const itemJson = await download(`meta/item`, `${appearance.itemDisplayInfoId}.json`);
queueTexturesAndModels(itemJson);
progress.increment();
} catch (err) {
@ -306,13 +307,11 @@ async function readDbcData(): Promise<void> {
}
async function downloadMounts(): Promise<void> {
const mountSpells = await dbc
.spell()
.filter((spell) => spell.mechanic === spellMechanicMounted)
.toArray();
const mountSpells = await dbc.spell().filter(spell => spell.mechanic === spellMechanicMounted).toArray();
const progress = new Progress("Downloading mount data...", mountSpells.length);
await promisepool.PromisePool.for(mountSpells)
await promisepool.PromisePool
.for(mountSpells)
.withConcurrency(50)
.process(async (spell) => {
const mount = dbcMountBySourceSpellId[spell.id];
@ -334,7 +333,8 @@ async function downloadMounts(): Promise<void> {
async function downloadTextures(): Promise<void> {
const progress = new Progress("Downloading textures...", texturesDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(texturesDownloadQueue))
await promisepool.PromisePool
.for(Array.from(texturesDownloadQueue))
.withConcurrency(25)
.process(async (fileDataId) => {
await download("textures", `${fileDataId}.png`);
@ -347,7 +347,8 @@ async function downloadTextures(): Promise<void> {
async function downloadModels(): Promise<void> {
const progress = new Progress("Downloading models...", modelsDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(modelsDownloadQueue))
await promisepool.PromisePool
.for(Array.from(modelsDownloadQueue))
.withConcurrency(25)
.process(async (fileDataId) => {
await download("mo3", `${fileDataId}.mo3`);
@ -360,7 +361,8 @@ async function downloadModels(): Promise<void> {
async function downloadBones(): Promise<void> {
const progress = new Progress("Downloading bones...", bonesDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(bonesDownloadQueue))
await promisepool.PromisePool
.for(Array.from(bonesDownloadQueue))
.withConcurrency(25)
.process(async (fileDataId) => {
try {
@ -379,10 +381,11 @@ async function downloadBones(): 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);
await promisepool.PromisePool.for(files)
await promisepool.PromisePool
.for(files)
.withConcurrency(20)
.process(async (file) => {
const buffer = await fsp.readFile(file);

View 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>

View file

@ -69,194 +69,193 @@
</div>
<script type="application/javascript">
$(window).on("load", () => {
let achievementsData = {};
let $openCategory;
const $achievementTemplate = $("#achievement-template").detach().removeAttr("id");
const $progressTemplate = $("#progress-template").detach().removeAttr("id");
let achievementsData = {};
let $openCategory;
const $achievementTemplate = $("#achievement-template").detach().removeAttr("id");
const $progressTemplate = $("#progress-template").detach().removeAttr("id");
function makeCategoryLink(cat) {
const $a = $("<a>")
.attr("href", "#")
.text(cat.nameLang0)
.on("click", () => {
if ($openCategory !== undefined && $openCategory !== $a && !$openCategory.data("children")[0].contains($a[0])) {
closeCategory();
}
function makeCategoryLink(cat) {
const $a = $("<a>")
.attr("href", "#")
.text(cat.nameLang0)
.on("click", () => {
if ($openCategory !== undefined && $openCategory !== $a && !$openCategory.data("children")[0].contains($a[0])) {
closeCategory();
}
const $children = $a.data("children");
if ($children !== undefined) {
$openCategory = $a;
if (!$children.is(":visible")) {
// Open the clicked category
$children.slideDown();
}
}
openCategory(cat);
return false;
});
return $a;
}
function makeAchievement(achievement) {
const isEarned = achievement.id in achievementsData.earned;
if (!isEarned && achievement.category === 81) {
// Show unlocked feats of strength only
return;
}
const $achievement = $achievementTemplate
.clone(false)
.appendTo($("#achievements"));
$achievement.find(".name").text(achievement.title);
$achievement.find(".description").text(achievement.description);
$achievement.find(".points .value").text(achievement.points);
if (achievement.points === 0) {
$achievement.find(".points").addClass("no-points");
}
$achievement.find(".iconlarge .icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/large/${achievement.icon}.jpg")`);
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
if (isEarned) {
const date = new Date(achievementsData.earned[achievement.id] * 1000);
$achievement.find(".earned-date").text(date.toLocaleDateString());
$achievement.addClass("earned");
}
return $achievement;
}
function openCategory(cat) {
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id);
$("#achievements").empty();
$(".summary").hide();
for (const achievement of categoryAchievements) {
makeAchievement(achievement);
}
}
function closeCategory() {
if ($openCategory === undefined || $openCategory.data("children") === undefined) {
return;
}
$openCategory
.data("children")
.slideUp();
}
function openSummary() {
closeCategory();
$("#achievements").empty();
const achievements = Object.keys(achievementsData.earned)
.filter(key => achievementsData.earned.hasOwnProperty(key))
.map(key => parseInt(key, 10))
.map(achievement => {
return {
achievement,
date: achievementsData.earned[achievement],
};
});
const mostRecent = achievements.sort((a, b) => b.date - a.date).slice(0, 4);
for (const row of mostRecent) {
const achievement = achievementsData.achievements.find(ach => ach.id === row.achievement);
makeAchievement(achievement);
}
$(".summary").show();
}
function makeProgressBarRow(barsData) {
const $row = $("<div>").addClass("columns");
$("#progress-bars").append($row);
for (const bar of barsData) {
let $bar = $progressTemplate.clone(false);
$bar.find(".title").text(bar.name);
$bar.find(".value").text(`${bar.earned} / ${bar.total}`);
$bar.find(".progress").attr("value", bar.earned).attr("max", bar.total);
$row.append($bar);
}
return $row;
}
function makeProgressBars() {
let totalAchievements = 0;
let totalAchievementsEarned = 0;
const findProgress = (catId) => {
const cat = achievementsData.categories.find(cat => cat.id === catId);
const subCategories = achievementsData.categories.filter(cat => cat.parent === catId);
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id || subCategories.some(cat => cat.id === ach.category));
let earned = 0;
for (const ach of categoryAchievements) {
if (ach.id in achievementsData.earned) {
++earned;
const $children = $a.data("children");
if ($children !== undefined) {
$openCategory = $a;
if (!$children.is(":visible")) {
// Open the clicked category
$children.slideDown();
}
}
totalAchievementsEarned += earned;
totalAchievements += categoryAchievements.length;
openCategory(cat);
return false;
});
return $a;
}
function makeAchievement(achievement) {
const isEarned = achievement.id in achievementsData.earned;
if (!isEarned && achievement.category === 81) {
// Show unlocked feats of strength only
return;
}
const $achievement = $achievementTemplate
.clone(false)
.appendTo($("#achievements"));
$achievement.find(".name").text(achievement.title);
$achievement.find(".description").text(achievement.description);
$achievement.find(".points .value").text(achievement.points);
if (achievement.points === 0) {
$achievement.find(".points").addClass("no-points");
}
$achievement.find(".iconlarge .icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/large/${achievement.icon}.jpg")`);
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
if (isEarned) {
const earned = achievementsData.earned[achievement.id];
const date = new Date(earned.date * 1000);
$achievement.find(".earned-date").text(date.toLocaleDateString());
$achievement.addClass("earned");
}
return $achievement;
}
function openCategory(cat) {
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id);
$("#achievements").empty();
$(".summary").hide();
for (const achievement of categoryAchievements) {
makeAchievement(achievement);
}
}
function closeCategory() {
if ($openCategory === undefined || $openCategory.data("children") === undefined) {
return;
}
$openCategory
.data("children")
.slideUp();
}
function openSummary() {
closeCategory();
$("#achievements").empty();
const achievements = Object.keys(achievementsData.earned)
.filter(key => achievementsData.earned.hasOwnProperty(key))
.map(key => parseInt(key, 10))
.map(achievement => {
return {
name: cat.nameLang0,
earned,
total: categoryAchievements.length,
achievement,
...achievementsData.earned[achievement],
};
};
makeProgressBarRow([{ ...findProgress(92) }, { ...findProgress(96) }]);
makeProgressBarRow([{ ...findProgress(97) }, { ...findProgress(95) }]);
makeProgressBarRow([{ ...findProgress(168) }, { ...findProgress(169) }]);
makeProgressBarRow([{ ...findProgress(201) }, { ...findProgress(155) }]);
const $totalRow = makeProgressBarRow([{ name: "Achievements Earned", earned: totalAchievementsEarned, total: totalAchievements }]);
$totalRow.prependTo($("#progress-bars")); // Move the total bar to the top
});
const mostRecent = achievements.sort((a, b) => b.date - a.date).slice(0, 4);
for (const row of mostRecent) {
const achievement = achievementsData.achievements.find(ach => ach.id === row.achievement);
makeAchievement(achievement);
}
$.getJSON(`{{websiteRoot}}/character/{{{realm}}}/{{guid}}/achievements/data`, (data) => {
achievementsData = data;
$(".summary").show();
}
const $summaryLink = $("<a>")
.attr("href", "#")
.text("Summary")
.on("click", openSummary)
.appendTo($("#categories"));
function makeProgressBarRow(barsData) {
const $row = $("<div>").addClass("columns");
$("#progress-bars").append($row);
for (const cat of achievementsData.categories.filter(c => c.parent < 0)) {
if (cat.id === 1) {
// Skip Statistics category
continue;
}
const children = achievementsData.categories.filter(c => c.parent === cat.id);
for (const bar of barsData) {
let $bar = $progressTemplate.clone(false);
$bar.find(".title").text(bar.name);
$bar.find(".value").text(`${bar.earned} / ${bar.total}`);
$bar.find(".progress").attr("value", bar.earned).attr("max", bar.total);
$row.append($bar);
}
const $cat = makeCategoryLink(cat);
$("#categories").append($cat);
return $row;
}
if (children.length > 0) {
const $children = $("<div>")
.hide()
.addClass("subcategories");
$cat.data("children", $children);
for (const child of children) {
const $child = makeCategoryLink(child);
$child.appendTo($children);
}
function makeProgressBars() {
let totalAchievements = 0;
let totalAchievementsEarned = 0;
$("#categories").append($children);
}
}
let totalPoints = 0;
for (const ach of achievementsData.achievements) {
const findProgress = (catId) => {
const cat = achievementsData.categories.find(cat => cat.id === catId);
const subCategories = achievementsData.categories.filter(cat => cat.parent === catId);
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id || subCategories.some(cat => cat.id === ach.category));
let earned = 0;
for (const ach of categoryAchievements) {
if (ach.id in achievementsData.earned) {
totalPoints += ach.points;
++earned;
}
}
$("#total-points").text(totalPoints);
totalAchievementsEarned += earned;
totalAchievements += categoryAchievements.length;
makeProgressBars();
openSummary();
});
return {
name: cat.nameLang0,
earned,
total: categoryAchievements.length,
};
};
makeProgressBarRow([{ ...findProgress(92) }, { ...findProgress(96) }]);
makeProgressBarRow([{ ...findProgress(97) }, { ...findProgress(95) }]);
makeProgressBarRow([{ ...findProgress(168) }, { ...findProgress(169) }]);
makeProgressBarRow([{ ...findProgress(201) }, { ...findProgress(155) }]);
const $totalRow = makeProgressBarRow([{ name: "Achievements Earned", earned: totalAchievementsEarned, total: totalAchievements }]);
$totalRow.prependTo($("#progress-bars")); // Move the total bar to the top
}
$.getJSON(`{{websiteRoot}}/character/{{realm}}/{{guid}}/achievements/data`, (data) => {
achievementsData = data;
const $summaryLink = $("<a>")
.attr("href", "#")
.text("Summary")
.on("click", openSummary)
.appendTo($("#categories"));
for (const cat of achievementsData.categories.filter(c => c.parent < 0)) {
if (cat.id === 1) {
// Skip Statistics category
continue;
}
const children = achievementsData.categories.filter(c => c.parent === cat.id);
const $cat = makeCategoryLink(cat);
$("#categories").append($cat);
if (children.length > 0) {
const $children = $("<div>")
.hide()
.addClass("subcategories");
$cat.data("children", $children);
for (const child of children) {
const $child = makeCategoryLink(child);
$child.appendTo($children);
}
$("#categories").append($children);
}
}
let totalPoints = 0;
for (const ach of achievementsData.achievements) {
if (ach.id in achievementsData.earned) {
totalPoints += ach.points;
}
}
$("#total-points").text(totalPoints);
makeProgressBars();
openSummary();
});
</script>

View file

@ -20,9 +20,7 @@
<div class="column is-full-touch is-one-quarter-desktop">
<div class="box arena-team" data-team-id="{{this.id}}">
<div class="info has-text-centered">
<div class="is-size-4">
<a href="{{../websiteRoot}}/arena/team/{{../realm}}/{{this.name}}">{{this.name}}</a>
</div>
<div class="is-size-4">{{this.name}}</div>
<div class="is-size-5">{{this.type}}v{{this.type}} Team</div>
<div>Rating: {{this.rating}}</div>
<div>{{this.seasonWins}} W / {{subtract this.seasonGames this.seasonWins}} L</div>
@ -38,9 +36,7 @@
</div>
<script type="application/javascript">
$(window).on("load", () => {
{{#each arenaTeams}}
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-team[data-team-id={{this.id}}] .arena-emblem")[0]);
{{/each}}
});
{{#each arenaTeams}}
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-team[data-team-id={{this.id}}] .arena-emblem")[0]);
{{/each}}
</script>

View file

@ -3,6 +3,7 @@
<script type="application/javascript">
const aowow_tooltips = { "renamelinks": true, };
</script>
<script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script>
{{> character-header }}
@ -46,193 +47,189 @@
</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">
$(window).on("load", () => {
const talentsData = {{{JSONstringify data}}};
const talentsData = {{{JSONstringify data}}};
function createTree(data, learnedTalents, container) {
const $columns = $(container).find(".col");
let points = 0;
for (let spec = 0; spec < 2; ++spec) {
const $spec = $(`#talents-spec-${spec}`);
for (let i = 0; i < 4; ++i) {
const nbRows = Math.max(...data.spells.filter(s => s.columnIndex === i).map(s => s.tierId));
const $column = $columns.eq(i);
if (nbRows < 0) {
// Remove empty column
$column.remove();
continue;
}
for (let j = 0; j <= nbRows; ++j) {
$("<div>")
.addClass("iconmedium")
.appendTo($column);
}
}
for (const spell of data.spells) {
const $column = $columns.eq(spell.columnIndex);
const $placeholder = $column.find(".iconmedium").eq(spell.tierId);
const $template = $("#talent-template");
const $talent = $template.clone(false);
$talent.removeAttr("id");
$talent.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${spell.icon}.jpg")`);
const rank = spell.id in learnedTalents ? (learnedTalents[spell.id].rank - 1) : 0;
$talent.find("a").attr("href", `{{aowow}}/?spell=${spell["spellRank" + rank]}`);
$talent.data("id", spell.id);
if (spell.id in learnedTalents) {
points += learnedTalents[spell.id].rank;
const ranks = [spell.spellRank0, spell.spellRank1, spell.spellRank2, spell.spellRank3, spell.spellRank4].filter(r => r !== 0).length;
const maxRank = learnedTalents[spell.id].rank === ranks;
$talent
.removeClass("empty")
.addClass(maxRank ? "full" : "notfull");
$talent.attr("rank", `${learnedTalents[spell.id].rank}/${ranks}`);
$talent.data("rank", learnedTalents[spell.id].rank);
}
$placeholder.replaceWith($talent);
}
// Add arrows
for (let i = 0; i < 4; ++i) {
const $column = $columns.eq(i);
for (const icon of $column.find(".iconmedium")) {
const $icon = $(icon);
const spell = data.spells.find(s => s.id === $icon.data("id"));
if (spell === undefined || spell.prereqTalent0 === 0) {
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;
}
const req = data.spells.find(s => s.id === spell.prereqTalent0);
if (req === undefined) {
continue;
}
if (req.columnIndex === spell.columnIndex) {
// Down
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 25;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
} else if (req.tierId === spell.tierId && req.columnIndex < spell.columnIndex) {
// Right
const width = (44 + 14) * (spell.columnIndex - req.columnIndex - 1) + 23;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, -(width - 8), undefined, 16, "right" + arrowFile, "right center", $icon);
} else if (req.tierId === spell.tierId && req.columnIndex > spell.columnIndex) {
// Left
const width = (44 + 14) * (req.columnIndex - spell.columnIndex - 1) + 23;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, undefined, -(width - 8), 16, "left" + arrowFile, "left center", $icon)
} else if (req.tierId < spell.tierId && req.columnIndex < spell.columnIndex) {
// Right Down
const width = 44 * (spell.columnIndex - req.columnIndex) + 2;
const left = -((44 + 14) * (spell.columnIndex - req.columnIndex - 1) + 15);
const top = -((44 + 14) * (spell.tierId - req.tierId) - 14);
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, left, undefined, top, "rightdown" + arrowFile, "right center", $icon);
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 40;
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
} else {
// Left Down
const width = 44 * (req.columnIndex - spell.columnIndex) - 1;
const right = -((44 + 14) * (req.columnIndex - spell.columnIndex - 1) + 15);
const top = -((44 + 14) * (spell.tierId - req.tierId) - 14);
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, undefined, right, top, "leftdown" + arrowFile, "left center", $icon);
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 40;
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
}
learned[s.id] = {
spell: s,
rank: idx + 1,
};
++nbLearned;
break;
}
}
$(container).find(".header .points").text(points);
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]);
}
function createArrow(width, height, left, right, top, file, bgPos, container) {
const $arrow = $("<div>");
$arrow.css("position", "absolute");
if (left !== undefined) {
$arrow.css("left", `${left}px`);
}
if (right !== undefined) {
$arrow.css("right", `${right}px`);
}
$arrow.css("top", `${top}px`);
$arrow.css("width", `${width}px`);
$arrow.css("height", `${height}px`);
$arrow.css("background-image", `url("{{aowow}}/static/images/TalentCalc/arrows/${file}.png")`);
$arrow.css("background-position", bgPos);
$(container).append($arrow);
if (nbLearned === 0 && spec !== 0) {
$spec.addClass("hidden");
$("#spec-links").hide();
continue;
}
function main() {
for (let spec = 0; spec < 2; ++spec) {
const $spec = $(`#talents-spec-${spec}`);
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);
}
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;
}
}
function createTree(data, learnedTalents, container) {
const $columns = $(container).find(".col");
let points = 0;
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);
for (let i = 0; i < 4; ++i) {
const nbRows = Math.max(...data.spells.filter(s => s.columnIndex === i).map(s => s.tierId));
const $column = $columns.eq(i);
if (nbRows < 0) {
// Remove empty column
$column.remove();
continue;
}
for (let j = 0; j <= nbRows; ++j) {
$("<div>")
.addClass("iconmedium")
.appendTo($column);
}
}
createTree(tree, learned, $tree[0]);
}
for (const spell of data.spells) {
const $column = $columns.eq(spell.columnIndex);
const $placeholder = $column.find(".iconmedium").eq(spell.tierId);
const $template = $("#talent-template");
const $talent = $template.clone(false);
$talent.removeAttr("id");
$talent.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${spell.icon}.jpg")`);
const rank = spell.id in learnedTalents ? (learnedTalents[spell.id].rank - 1) : 0;
$talent.find("a").attr("href", `{{aowow}}/?spell=${spell["spellRank" + rank]}`);
$talent.data("id", spell.id);
if (nbLearned === 0 && spec !== 0) {
$spec.addClass("hidden");
$("#spec-links").hide();
if (spell.id in learnedTalents) {
points += learnedTalents[spell.id].rank;
const ranks = [spell.spellRank0, spell.spellRank1, spell.spellRank2, spell.spellRank3, spell.spellRank4].filter(r => r !== 0).length;
const maxRank = learnedTalents[spell.id].rank === ranks;
$talent
.removeClass("empty")
.addClass(maxRank ? "full" : "notfull");
$talent.attr("rank", `${learnedTalents[spell.id].rank}/${ranks}`);
$talent.data("rank", learnedTalents[spell.id].rank);
}
$placeholder.replaceWith($talent);
}
// Add arrows
for (let i = 0; i < 4; ++i) {
const $column = $columns.eq(i);
for (const icon of $column.find(".iconmedium")) {
const $icon = $(icon);
const spell = data.spells.find(s => s.id === $icon.data("id"));
if (spell === undefined || spell.prereqTalent0 === 0) {
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>");
const req = data.spells.find(s => s.id === spell.prereqTalent0);
if (req === undefined) {
continue;
}
if (req.columnIndex === spell.columnIndex) {
// Down
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 25;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
} else if (req.tierId === spell.tierId && req.columnIndex < spell.columnIndex) {
// Right
const width = (44 + 14) * (spell.columnIndex - req.columnIndex - 1) + 23;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, -(width - 8), undefined, 16, "right" + arrowFile, "right center", $icon);
} else if (req.tierId === spell.tierId && req.columnIndex > spell.columnIndex) {
// Left
const width = (44 + 14) * (req.columnIndex - spell.columnIndex - 1) + 23;
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, undefined, -(width - 8), 16, "left" + arrowFile, "left center", $icon)
} else if (req.tierId < spell.tierId && req.columnIndex < spell.columnIndex) {
// Right Down
const width = 44 * (spell.columnIndex - req.columnIndex) + 2;
const left = -((44 + 14) * (spell.columnIndex - req.columnIndex - 1) + 15);
const top = -((44 + 14) * (spell.tierId - req.tierId) - 14);
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, left, undefined, top, "rightdown" + arrowFile, "right center", $icon);
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 40;
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
} else {
// Left Down
const width = 44 * (req.columnIndex - spell.columnIndex) - 1;
const right = -((44 + 14) * (req.columnIndex - spell.columnIndex - 1) + 15);
const top = -((44 + 14) * (spell.tierId - req.tierId) - 14);
const arrowFile = $icon.data("rank") > 0 ? "2" : "";
createArrow(width, 15, undefined, right, top, "leftdown" + arrowFile, "left center", $icon);
const height = (44 + 14) * (spell.tierId - req.tierId - 1) + 40;
createArrow(15, height, 16, undefined, -(height - 8), "down" + arrowFile, "center bottom", $icon);
}
$glyphs.parent().appendTo($spec);
}
$("#link-spec-0").on("click", () => {
$("#talents-spec-0").removeClass("hidden");
$("#talents-spec-1").addClass("hidden");
});
$("#link-spec-1").on("click", () => {
$("#talents-spec-1").removeClass("hidden");
$("#talents-spec-0").addClass("hidden");
});
if (location.hash === "#1") {
$("#link-spec-1").click();
}
$("<script>")
.attr("src", "{{aowow}}/static/widgets/power.js")
.appendTo("body");
}
main();
$(container).find(".header .points").text(points);
}
function createArrow(width, height, left, right, top, file, bgPos, container) {
const $arrow = $("<div>");
$arrow.css("position", "absolute");
if (left !== undefined) {
$arrow.css("left", `${left}px`);
}
if (right !== undefined) {
$arrow.css("right", `${right}px`);
}
$arrow.css("top", `${top}px`);
$arrow.css("width", `${width}px`);
$arrow.css("height", `${height}px`);
$arrow.css("background-image", `url("{{aowow}}/static/images/TalentCalc/arrows/${file}.png")`);
$arrow.css("background-position", bgPos);
$(container).append($arrow);
}
$("#link-spec-0").on("click", () => {
$("#talents-spec-0").removeClass("hidden");
$("#talents-spec-1").addClass("hidden");
});
$("#link-spec-1").on("click", () => {
$("#talents-spec-1").removeClass("hidden");
$("#talents-spec-0").addClass("hidden");
});
if (location.hash === "#1") {
$("#link-spec-1").click();
}
</script>

View file

@ -1,13 +1,8 @@
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/character.css">
{{> 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="{{aowow}}/static/widgets/power.js"></script>
{{> character-header }}
@ -21,16 +16,8 @@
<span data-icon-size="large" class="icon-size is-hidden-mobile"></span>
<span data-icon-size="medium" class="icon-size is-hidden-tablet"></span>
<div class="columns">
<div class="column">
<label class="checkbox">
<input id="cb-enable-3d-viewer" type="checkbox">
Enable 3D viewer
</label>
</div>
</div>
<div id="3d-viewer-options" class="columns">
<br>
<div id="char-options" class="columns">
<div class="column is-one-quarter">
<label class="checkbox">
<input id="cb-hide-helmet" type="checkbox">
@ -48,26 +35,12 @@
<input id="cb-hide-tabard" type="checkbox">
Hide tabard
</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 id="mounts-container" class="column">
<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="border"></div>
<a target="_blank"></a>
@ -76,385 +49,234 @@
</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="icon"></div>
<div class="border"></div>
<a target="_blank"></a>
</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">
$(window).on("load", () => {
const $model = $("#model");
const $itemSlotTemplate = $("#item-slot-template").detach().removeAttr("id");
const $mountTemplate = $("#mount-template").detach().removeAttr("id");
const $model = $("#model");
const $itemSlotTemplate = $("#item-slot-template").detach().removeAttr("id");
const $mountTemplate = $("#mount-template").detach().removeAttr("id");
window.WH = {
//debug: console.log,
debug: () => { },
};
window.WH = {
//debug: console.log,
debug: () => { },
};
const charData = {{{ JSONstringify data }}};
const races = {
1: "human",
2: "orc",
3: "dwarf",
4: "nightelf",
5: "scourge",
6: "tauren",
7: "gnome",
8: "troll",
10: "bloodelf",
11: "draenei",
};
const genders = ["male", "female"];
const charData = {{{JSONstringify data}}};
const races = {
1: "human",
2: "orc",
3: "dwarf",
4: "nightelf",
5: "scourge",
6: "tauren",
7: "gnome",
8: "troll",
10: "bloodelf",
11: "draenei",
};
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");
const hideHelm = (charData.flags & 0x0400) === 0x0400;
$("#cb-hide-helmet").prop("checked", hideHelm);
const hideCloak = (charData.flags & 0x0800) === 0x0800;
$("#cb-hide-cloak").prop("checked", hideCloak);
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");
}
});
$("#cb-hide-helmet").change(() => {
const hide = $("#cb-hide-helmet").prop("checked");
setSlotVisible(1, !hide);
});
$("#cb-hide-cloak").change(() => {
const hide = $("#cb-hide-cloak").prop("checked");
setSlotVisible(16, !hide);
});
$("#cb-hide-tabard").change(() => {
const hide = $("#cb-hide-tabard").prop("checked");
setSlotVisible(19, !hide);
});
const hideHelm = (charData.flags & 0x0400) === 0x0400;
$("#cb-hide-helmet").prop("checked", hideHelm);
const hideCloak = (charData.flags & 0x0800) === 0x0800;
$("#cb-hide-cloak").prop("checked", hideCloak);
let viewer;
function createViewer() {
viewer?.destroy();
viewer = new ZamModelViewer(characterModel);
}
$("#cb-hide-helmet").change(() => {
const hide = $("#cb-hide-helmet").prop("checked");
setSlotVisible(1, !hide);
});
$("#cb-hide-cloak").change(() => {
const hide = $("#cb-hide-cloak").prop("checked");
setSlotVisible(16, !hide);
});
$("#cb-hide-tabard").change(() => {
const hide = $("#cb-hide-tabard").prop("checked");
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;
function createViewer() {
if (Cookies.get("disable-3d-viewer") === "1") {
return;
}
viewer?.destroy();
viewer = new ZamModelViewer(characterModel);
const isLoadedInterval = setInterval(() => {
if (viewer.method("isLoaded")) {
clearInterval(isLoadedInterval);
setBackground("background.png");
}
}, 100);
}
function setSlotVisible(slot, visible) {
if (visible) {
const transmogged = !$("#cb-hide-transmogs").prop("checked");
const items = transmogged ? charData.characterModelTransmogs : charData.characterModelItems;
const item = items.find(([sl, appearance]) => sl === slot);
if (item !== undefined) {
if (item[1] === -1) {
clearSlots([slot]);
} else {
setItems([item]);
}
}
} else {
clearSlots([slot]);
}
}
function clearSlots(slots) {
viewer.method("clearSlots", slots.join(","));
characterModel.items = characterModel.items.filter(item => !slots.includes(item[0]));
}
function setItems(items) {
const data = items.map(([slot, display]) => {
return {
slot,
display,
};
});
viewer.method("setItems", [data]);
characterModel.items = characterModel.items.filter(item => !items.some(item2 => item[0] === item2[0]));
characterModel.items.push(...items);
}
function createItemSlot(item, invSlot, icon, quality, rel, container) {
const $item = $itemSlotTemplate.clone();
$item
.data("icon", icon)
.data("quality", quality)
.find(".inventory-slot")
.css("background-image", `url("{{websiteRoot}}/img/inventory-slot/${invSlot}.png")`);
function setSlotVisible(slot, visible) {
if (visible) {
const item = charData.characterModelItems.find(([sl, appearance]) => sl === slot);
if (item !== undefined) {
$item.data("item", item);
$item.find("a").attr("href", `{{aowow}}/?item=${item}`);
$item.find("a").attr("rel", rel);
setItems([item]);
}
$item.appendTo($(container));
} else {
clearSlots([slot]);
}
const invSlotContainers = [
{ element: "#equipment-col-left", slots: [0, 1, 2, 14, 4, 3, 18, 8] },
{ element: "#equipment-col-right", slots: [9, 5, 6, 7, 10, 11, 12, 13] },
{ element: "#equipment-bottom", slots: [15, 16, 17] }
];
for (const side of invSlotContainers) {
for (const slot of side.slots) {
const item = charData.equipment.find(item => item.slot === slot);
const rel = [];
}
function clearSlots(slots) {
viewer.method("clearSlots", slots.join(","));
characterModel.items = characterModel.items.filter(item => !slots.includes(item[0]));
}
function setItems(items) {
const data = items.map(([slot, display]) => {
return {
slot,
display,
};
});
viewer.method("setItems", [data]);
characterModel.items = characterModel.items.filter(item => !items.some(item2 => item[0] === item2[0]));
characterModel.items.push(...items);
}
function createItemSlot(item, invSlot, icon, rel, container) {
const $item = $itemSlotTemplate.clone();
$item
.data("icon", icon)
.find(".inventory-slot")
.css("background-image", `url("{{websiteRoot}}/img/inventory-slot/${invSlot}.png")`);
if (item !== undefined) {
$item.data("item", item);
$item.find("a").attr("href", `{{aowow}}/?item=${item}`);
$item.find("a").attr("rel", rel);
}
$item.appendTo($(container));
}
const invSlotContainers = [
{ element: "#equipment-col-left", slots: [0, 1, 2, 14, 4, 3, 18, 8] },
{ element: "#equipment-col-right", slots: [9, 5, 6, 7, 10, 11, 12, 13] },
{ element: "#equipment-bottom", slots: [15, 16, 17] }
];
for (const side of invSlotContainers) {
for (const slot of side.slots) {
const item = charData.equipment.find(item => item.slot === slot);
const rel = [];
if (item !== undefined) {
rel.push("pcs=" + charData.equipment.map(item => item.itemEntry).join(":"));
if (item.gems.length !== 0) {
rel.push("gems=" + item.gems.join(":"));
}
if (item.enchantments.length !== 0) {
rel.push("ench=" + item.enchantments.join(":"));
}
if (item.randomPropertyId !== 0) {
rel.push("rand=" + item.randomPropertyId);
}
}
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);
function onResize(init = false) {
const iconSize = $(".icon-size:visible").data("icon-size");
const iconSizePx = iconSize === "medium" ? 44 : 68;
$("#model-container").css("height", (iconSizePx * nbItemRows) + "px");
$(".item-slot")
.removeClass("iconmedium iconlarge")
.addClass(`icon${iconSize}`)
.each((idx, el) => {
const $item = $(el);
const item = $item.data("item");
const icon = $item.data("icon");
if (item !== undefined) {
rel.push("pcs=" + charData.equipment.map(item => item.itemEntry).join(":"));
if (item.gems.length !== 0) {
rel.push("gems=" + item.gems.join(":"));
}
if (item.enchantments.length !== 0) {
rel.push("ench=" + item.enchantments.join(":"));
}
if (item.randomPropertyId !== 0) {
rel.push("rand=" + item.randomPropertyId);
}
$item.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/${iconSize}/${icon}.jpg")`);
}
createItemSlot(item?.itemEntry, slot, item?.icon?.toLowerCase(), item?.quality, rel.join("&"), side.element);
}
});
if (!init) {
characterModel.aspect = $model.outerWidth() / $model.outerHeight();
createViewer();
}
const nbItemRows = Math.max($("#equipment-col-left").children().length, $("#equipment-col-right").children().length);
function onResize(init = false) {
const iconSize = $(".icon-size:visible").data("icon-size");
const iconSizePx = iconSize === "medium" ? 44 : 68;
$("#model-container").css("height", (iconSizePx * nbItemRows) + "px");
$(".item-slot")
.removeClass("iconmedium iconlarge")
.addClass(`icon${iconSize}`)
.each((idx, el) => {
const $item = $(el);
const item = $item.data("item");
const icon = $item.data("icon");
const quality = $item.data("quality");
if (item !== undefined) {
$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) {
characterModel.aspect = $model.outerWidth() / $model.outerHeight();
createViewer();
}
}
onResize(true);
function setMount(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;
}
}
onResize(true);
function setMount(mountId) {
if (characterModel.mount.id !== mountId) {
characterModel.mount.id = mountId;
createViewer();
}
}
function setBackground(file) {
if (!viewer) {
return;
}
if (charData.mounts.length === 0) {
$("#mounts-container").hide();
} else {
const $dismount = $mountTemplate.clone();
$dismount.find(".icon").css("background-image", `url("{{websiteRoot}}/img/UI-GearManager-LeaveItem.png")`);
$dismount.find("a")
.on("click", () => {
setMount(0);
return false;
});
$("#mounts").append($dismount);
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) {
$("#mounts-container").hide();
} else {
const $dismount = $mountTemplate.clone();
$dismount.find(".icon").css("background-image", `url("{{websiteRoot}}/img/UI-GearManager-LeaveItem.png")`);
$dismount.find("a")
for (const mount of charData.mounts) {
const $mount = $mountTemplate.clone();
$mount.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${mount.icon}.jpg")`);
$mount.find("a")
.attr("href", `{{aowow}}/?spell=${mount.spell}`)
.on("click", () => {
setMount(0);
setMount(mount.creatureDisplayId);
return false;
});
$("#mounts").append($dismount);
for (const mount of charData.mounts) {
const $mount = $mountTemplate.clone();
$mount.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${mount.icon}.jpg")`);
$mount.find("a")
.attr("href", `{{aowow}}/?spell=${mount.spell}`)
.on("click", () => {
setMount(mount.creatureDisplayId);
return false;
});
$("#mounts").append($mount);
}
$("#mounts").append($mount);
}
$("#btn-mounts").on("click", () => {
$("#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 = {
type: ZamModelViewer.WOW,
contentPath: "{{ contentPath }}",
container: $model,
hd: true,
aspect: $model.outerWidth() / $model.outerHeight(),
charCustomization: {
race: charData.race,
gender: charData.gender,
options: charData.customizationOptions,
sheathMain: -1,
sheathOff: -1,
},
cls: charData.class,
items: (charData.characterModelTransmogs ?? charData.characterModelItems)
.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak)) && item[1] !== -1),
models: {
type: ZamModelViewer.Wow.Types.CHARACTER,
id: `${races[charData.race]}${genders[charData.gender]}`,
},
mount: {
type: ZamModelViewer.Wow.Types.NPC,
id: 0,
},
};
createViewer();
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
let windowWidth = $(window).width();
$(window).resize(debounce(() => {
const newWidth = $(window).width();
if (windowWidth === newWidth) {
// Do nothing if only the height changed.
// This prevents triggering a resize when the navigation bar moves on mobile when scrolling.
return;
}
windowWidth = newWidth;
onResize();
}, 500));
$("<script>")
.attr("src", "{{aowow}}/static/widgets/power.js")
.appendTo("body");
}
$("#btn-mounts").on("click", () => {
$("#mounts").slideToggle("fast");
});
const characterModel = {
type: ZamModelViewer.WOW,
contentPath: "{{websiteRoot}}/data/",
background: "background.png",
container: $model,
hd: true,
aspect: $model.outerWidth() / $model.outerHeight(),
charCustomization: {
race: charData.race,
gender: charData.gender,
options: charData.customizationOptions,
sheathMain: -1,
sheathOff: -1,
},
cls: charData.class,
items: charData.characterModelItems.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak))),
models: {
type: ZamModelViewer.Wow.Types.CHARACTER,
id: `${races[charData.race]}${genders[charData.gender]}`,
},
mount: {
type: ZamModelViewer.Wow.Types.NPC,
id: 0,
},
};
createViewer();
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
let windowWidth = $(window).width();
$(window).resize(debounce(() => {
const newWidth = $(window).width();
if (windowWidth === newWidth) {
// Do nothing if only the height changed.
// This prevents triggering a resize when the navigation bar moves on mobile when scrolling.
return;
}
windowWidth = newWidth;
onResize();
}, 500));
</script>

View file

@ -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;
}

View file

@ -24,12 +24,3 @@
height: 100% !important;
width: 100% !important;
}
.no-link-text a span {
display: none;
}
#list-transmogs-container,
#lbl-hide-transmogs {
display: none;
}

View file

@ -45,7 +45,6 @@
position: absolute;
background-repeat: no-repeat;
z-index: 1;
background-size: 36px;
}
.iconlarge {

View file

@ -1,3 +1,7 @@
#select-realm {
margin-bottom: 16px;
}
#results_processing {
height: 70px;
}

View file

@ -1,8 +0,0 @@
#select-arena-type-container {
display: flex;
}
#select-arena-type-container .arena-type-label {
align-self: center;
margin-right: 4px;
}

View file

@ -59,45 +59,43 @@
</table>
<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({
processing: true,
serverSide: true,
searchDelay: 800,
ajax: {
url: `{{websiteRoot}}/guild/{{{realm}}}/{{id}}/members`,
dt = $("#members").DataTable({
processing: true,
serverSide: true,
searchDelay: 800,
ajax: {
url: `{{websiteRoot}}/guild/{{realm}}/{{id}}/members`,
},
columnDefs: [
{
targets: 0,
render: (name) => `<a href="{{websiteRoot}}/character/{{realm}}/${name}">${name}</a>`,
},
columnDefs: [
{
targets: 0,
render: (name) => `<a href="{{websiteRoot}}/character/{{realm}}/${name}">${name}</a>`,
},
{
targets: 1,
render: (rank, type, row, meta) => meta.settings.json.ranks[rank],
},
{
searchable: false,
targets: 3,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/class_${data}.jpg">`,
},
{
searchable: false,
targets: 4,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/race_${data}.jpg">`,
},
{
searchable: false,
targets: 5,
render: online => online ? "🟢" : "🔴",
},
],
responsive: {
details: true,
{
targets: 1,
render: (rank, type, row, meta) => meta.settings.json.ranks[rank],
},
order: [[1, "asc"]],
});
{
searchable: false,
targets: 3,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/class_${data}.jpg">`,
},
{
searchable: false,
targets: 4,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/race_${data}.jpg">`,
},
{
searchable: false,
targets: 5,
render: online => online ? "🟢" : "🔴",
},
],
responsive: {
details: true,
},
order: [[1, "asc"]],
});
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

View file

@ -3,11 +3,8 @@
<h1 class="title is-size-1">Armory</h1>
<a href="{{websiteRoot}}/arena">Arena Ladder</a>&emsp;
<br><br>
{{#if (not (equalsLength realms 1))}}
<div id="select-realm-container">
<span class="realm-label">Realm:</span>
<div class="select">
@ -37,51 +34,49 @@
</table>
<script type="application/javascript">
$(window).on("load", () => {
let dt;
let dt;
$("#select-realm").on("change", () => {
dt.draw();
});
$("#select-realm").on("change", () => {
dt.draw();
});
dt = $("#results").DataTable({
processing: true,
serverSide: true,
searchDelay: 800,
ajax: {
url: `{{websiteRoot}}/search`,
data: d => {
d.realm = $("#select-realm").val();
},
dt = $("#results").DataTable({
processing: true,
serverSide: true,
searchDelay: 800,
ajax: {
url: `{{websiteRoot}}/search`,
data: d => {
d.realm = $("#select-realm").val();
},
columnDefs: [
{
targets: 0,
render: (name, type, row, meta) => `<a href="{{websiteRoot}}/character/${meta.settings.json.realm}/${name}">${name}</a>`,
},
{
targets: 1,
render: (guild, type, row, meta) => guild === null ? "" : `<a href="{{websiteRoot}}/guild/${meta.settings.json.realm}/${guild}">${guild}</a>`,
},
{
searchable: false,
targets: 3,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/class_${data}.jpg">`,
},
{
searchable: false,
targets: 4,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/race_${data}.jpg">`,
},
{
searchable: false,
targets: 5,
render: online => online ? "🟢" : "🔴",
},
],
responsive: {
details: true,
},
columnDefs: [
{
targets: 0,
render: (name, type, row, meta) => `<a href="{{websiteRoot}}/character/${meta.settings.json.realm}/${name}">${name}</a>`,
},
});
{
targets: 1,
render: (guild, type, row, meta) => guild === null ? "" : `<a href="{{websiteRoot}}/guild/${meta.settings.json.realm}/${guild}">${guild}</a>`,
},
{
searchable: false,
targets: 3,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/class_${data}.jpg">`,
},
{
searchable: false,
targets: 4,
render: data => `<img src="{{aowow}}/static/images/wow/icons/medium/race_${data}.jpg">`,
},
{
searchable: false,
targets: 5,
render: online => online ? "🟢" : "🔴",
},
],
responsive: {
details: true,
},
});
</script>

View file

@ -1,16 +1,14 @@
function waitForEmblemImages($emblem) {
return Promise.all(
$emblem.find(".images img").map((idx, img) => {
return new Promise((res, rej) => {
if (img.complete) {
res();
} else {
img.addEventListener("load", res);
img.addEventListener("error", rej);
}
});
}),
);
return Promise.all($emblem.find(".images img").map((idx, img) => {
return new Promise((res, rej) => {
if (img.complete) {
res();
} else {
img.addEventListener("load", res);
img.addEventListener("error", rej);
}
});
}));
}
function createGuildEmblem(emblem, el) {
@ -18,8 +16,7 @@ function createGuildEmblem(emblem, el) {
const canvas = $emblem.find("canvas")[0];
const ctx = canvas.getContext("2d");
const imgUrl = (type, section, value, value2) =>
`${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? "_" + value2 : ""}_T${section}_U.PNG`;
const imgUrl = (type, section, value, value2) => `${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? ("_" + value2) : ""}_T${section}_U.PNG`;
const $images = $("<div>").addClass("images").appendTo($emblem);
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 ctx = canvas.getContext("2d");
const imgUrl = (teamSize, type, value) =>
`${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? "-" + teamSize : ""}${type ? "-" + type : ""}${
value ? "-" + value : ""
}.PNG`;
const imgUrl = (teamSize, type, value) => `${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? ("-" + teamSize) : ""}${type ? ("-" + type) : ""}${value ? ("-" + value) : ""}.PNG`;
const $images = $("<div>").addClass("images").appendTo($emblem);
const banner = $("<img>").attr("src", imgUrl(teamSize)).appendTo($images)[0];

View file

@ -1,6 +1,3 @@
window.parent.postMessage(
{
url: window.location.pathname.replace(handlebarsData.websiteRoot, ""),
},
"*",
);
window.parent.postMessage({
url: window.location.pathname.replace(handlebarsData.websiteRoot, ""),
}, "*");

View file

@ -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>&emsp;
<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>
&ensp;
<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>

View file

@ -15,7 +15,6 @@
const handlebarsData = {{{JSONstringify locals}}};
</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}}
<script type="application/javascript">
@ -37,5 +36,3 @@
{{{ body }}}
</section>
</body>
</html>

View file

@ -1,10 +1,10 @@
<a href="{{websiteRoot}}/">Back to Armory</a>
<br><br>
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a>&emsp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a>&emsp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a>&emsp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a>&emsp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a>&ensp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a>&ensp;
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a>
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a>
<br>
<br>

View file

@ -1,7 +1,7 @@
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/icons.css">
<style>
.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 {
@ -9,7 +9,7 @@
}
.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 {

View file

@ -4,6 +4,8 @@
"outDir": "build",
"moduleResolution": "node"
},
"exclude": ["node_modules"],
"exclude": [
"node_modules"
],
"include": ["src/**/*.ts"]
}