diff --git a/src/armory/Armory.ts b/src/armory/Armory.ts index e063537..7f457da 100644 --- a/src/armory/Armory.ts +++ b/src/armory/Armory.ts @@ -1,259 +1,259 @@ -import * as path from 'path'; -import * as uuid from 'uuid'; -import { Express } from 'express'; -import * as express from 'express'; -import * as winston from 'winston'; -import * as morgan from 'morgan'; -import { Pool, createPool } from 'mysql2/promise'; -import { engine as handlebarsEngine } from 'express-handlebars'; +import * as path from "path"; +import * as uuid from "uuid"; +import { Express } from "express"; +import * as express from "express"; +import * as winston from "winston"; +import * as morgan from "morgan"; +import { Pool, createPool } from "mysql2/promise"; +import { engine as handlebarsEngine } from "express-handlebars"; -import { Config, IRealmConfig } from './Config'; -import { DbcManager } from './data/DbcReader'; -import { CharacterCustomization } from './data/CharacterCustomization'; -import { IndexController } from './controllers/IndexController'; -import { CharacterController } from './controllers/CharacterController'; -import { GuildController } from './controllers/GuildController'; +import { Config, IRealmConfig } from "./Config"; +import { DbcManager } from "./data/DbcReader"; +import { CharacterCustomization } from "./data/CharacterCustomization"; +import { IndexController } from "./controllers/IndexController"; +import { CharacterController } from "./controllers/CharacterController"; +import { GuildController } from "./controllers/GuildController"; export class Armory { - public characterCustomization: CharacterCustomization; - public dbc: DbcManager; - public config: Config; - public worldDb: Pool; - public logger: winston.Logger; - public charsetCache: { [key: string]: string }; + public characterCustomization: CharacterCustomization; + public dbc: DbcManager; + public config: Config; + public worldDb: Pool; + public logger: winston.Logger; + public charsetCache: { [key: string]: string }; - private charsDbs: { [key: string]: Pool }; - private errorNames: { [key: number]: string }; - private errorDescriptions: { [key: number]: string }; + private charsDbs: { [key: string]: Pool }; + private errorNames: { [key: number]: string }; + private errorDescriptions: { [key: number]: string }; - public constructor() { - this.dbc = new DbcManager(); - this.characterCustomization = new CharacterCustomization(); - this.charsDbs = {}; - this.logger = winston.createLogger({ - level: 'info', - format: winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), - winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`), - ), - transports: [ - new winston.transports.Console({ level: 'debug' }), - new winston.transports.File({ filename: path.join('logs', 'armory.error.log'), level: 'error' }), - new winston.transports.File({ filename: path.join('logs', 'armory.combined.log'), level: 'http' }), - ], - }); - this.charsetCache = {}; + public constructor() { + this.dbc = new DbcManager(); + this.characterCustomization = new CharacterCustomization(); + this.charsDbs = {}; + this.logger = winston.createLogger({ + level: "info", + format: winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }), + winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`), + ), + transports: [ + new winston.transports.Console({ level: "debug" }), + new winston.transports.File({ filename: path.join("logs", "armory.error.log"), level: "error" }), + new winston.transports.File({ filename: path.join("logs", "armory.combined.log"), level: "http" }), + ], + }); + this.charsetCache = {}; - this.errorNames = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - }; - this.errorDescriptions = { - 400: 'Invalid request.', - 404: 'Sorry, we could not find what you were looking for.', - 500: 'An unexpected internal error has occurred. Please contact the site owner.', - }; - } + this.errorNames = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 500: "Internal Server Error", + }; + this.errorDescriptions = { + 400: "Invalid request.", + 404: "Sorry, we could not find what you were looking for.", + 500: "An unexpected internal error has occurred. Please contact the site owner.", + }; + } - public async start(): Promise { - const app: Express = express(); - const listenPort = 48733; + public async start(): Promise { + const app: Express = express(); + const listenPort = 48733; - this.logger.info('Loading config...'); - this.config = await Config.load(this.logger); - this.logger.info('Loading data files...'); - if (this.config.loadDbcs) { - await this.dbc.loadAllFiles(); - } - await this.characterCustomization.loadData(); + this.logger.info("Loading config..."); + this.config = await Config.load(this.logger); + this.logger.info("Loading data files..."); + if (this.config.loadDbcs) { + await this.dbc.loadAllFiles(); + } + await this.characterCustomization.loadData(); - this.logger.info('Connecting to databases...'); - this.worldDb = createPool(this.config.worldDatabase); - for (const realm of this.config.realms) { - this.charsDbs[realm.name.toLowerCase()] = createPool(realm.charactersDatabase); - } + this.logger.info("Connecting to databases..."); + this.worldDb = createPool(this.config.worldDatabase); + for (const realm of this.config.realms) { + this.charsDbs[realm.name.toLowerCase()] = createPool(realm.charactersDatabase); + } - this.logger.info('Starting server...'); + this.logger.info("Starting server..."); - const locals = { - aowow: this.config.aowowUrl, - websiteUrl: this.config.websiteUrl, - websiteName: this.config.websiteName, - websiteRoot: this.config.websiteRoot, - iframeMode: this.config.iframeMode, - }; - for (const key in locals) { - if (locals.hasOwnProperty(key)) { - app.locals[key] = locals[key]; - } - } - app.locals.locals = locals; + const locals = { + aowow: this.config.aowowUrl, + websiteUrl: this.config.websiteUrl, + websiteName: this.config.websiteName, + websiteRoot: this.config.websiteRoot, + iframeMode: this.config.iframeMode, + }; + 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: { - ...require('handlebars-helpers')(), - }, - }), - ); - app.set('view engine', 'handlebars'); - app.set('views', path.join(process.cwd(), 'static')); + 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")); - app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { - req.id = uuid.v4(); - next(); - }); + app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { + req.id = uuid.v4(); + next(); + }); - morgan.token('id', (req: express.Request) => { - return req.id; - }); - morgan.token('ip', (req: express.Request) => { - const forwardedFor = req.headers['x-forwarded-for']; - if (forwardedFor) { - if (typeof forwardedFor === 'string') { - return forwardedFor; - } - return forwardedFor.join(', '); - } - 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()), - }, - }), - ); + morgan.token("id", (req: express.Request) => { + return req.id; + }); + morgan.token("ip", (req: express.Request) => { + const forwardedFor = req.headers["x-forwarded-for"]; + if (forwardedFor) { + if (typeof forwardedFor === "string") { + return forwardedFor; + } + return forwardedFor.join(", "); + } + 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('/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))); - app.get('/search', this.wrapRoute(indexController.search.bind(indexController))); + const indexController = new IndexController(this); + app.get("/", this.wrapRoute(indexController.index.bind(indexController))); + app.get("/search", this.wrapRoute(indexController.search.bind(indexController))); - const charsController = new CharacterController(this); - await charsController.load(); - app.get('/character/:realm/:name', this.wrapRoute(charsController.character.bind(charsController))); - app.get('/character/:realm/:name/talents', this.wrapRoute(charsController.talents.bind(charsController))); - app.get('/character/:realm/:name/achievements', this.wrapRoute(charsController.achievements.bind(charsController))); - app.get('/character/:realm/:character/achievements/data', this.wrapRoute(charsController.achievementsData.bind(charsController))); - app.get('/character/:realm/:name/pvp', this.wrapRoute(charsController.pvp.bind(charsController))); + const charsController = new CharacterController(this); + await charsController.load(); + app.get("/character/:realm/:name", this.wrapRoute(charsController.character.bind(charsController))); + app.get("/character/:realm/:name/talents", this.wrapRoute(charsController.talents.bind(charsController))); + app.get("/character/:realm/:name/achievements", this.wrapRoute(charsController.achievements.bind(charsController))); + app.get("/character/:realm/:character/achievements/data", this.wrapRoute(charsController.achievementsData.bind(charsController))); + app.get("/character/:realm/:name/pvp", this.wrapRoute(charsController.pvp.bind(charsController))); - const guildsController = new GuildController(this); - app.get('/guild/:realm/:name', this.wrapRoute(guildsController.guild.bind(guildsController))); - app.get('/guild/:realm/:guild/members', this.wrapRoute(guildsController.members.bind(guildsController))); + const guildsController = new GuildController(this); + app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController))); + app.get("/guild/:realm/:guild/members", this.wrapRoute(guildsController.members.bind(guildsController))); - app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => { - // Error handler + app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => { + // Error handler - if (err instanceof Error) { - const contents = err.stack ?? `${err.name}: ${err.message}`; - this.logger.error(`Error on request ${req.id}. ${contents}`); - } + if (err instanceof Error) { + const contents = err.stack ?? `${err.name}: ${err.message}`; + this.logger.error(`Error on request ${req.id}. ${contents}`); + } - let status = 500; - if (typeof err === 'number') { - status = err; - } + let status = 500; + if (typeof err === "number") { + status = err; + } - res.status(status).render('error.hbs', this.getErrorViewData(status, req)); - }); + res.status(status).render("error.hbs", this.getErrorViewData(status, req)); + }); - app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { - // 404 handler - res.status(404); + app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { + // 404 handler + res.status(404); - // Respond with html page - if (req.accepts('html')) { - return res.render('error.hbs', this.getErrorViewData(404, req)); - } + // Respond with html page + if (req.accepts("html")) { + return res.render("error.hbs", this.getErrorViewData(404, req)); + } - // Respond with json - if (req.accepts('json')) { - return res.json({ error: this.errorNames[404] }); - } + // Respond with json + if (req.accepts("json")) { + return res.json({ error: this.errorNames[404] }); + } - // Default to plain-text - res.type('txt').send(this.errorNames[404]); - }); + // Default to plain-text + res.type("txt").send(this.errorNames[404]); + }); - this.gc(); - app.listen(listenPort, '0.0.0.0', () => { - this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`); - }); - } + this.gc(); + app.listen(listenPort, "0.0.0.0", () => { + this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`); + }); + } - public getCharactersDb(realm: string): Pool { - return this.charsDbs[realm.toLowerCase()]; - } + public getCharactersDb(realm: string): Pool { + return this.charsDbs[realm.toLowerCase()]; + } - public getRealm(realm: string): IRealmConfig { - return this.config.realms.find((r) => r.name.toLowerCase() === realm.toLowerCase()); - } + public getRealm(realm: string): IRealmConfig { + return this.config.realms.find((r) => r.name.toLowerCase() === realm.toLowerCase()); + } - public async getDatabaseCharset(realm: string): Promise { - const db = this.getCharactersDb(realm); + public async getDatabaseCharset(realm: string): Promise { + const db = this.getCharactersDb(realm); - if (!(realm in this.charsetCache)) { - const [rows, fields] = await db.query({ - sql: ` + if (!(realm in this.charsetCache)) { + 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 WHERE CCSA.collation_name = T.table_collation AND T.table_schema = "${(await db.getConnection()).config.database}" AND T.table_name = "characters" `, - timeout: this.config.dbQueryTimeout, - }); - this.charsetCache[realm] = rows[0].charset; - } - return this.charsetCache[realm]; - } + timeout: this.config.dbQueryTimeout, + }); + this.charsetCache[realm] = rows[0].charset; + } + return this.charsetCache[realm]; + } - public gc(): void { - if (this.config.loadDbcs) { - return; - } + public gc(): void { + if (this.config.loadDbcs) { + return; + } - setTimeout(() => { - if (global.gc) { - global.gc(); - } - }, 500); - } + setTimeout(() => { + if (global.gc) { + global.gc(); + } + }, 500); + } - private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise) { - // Adds error handling for promise-based controller methods - return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - try { - await fn(req, res, next); - } catch (e) { - next(e); - } - }; - } + private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise) { + // Adds error handling for promise-based controller methods + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + await fn(req, res, next); + } catch (e) { + next(e); + } + }; + } - private getErrorViewData(status: number, req: express.Request) { - return { - status, - name: this.errorNames[status] || 'An error occurred', - description: this.errorDescriptions[status] || '', - reqId: req.id, - }; - } + private getErrorViewData(status: number, req: express.Request) { + return { + status, + name: this.errorNames[status] || "An error occurred", + description: this.errorDescriptions[status] || "", + reqId: req.id, + }; + } } diff --git a/src/armory/Config.ts b/src/armory/Config.ts index ab8170e..c3af755 100644 --- a/src/armory/Config.ts +++ b/src/armory/Config.ts @@ -1,180 +1,180 @@ -import * as fs from 'fs'; +import * as fs from "fs"; const fsp = fs.promises; -import * as winston from 'winston'; +import * as winston from "winston"; export interface IDatabaseConfig { - host: string; - port: number; - user: string; - password: string; - database: string; + host: string; + port: number; + user: string; + password: string; + database: string; } export interface IRealmConfig { - name: string; - realmId: number; - authDatabase: string; - charactersDatabase: IDatabaseConfig; + name: string; + realmId: number; + authDatabase: string; + charactersDatabase: IDatabaseConfig; } export interface IIframeModeConfig { - enabled: boolean; - url: string; + enabled: boolean; + url: string; } export class Config { - public aowowUrl: string; - public websiteUrl: string; - public websiteName: string; - public websiteRoot: string; - public iframeMode: IIframeModeConfig; - public loadDbcs: boolean; - public hideGameMasters: boolean; - public realms: IRealmConfig[]; - public worldDatabase: IDatabaseConfig; - public dbQueryTimeout: number; + public aowowUrl: string; + public websiteUrl: string; + public websiteName: string; + public websiteRoot: string; + public iframeMode: IIframeModeConfig; + public loadDbcs: boolean; + public hideGameMasters: boolean; + public realms: IRealmConfig[]; + public worldDatabase: IDatabaseConfig; + public dbQueryTimeout: number; - private static envPrefix: string = 'ACORE_ARMORY'; - private static checkedMissingField: boolean = false; + private static envPrefix: string = "ACORE_ARMORY"; + private static checkedMissingField: boolean = false; - public static async load(logger: winston.Logger): Promise { - try { - await fsp.access('config.json'); - return await Config.loadFromFile(logger); - } catch (err) { - return await Config.loadFromEnv(logger); - } - } + public static async load(logger: winston.Logger): Promise { + try { + await fsp.access("config.json"); + return await Config.loadFromFile(logger); + } catch (err) { + return await Config.loadFromEnv(logger); + } + } - private static async loadFromFile(logger: winston.Logger): Promise { - const json: Buffer = await fsp.readFile('config.json'); - const config = JSON.parse(json.toString()) as Config; + private static async loadFromFile(logger: winston.Logger): Promise { + const json: Buffer = await fsp.readFile("config.json"); + const config = JSON.parse(json.toString()) as Config; - if (!Config.checkedMissingField) { - const defaultConfigJson = await fsp.readFile('config.default.json'); - const defaultConfig = JSON.parse(defaultConfigJson.toString()); - Config.checkAllMissingFields(logger, config, defaultConfig); - Config.checkedMissingField = true; - } + if (!Config.checkedMissingField) { + const defaultConfigJson = await fsp.readFile("config.default.json"); + const defaultConfig = JSON.parse(defaultConfigJson.toString()); + Config.checkAllMissingFields(logger, config, defaultConfig); + Config.checkedMissingField = true; + } - return config; - } + return config; + } - private static async loadFromEnv(logger: winston.Logger): Promise { - const config = {}; - const json = await fsp.readFile('config.default.json'); - const defaultConfig = JSON.parse(json.toString()); - Config.loadObjFromEnv(logger, config, defaultConfig); - Config.checkedMissingField = true; - return config as Config; - } + private static async loadFromEnv(logger: winston.Logger): Promise { + const config = {}; + const json = await fsp.readFile("config.default.json"); + const defaultConfig = JSON.parse(json.toString()); + Config.loadObjFromEnv(logger, config, defaultConfig); + Config.checkedMissingField = true; + return config as Config; + } - private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName: string = '') { - if (parentName !== '') { - parentName += '.'; - } + private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName: string = "") { + if (parentName !== "") { + parentName += "."; + } - for (const field in model) { - if (!model.hasOwnProperty(field)) { - continue; - } + for (const field in model) { + if (!model.hasOwnProperty(field)) { + continue; + } - if (Array.isArray(model[field])) { - obj[field] = Config.loadArrayFromEnv(logger, model[field][0], parentName + field); - } else if (typeof model[field] === 'object') { - obj[field] = {}; - Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field); - } else if (!obj.hasOwnProperty(field)) { - const key = Config.getEnvKey(parentName + field); - 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!`); - } - } - } - } + if (Array.isArray(model[field])) { + obj[field] = Config.loadArrayFromEnv(logger, model[field][0], parentName + field); + } else if (typeof model[field] === "object") { + obj[field] = {}; + Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field); + } else if (!obj.hasOwnProperty(field)) { + const key = Config.getEnvKey(parentName + field); + 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!`); + } + } + } + } - private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ''): any[] { - if (parentName !== '') { - parentName += '.'; - } + private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ""): any[] { + if (parentName !== "") { + parentName += "."; + } - const arr = []; - let i = 0; - while (true) { - const key = Config.getEnvKey(parentName + i); - const found = Object.keys(process.env).some((k) => k.startsWith(key)); - if (!found) { - break; - } + const arr = []; + let i = 0; + while (true) { + const key = Config.getEnvKey(parentName + i); + const found = Object.keys(process.env).some((k) => k.startsWith(key)); + if (!found) { + break; + } - if (Array.isArray(model)) { - arr.push(Config.loadArrayFromEnv(logger, model[0], parentName + i)); - } else if (typeof model === 'object') { - const obj = {}; - Config.loadObjFromEnv(logger, obj, model, parentName + i); - if (Object.keys(obj).length > 0) { - arr.push(obj); - } - } else if (process.env.hasOwnProperty(key)) { - arr.push(Config.parseEnvValue(process.env[key], model)); - } else { - break; - } + if (Array.isArray(model)) { + arr.push(Config.loadArrayFromEnv(logger, model[0], parentName + i)); + } else if (typeof model === "object") { + const obj = {}; + Config.loadObjFromEnv(logger, obj, model, parentName + i); + if (Object.keys(obj).length > 0) { + arr.push(obj); + } + } else if (process.env.hasOwnProperty(key)) { + arr.push(Config.parseEnvValue(process.env[key], model)); + } else { + break; + } - ++i; - } + ++i; + } - return arr; - } + return arr; + } - private static getEnvKey(key: string): string { - return ( - Config.envPrefix + - '_' + - key - .replace(/\./g, '__') - .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) - .toUpperCase() - ); - } + private static getEnvKey(key: string): string { + return ( + Config.envPrefix + + "_" + + key + .replace(/\./g, "__") + .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + .toUpperCase() + ); + } - private static parseEnvValue(value: string, model: any): any { - const type = typeof model; - const lower = value.toLowerCase(); - if (type === 'boolean') { - return lower === 'true' || value === '1'; - } - if (type === 'number') { - return parseFloat(value); - } - return value; - } + private static parseEnvValue(value: string, model: any): any { + const type = typeof model; + const lower = value.toLowerCase(); + if (type === "boolean") { + return lower === "true" || value === "1"; + } + if (type === "number") { + return parseFloat(value); + } + return value; + } - private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName: string = '') { - const missing = Config.hasMissingFields(obj, model); - if (parentName !== '') { - parentName += '.'; - } - for (const field of missing) { - logger.warn(`Field ${parentName}${field} is missing from config.json!`); - } - for (const key in model) { - if (typeof model[key] === 'object' && obj.hasOwnProperty(key)) { - Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key); - } - } - } + private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName: string = "") { + const missing = Config.hasMissingFields(obj, model); + if (parentName !== "") { + parentName += "."; + } + for (const field of missing) { + logger.warn(`Field ${parentName}${field} is missing from config.json!`); + } + 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 missing = []; - for (const key in model) { - if (!obj.hasOwnProperty(key)) { - missing.push(key); - } - } - return missing; - } + private static hasMissingFields(obj: object, model: object): string[] { + const missing = []; + for (const key in model) { + if (!obj.hasOwnProperty(key)) { + missing.push(key); + } + } + return missing; + } } diff --git a/src/armory/DataTablesSsp.ts b/src/armory/DataTablesSsp.ts index f82e9f6..ee8b8e9 100644 --- a/src/armory/DataTablesSsp.ts +++ b/src/armory/DataTablesSsp.ts @@ -1,170 +1,170 @@ -import { Pool } from 'mysql2/promise'; -import { Query } from 'express-serve-static-core'; +import { Pool } from "mysql2/promise"; +import { Query } from "express-serve-static-core"; export interface IResult { - recordsTotal: number; - recordsFiltered: number; - draw: number; - data: any[][]; + recordsTotal: number; + recordsFiltered: number; + draw: number; + data: any[][]; } export interface IColumnSettings { - name: string; - collation?: string; - formatter?: (data: string | number | null, row: any) => string; - table?: string; - database?: string; + name: string; + collation?: string; + formatter?: (data: string | number | null, row: any) => string; + table?: string; + database?: string; } export interface IColumnJoin { - table1: string; - column1: string; - table2: string; - column2: string; - database2?: string; - kind: 'INNER' | 'FULL OUTER' | 'LEFT' | 'RIGHT'; + table1: string; + column1: string; + table2: string; + column2: string; + database2?: string; + kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT"; } export class DataTablesSsp { - public draw: number; - public joins: IColumnJoin[] = []; - public extraDataColumns: string[] = []; + public draw: number; + public joins: IColumnJoin[] = []; + public extraDataColumns: string[] = []; - private db: Pool; - private table: string; - private primaryKey: string; - private columnSettings: IColumnSettings[]; + private db: Pool; + private table: string; + private primaryKey: string; + private columnSettings: IColumnSettings[]; - private start: number; - private length: number; - private _order: { - column: number; - dir: string; - }[]; - private columns: { - data: number; - name: string; - searchable: boolean; - orderable: boolean; - search: { - value: string; - regex: boolean; - }; - }[]; - private search: { - value: string; - regex: boolean; - }; + private start: number; + private length: number; + private _order: { + column: number; + dir: string; + }[]; + private columns: { + data: number; + name: string; + searchable: boolean; + orderable: boolean; + search: { + value: string; + regex: boolean; + }; + }[]; + private search: { + value: string; + regex: boolean; + }; - private wheres: string[] = []; - private filterBindings: (string | number)[] = []; - private customBindings: (string | number)[] = []; - private filterWhereSql: string = '1'; - private customWhereSql: string = '1'; - private limitSql: string = ''; - private orderSql: string = ''; - private joinSql: string = ''; + private wheres: string[] = []; + private filterBindings: (string | number)[] = []; + private customBindings: (string | number)[] = []; + 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 = 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) => { - 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 any).value as string, - regex: (query.search as any).regex === 'true', - }; + public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) { + 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) => { + 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 any).value as string, + regex: (query.search as any).regex === "true", + }; - this.db = db; - this.table = table; - this.primaryKey = primaryKey; - this.columnSettings = columnSettings; - } + this.db = db; + this.table = table; + this.primaryKey = primaryKey; + this.columnSettings = columnSettings; + } - private colSettingsToStr(colSettings: IColumnSettings) { - const db = colSettings.database ? '`' + colSettings.database + '`.' : ''; - return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``; - } + private colSettingsToStr(colSettings: IColumnSettings) { + const db = colSettings.database ? "`" + colSettings.database + "`." : ""; + return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``; + } - private limit() { - if (this.start !== undefined && this.length !== -1) { - this.limitSql = `LIMIT ${this.length} OFFSET ${this.start}`; - } - return this; - } + private limit() { + if (this.start !== undefined && this.length !== -1) { + this.limitSql = `LIMIT ${this.length} OFFSET ${this.start}`; + } + return this; + } - private order() { - if (this._order === undefined) { - return this; - } + private order() { + if (this._order === undefined) { + return this; + } - const orderBy = []; - for (const order of this._order) { - const requestColumn = this.columns[order.column]; - if (!requestColumn.orderable) { - continue; - } + const orderBy = []; + for (const order of this._order) { + const requestColumn = this.columns[order.column]; + if (!requestColumn.orderable) { + continue; + } - const colSettings = this.columnSettings[requestColumn.data]; - orderBy.push(`${this.colSettingsToStr(colSettings)} ${order.dir}`); - } - orderBy.push(`\`${this.table}\`.\`${this.primaryKey}\``); + const colSettings = this.columnSettings[requestColumn.data]; + orderBy.push(`${this.colSettingsToStr(colSettings)} ${order.dir}`); + } + orderBy.push(`\`${this.table}\`.\`${this.primaryKey}\``); - if (orderBy.length > 0) { - this.orderSql = 'ORDER BY ' + orderBy.join(', '); - } + if (orderBy.length > 0) { + this.orderSql = "ORDER BY " + orderBy.join(", "); + } - return this; - } + return this; + } - private join() { - for (const join of this.joins) { - 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`; - } + private join() { + for (const join of this.joins) { + 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; - } + return this; + } - private filter() { - if (this.search.value?.length > 0) { - const filterWheres = []; - this.filterBindings = []; + private filter() { + if (this.search.value?.length > 0) { + const filterWheres = []; + this.filterBindings = []; - for (const col of this.columns) { - if (!col.searchable) { - continue; - } + for (const col of this.columns) { + if (!col.searchable) { + continue; + } - const colSettings = this.columnSettings[col.data]; - const collate = colSettings.collation !== undefined ? `COLLATE ${colSettings.collation} ` : ''; - filterWheres.push(`${this.colSettingsToStr(colSettings)} ${collate}LIKE ?`); - this.filterBindings.push(`%${this.search.value}%`); - } - if (filterWheres.length > 0) { - this.filterWhereSql = '(' + filterWheres.map((w) => `(${w})`).join(' OR ') + ')'; - } - } + const colSettings = this.columnSettings[col.data]; + const collate = colSettings.collation !== undefined ? `COLLATE ${colSettings.collation} ` : ""; + filterWheres.push(`${this.colSettingsToStr(colSettings)} ${collate}LIKE ?`); + this.filterBindings.push(`%${this.search.value}%`); + } + if (filterWheres.length > 0) { + this.filterWhereSql = "(" + filterWheres.map((w) => `(${w})`).join(" OR ") + ")"; + } + } - this.customWhereSql = this.wheres.map((w) => `(${w})`).join(' AND '); - return this; - } + 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]; - return ` - SELECT ${columns.join(', ')} + public sql(): string { + const columns = [...this.columnSettings.map((c) => this.colSettingsToStr(c)), ...this.extraDataColumns]; + return ` + SELECT ${columns.join(", ")} FROM ${this.table} ${this.joinSql} WHERE @@ -173,19 +173,19 @@ export class DataTablesSsp { ${this.orderSql} ${this.limitSql} `; - } + } - private buildTotalCountSql(): string { - return ` + private buildTotalCountSql(): string { + return ` SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\` FROM ${this.table} ${this.joinSql} WHERE ${this.customWhereSql} `; - } + } - private buildFilteredCountSql(): string { - return ` + private buildFilteredCountSql(): string { + return ` SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\` FROM ${this.table} ${this.joinSql} @@ -193,56 +193,56 @@ export class DataTablesSsp { ${this.filterWhereSql} AND ${this.customWhereSql} `; - } + } - public async run(queryTimeout: number = 10_000): Promise { - this.limit().order().join().filter(); + public async run(queryTimeout: number = 10_000): Promise { + this.limit().order().join().filter(); - const bindings = [...this.filterBindings, ...this.customBindings]; + const bindings = [...this.filterBindings, ...this.customBindings]; - let [rows, fields] = await this.db.query({ - sql: this.buildTotalCountSql(), - values: this.customBindings, - timeout: queryTimeout, - }); - const recordsTotal = rows[0].count; + let [rows, fields] = await this.db.query({ + sql: this.buildTotalCountSql(), + values: this.customBindings, + timeout: queryTimeout, + }); + const recordsTotal = rows[0].count; - [rows, fields] = await this.db.query({ - sql: this.buildFilteredCountSql(), - values: bindings, - timeout: queryTimeout, - }); - const recordsFiltered = rows[0].count; + [rows, fields] = await this.db.query({ + sql: this.buildFilteredCountSql(), + values: bindings, + timeout: queryTimeout, + }); + const recordsFiltered = rows[0].count; - [rows, fields] = await this.db.query({ - sql: this.sql(), - rowsAsArray: true, - values: bindings, - timeout: queryTimeout, - }); - rows = (rows as any[][]).map((row) => { - for (let i = 0; i < this.columnSettings.length; ++i) { - const col = this.columnSettings[i]; - if (col.formatter !== undefined) { - row[i] = col.formatter(row[i], row); - } - } - return row; - }); + [rows, fields] = await this.db.query({ + sql: this.sql(), + rowsAsArray: true, + values: bindings, + timeout: queryTimeout, + }); + rows = (rows as any[][]).map((row) => { + for (let i = 0; i < this.columnSettings.length; ++i) { + const col = this.columnSettings[i]; + if (col.formatter !== undefined) { + row[i] = col.formatter(row[i], row); + } + } + return row; + }); - return { - recordsTotal, - recordsFiltered, - draw: this.draw, - data: rows, - }; - } + return { + recordsTotal, + recordsFiltered, + draw: this.draw, + data: rows, + }; + } - public where(condition: string, binding?: string | number) { - this.wheres.push(condition); - if (binding !== undefined) { - this.customBindings.push(binding); - } - return this; - } + public where(condition: string, binding?: string | number) { + this.wheres.push(condition); + if (binding !== undefined) { + this.customBindings.push(binding); + } + return this; + } } diff --git a/src/armory/Utils.ts b/src/armory/Utils.ts index 05d158f..7f0c427 100644 --- a/src/armory/Utils.ts +++ b/src/armory/Utils.ts @@ -1,54 +1,54 @@ export enum EFaction { - Horde = 0, - Alliance = 1, + Horde = 0, + Alliance = 1, } export interface IEmblem { - icon: string; - iconColor: string; - border: string; - borderColor: string; - background: string; + icon: string; + iconColor: string; + border: string; + borderColor: string; + background: string; } export class Utils { - public static raceNames = { - 1: 'human', - 2: 'orc', - 3: 'dwarf', - 4: 'nightelf', - 5: 'scourge', - 6: 'tauren', - 7: 'gnome', - 8: 'troll', - 10: 'bloodelf', - 11: 'draenei', - }; - public static classNames = { - 1: 'warrior', - 2: 'paladin', - 3: 'hunter', - 4: 'rogue', - 5: 'priest', - 6: 'deathknight', - 7: 'shaman', - 8: 'mage', - 9: 'warlock', - 11: 'druid', - }; + public static raceNames = { + 1: "human", + 2: "orc", + 3: "dwarf", + 4: "nightelf", + 5: "scourge", + 6: "tauren", + 7: "gnome", + 8: "troll", + 10: "bloodelf", + 11: "draenei", + }; + public static classNames = { + 1: "warrior", + 2: "paladin", + 3: "hunter", + 4: "rogue", + 5: "priest", + 6: "deathknight", + 7: "shaman", + 8: "mage", + 9: "warlock", + 11: "druid", + }; - public static getFactionFromRaceId(race: number): EFaction { - return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde; - } + public static getFactionFromRaceId(race: number): EFaction { + return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde; + } - public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem { - const padLength = padWithZeroes ? 2 : 0; - return { - icon: obj.emblemStyle.toString().padStart(padLength, '0'), - iconColor: obj.emblemColor.toString().padStart(padLength, '0'), - border: obj.borderStyle.toString().padStart(padLength, '0'), - borderColor: obj.borderColor.toString().padStart(padLength, '0'), - background: obj.background.toString().padStart(padLength, '0'), - }; - } + public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem { + const padLength = padWithZeroes ? 2 : 0; + return { + icon: obj.emblemStyle.toString().padStart(padLength, "0"), + iconColor: obj.emblemColor.toString().padStart(padLength, "0"), + border: obj.borderStyle.toString().padStart(padLength, "0"), + borderColor: obj.borderColor.toString().padStart(padLength, "0"), + background: obj.background.toString().padStart(padLength, "0"), + }; + } } diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index 1a7d37d..243c0ac 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -1,316 +1,316 @@ -import * as express from 'express'; -import { RowDataPacket } from 'mysql2/promise'; +import * as express from "express"; +import { RowDataPacket } from "mysql2/promise"; -import { Utils } from '../Utils'; -import { Armory } from '../Armory'; -import { IRealmConfig } from '../Config'; -import { IAchievement } from '../data/DbcReader'; +import { Utils } from "../Utils"; +import { Armory } from "../Armory"; +import { IRealmConfig } from "../Config"; +import { IAchievement } from "../data/DbcReader"; interface ICharacterData { - guid: number; - name: string; - race: number; - class: number; - gender: number; - level: number; - skin: number; - face: number; - hairStyle: number; - hairColor: number; - facialStyle: number; - playerFlags: number; - online: number; - guild: string; + guid: number; + name: string; + race: number; + class: number; + gender: number; + level: number; + skin: number; + face: number; + hairStyle: number; + hairColor: number; + facialStyle: number; + playerFlags: number; + online: number; + guild: string; } interface IEquipmentData { - slot: number; - itemEntry: number; - flags: number; - enchantments: string; - randomPropertyId: number; - classId: number; - subclassId: number; + slot: number; + itemEntry: number; + flags: number; + enchantments: string; + randomPropertyId: number; + classId: number; + subclassId: number; } interface ICustomizationOption { - optionId: number; - choiceId: number; + optionId: number; + choiceId: number; } interface IMount { - creatureDisplayId: number; - spell: number; - icon: string; + creatureDisplayId: number; + spell: number; + icon: string; } const ItemClassGem = 3; const SpellMechanicMounted = 21; const RaceDisplayName = { - 1: 'Human', - 2: 'Orc', - 3: 'Dwarf', - 4: 'Night Elf', - 5: 'Undead', - 6: 'Tauren', - 7: 'Gnome', - 8: 'Troll', - 10: 'Blood Elf', - 11: 'Draenei', + 1: "Human", + 2: "Orc", + 3: "Dwarf", + 4: "Night Elf", + 5: "Undead", + 6: "Tauren", + 7: "Gnome", + 8: "Troll", + 10: "Blood Elf", + 11: "Draenei", }; const ClassDisplayName = { - 1: 'Warrior', - 2: 'Paladin', - 3: 'Hunter', - 4: 'Rogue', - 5: 'Priest', - 6: 'Death Knight', - 7: 'Shaman', - 8: 'Mage', - 9: 'Warlock', - 11: 'Druid', + 1: "Warrior", + 2: "Paladin", + 3: "Hunter", + 4: "Rogue", + 5: "Priest", + 6: "Death Knight", + 7: "Shaman", + 8: "Mage", + 9: "Warlock", + 11: "Druid", }; export class CharacterController { - private armory: Armory; - private itemInventoryTypes: { [key: number]: number }; - private itemIcons: { [key: number]: number }; - private gemItems: { [key: number]: boolean }; - private enchantSrcItems: { [key: number]: number }; - private itemSocketBonuses: { [key: number]: number }; - private mountSpells: number[]; - private mountBySpellId: { [key: number]: IMount }; - private achievementById: { [key: number]: IAchievement }; + private armory: Armory; + private itemInventoryTypes: { [key: number]: number }; + private itemIcons: { [key: number]: number }; + private gemItems: { [key: number]: boolean }; + private enchantSrcItems: { [key: number]: number }; + private itemSocketBonuses: { [key: number]: number }; + private mountSpells: number[]; + private mountBySpellId: { [key: number]: IMount }; + private achievementById: { [key: number]: IAchievement }; - public constructor(armory: Armory) { - this.armory = armory; - } + public constructor(armory: Armory) { + this.armory = armory; + } - public async load(): Promise { - 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); - if (retailItem !== undefined) { - this.itemInventoryTypes[item.id] = retailItem.inventoryType; - } - } + public async load(): Promise { + 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); + if (retailItem !== undefined) { + this.itemInventoryTypes[item.id] = retailItem.inventoryType; + } + } - this.itemIcons = {}; - const itemIconsByDisplayInfoId: { [key: number]: number } = {}; - for await (const row of this.armory.dbc.itemDisplayInfo()) { - itemIconsByDisplayInfoId[row.id] = row.inventoryIcon0; - } - for await (const item of this.armory.dbc.item()) { - const icon = itemIconsByDisplayInfoId[item.displayInfoId]; - if (icon !== undefined) { - this.itemIcons[item.id] = icon; - } - } + this.itemIcons = {}; + const itemIconsByDisplayInfoId: { [key: number]: number } = {}; + for await (const row of this.armory.dbc.itemDisplayInfo()) { + itemIconsByDisplayInfoId[row.id] = row.inventoryIcon0; + } + for await (const item of this.armory.dbc.item()) { + const icon = itemIconsByDisplayInfoId[item.displayInfoId]; + if (icon !== undefined) { + this.itemIcons[item.id] = icon; + } + } - this.gemItems = {}; - for await (const row of this.armory.dbc.item().filter((item) => item.classId === ItemClassGem)) { - this.gemItems[row.id] = true; - } + this.gemItems = {}; + for await (const row of this.armory.dbc.item().filter((item) => item.classId === ItemClassGem)) { + this.gemItems[row.id] = true; + } - this.enchantSrcItems = {}; - for await (const row of this.armory.dbc.spellItemEnchantment()) { - this.enchantSrcItems[row.id] = row.srcItemId; - } + this.enchantSrcItems = {}; + for await (const row of this.armory.dbc.spellItemEnchantment()) { + this.enchantSrcItems[row.id] = row.srcItemId; + } - this.itemSocketBonuses = {}; - let [rows, fields] = await this.armory.worldDb.query({ - sql: 'SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0', - timeout: this.armory.config.dbQueryTimeout, - }); - for (const row of rows as RowDataPacket[]) { - this.itemSocketBonuses[row.entry] = row.socketBonus; - } + this.itemSocketBonuses = {}; + let [rows, fields] = await this.armory.worldDb.query({ + sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0", + timeout: this.armory.config.dbQueryTimeout, + }); + for (const row of rows as RowDataPacket[]) { + this.itemSocketBonuses[row.entry] = row.socketBonus; + } - const mountSpells = await this.armory.dbc - .spell() - .filter((m) => m.mechanic === SpellMechanicMounted) - .toArray(); - 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); - if (mount !== undefined) { - const display = await this.armory.dbc.mountDisplay().find((d) => d.mountId === mount.id); - if (display !== undefined) { - this.mountBySpellId[spell.id] = { - creatureDisplayId: display.creatureDisplayInfoId, - spell: spell.id, - icon: this.processSpellIconTexture(icon?.textureFilename ?? ''), - }; - } - } - } + const mountSpells = await this.armory.dbc + .spell() + .filter((m) => m.mechanic === SpellMechanicMounted) + .toArray(); + 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); + if (mount !== undefined) { + const display = await this.armory.dbc.mountDisplay().find((d) => d.mountId === mount.id); + if (display !== undefined) { + this.mountBySpellId[spell.id] = { + creatureDisplayId: display.creatureDisplayInfoId, + spell: spell.id, + icon: this.processSpellIconTexture(icon?.textureFilename ?? ""), + }; + } + } + } - this.achievementById = {}; - for await (const achievement of this.armory.dbc.achievement()) { - this.achievementById[achievement.id] = achievement; - } - } + this.achievementById = {}; + for await (const achievement of this.armory.dbc.achievement()) { + this.achievementById[achievement.id] = achievement; + } + } - public async character(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const charName = req.params.name; + public async character(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const charName = req.params.name; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const charData = await this.getCharacterData(realm, charName); - if (charData === null) { - // Could not find character - return next(404); - } + const charData = await this.getCharacterData(realm, charName); + if (charData === null) { + // Could not find character + return next(404); + } - const equipmentData = await this.getEquipmentData(realmName, charData.guid); - const customization = this.getCustomizationOptions(charData); - 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 equipmentData = await this.getEquipmentData(realmName, charData.guid); + const customization = this.getCustomizationOptions(charData); + 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); - res.render('character.hbs', { - title: `Armory - ${charData.name}`, - ...this.makeSharedDataObject(realm, charData), - data: { - race: charData.race, - gender: charData.gender, - class: charData.class, - flags: charData.playerFlags, - characterModelItems: await this.getModelViewerItems(equipmentData, charData.class), - customizationOptions: customization, - equipment, - mounts, - }, - }); + res.render("character.hbs", { + title: `Armory - ${charData.name}`, + ...this.makeSharedDataObject(realm, charData), + data: { + race: charData.race, + gender: charData.gender, + class: charData.class, + flags: charData.playerFlags, + characterModelItems: await this.getModelViewerItems(equipmentData, charData.class), + customizationOptions: customization, + equipment, + mounts, + }, + }); - this.armory.gc(); - } + this.armory.gc(); + } - public async talents(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const charName = req.params.name; + public async talents(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const charName = req.params.name; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const charData = await this.getCharacterData(realm, charName); - if (charData === null) { - // Could not find character - return next(404); - } + const charData = await this.getCharacterData(realm, charName); + if (charData === null) { + // Could not find character + return next(404); + } - res.render('character-talents.hbs', { - title: `Armory - ${charData.name} - Talents`, - ...this.makeSharedDataObject(realm, charData), - data: { - talents: await this.getTalents(realm.name, charData.guid), - trees: await this.getTalentTrees(charData.class), - glyphs: await this.getGlyphs(realm.name, charData.guid), - }, - }); - } + res.render("character-talents.hbs", { + title: `Armory - ${charData.name} - Talents`, + ...this.makeSharedDataObject(realm, charData), + data: { + talents: await this.getTalents(realm.name, charData.guid), + trees: await this.getTalentTrees(charData.class), + glyphs: await this.getGlyphs(realm.name, charData.guid), + }, + }); + } - public async achievements(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const charName = req.params.name; + public async achievements(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const charName = req.params.name; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const charData = await this.getCharacterData(realm, charName); - if (charData === null) { - // Could not find character - return next(404); - } + const charData = await this.getCharacterData(realm, charName); + if (charData === null) { + // Could not find character + return next(404); + } - res.render('character-achievements.hbs', { - title: `Armory - ${charData.name} - Achievements`, - ...this.makeSharedDataObject(realm, charData), - }); - } + res.render("character-achievements.hbs", { + title: `Armory - ${charData.name} - Achievements`, + ...this.makeSharedDataObject(realm, charData), + }); + } - public async achievementsData(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const character = parseInt(req.params.character) || -1; + public async achievementsData(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const character = parseInt(req.params.character) || -1; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const charData = await this.getCharacterData(realm, character); - if (charData === null) { - // Could not find character - return next(404); - } + const charData = await this.getCharacterData(realm, character); + if (charData === null) { + // Could not find character + return next(404); + } - res.json({ - categories: await this.armory.dbc.achievementCategory().toArray(), - ...(await this.getAchievements(realm.name, charData)), - }); - } + res.json({ + categories: await this.armory.dbc.achievementCategory().toArray(), + ...(await this.getAchievements(realm.name, charData)), + }); + } - public async pvp(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const charName = req.params.name; + public async pvp(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const charName = req.params.name; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const charData = await this.getCharacterData(realm, charName); - if (charData === null) { - // Could not find character - return next(404); - } + const charData = await this.getCharacterData(realm, charName); + if (charData === null) { + // Could not find character + return next(404); + } - res.render('character-pvp.hbs', { - title: `Armory - ${charData.name} - PvP`, - ...this.makeSharedDataObject(realm, charData), - faction: Utils.getFactionFromRaceId(charData.race), - kills: await this.getPvpKills(realm.name, charData.guid), - arenaTeams: await this.getArenaTeams(realm.name, charData.guid), - }); - } + res.render("character-pvp.hbs", { + title: `Armory - ${charData.name} - PvP`, + ...this.makeSharedDataObject(realm, charData), + faction: Utils.getFactionFromRaceId(charData.race), + kills: await this.getPvpKills(realm.name, charData.guid), + arenaTeams: await this.getArenaTeams(realm.name, charData.guid), + }); + } - private makeSharedDataObject(realm: IRealmConfig, charData: ICharacterData) { - return { - realm: realm.name, - name: charData.name, - guid: charData.guid, - race: RaceDisplayName[charData.race], - class: ClassDisplayName[charData.class], - level: charData.level, - online: charData.online === 1, - guild: charData.guild, - }; - } + private makeSharedDataObject(realm: IRealmConfig, charData: ICharacterData) { + return { + realm: realm.name, + name: charData.name, + guid: charData.guid, + race: RaceDisplayName[charData.race], + class: ClassDisplayName[charData.class], + level: charData.level, + online: charData.online === 1, + guild: charData.guild, + }; + } - private async getCharacterData(realm: IRealmConfig, character: string | number): Promise { - const where = typeof character === 'string' ? 'LOWER(`characters`.`name`) = LOWER(?)' : '`characters`.`guid` = ?'; - const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({ - sql: ` + private async getCharacterData(realm: IRealmConfig, character: string | number): Promise { + const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?"; + const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({ + sql: ` 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\` @@ -320,698 +320,698 @@ export class CharacterController { ${where} 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, - }); + values: [character, this.armory.config.hideGameMasters ? 1 : 0], + timeout: this.armory.config.dbQueryTimeout, + }); - if ((rows as RowDataPacket[]).length === 0) { - return null; - } - return rows[0]; - } + if ((rows as RowDataPacket[]).length === 0) { + return null; + } + return rows[0]; + } - private async getEquipmentData(realm: string, charGuid: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + private async getEquipmentData(realm: string, charGuid: number): Promise { + 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 FROM character_inventory JOIN item_instance ON item_instance.guid = character_inventory.item 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, - }); + values: [charGuid], + timeout: this.armory.config.dbQueryTimeout, + }); - const data = rows as RowDataPacket[] as IEquipmentData[]; + const data = rows as RowDataPacket[] as IEquipmentData[]; - 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; - } + 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; + } - return data; - } + return data; + } - private async getMounts(realm: string, charGuid: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + private async getMounts(realm: string, charGuid: number): Promise { + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` SELECT spell FROM character_spell WHERE guid = ? AND spell IN (?) `, - values: [charGuid, this.mountSpells], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [charGuid, this.mountSpells], + 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): Promise { - if (charClass !== 3) { - // Keep ranged weapon only if the character is a hunter - equipmentData = equipmentData.filter((row) => row.slot !== 17); - } - 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) - ); + private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number): Promise { + if (charClass !== 3) { + // Keep ranged weapon only if the character is a hunter + equipmentData = equipmentData.filter((row) => row.slot !== 17); + } + 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) + ); - const items: number[][] = []; - for (const equipment of visibleEquipment) { - const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find((row) => row.itemId === equipment.itemEntry); - if (modifiedAppearance === undefined) { - continue; - } - const appearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId); - if (appearance === undefined) { - continue; - } + const items: number[][] = []; + for (const equipment of visibleEquipment) { + const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find((row) => row.itemId === equipment.itemEntry); + if (modifiedAppearance === undefined) { + continue; + } + const appearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId); + if (appearance === undefined) { + continue; + } - items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]); - } + items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]); + } - return items; - } + return items; + } - private parseEnchantmentsString(enchantments: string): number[] { - return enchantments - .trim() - .split(' ') - .map((enchant) => parseInt(enchant)) - .filter((enchant) => enchant !== 0); - } + private parseEnchantmentsString(enchantments: string): number[] { + return enchantments + .trim() + .split(" ") + .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]); - } + 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]); + } - 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, - ); - } + 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, + ); + } - 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); - if (option !== undefined) { - 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); - if (option !== undefined) { - 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); - if (option !== undefined) { - options.push({ optionId: option.Id, choiceId: choiceId }); - } - }; + 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); + if (option !== undefined) { + 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); + if (option !== undefined) { + 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); + if (option !== undefined) { + options.push({ optionId: option.Id, choiceId: choiceId }); + } + }; - const optionMapping = { - Face: charData.face, - 'Skin Color': charData.skin, - 'Hair Style': charData.hairStyle, - 'Hair Color': charData.hairColor, - }; - for (const optionName in optionMapping) { - setOptionByChoiceIndex(optionName, optionMapping[optionName]); - } + const optionMapping = { + Face: charData.face, + "Skin Color": charData.skin, + "Hair Style": charData.hairStyle, + "Hair Color": charData.hairColor, + }; + for (const optionName in optionMapping) { + setOptionByChoiceIndex(optionName, optionMapping[optionName]); + } - // Race-specific customization options - 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('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], - ); - } 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], - ); - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 4534 : 4535); - } - break; - case 3: // Dwarf - setOptionByChoiceName('Tattoo', 'None'); - 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], - ); - 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], - ); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 5559 : 5587); - } - break; - case 7: // Gnome - if (charData.gender === 0) { - setOptionByChoiceIndex('Mustache', charData.facialStyle > 1 ? charData.facialStyle - 1 : 0); - setOptionByChoiceIndex('Beard', charData.facialStyle < 7 ? charData.facialStyle : 0); - setOptionByChoiceIndex('Eyebrows', charData.facialStyle < 6 ? charData.facialStyle : 1); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } else { - setOptionByChoiceIndex('Earrings', charData.facialStyle); - setOptionByChoiceId('Earring Color', 8796); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 5629 : 5643); - } - break; - case 4: // Night Elf - setOptionByChoiceName('Vines', 'None'); - setOptionByChoiceIndex('Vine Color', 0); - 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('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]); - } else { - setOptionByChoiceName('Eyebrows', 'Long'); - setOptionByChoiceIndex('Markings', charData.facialStyle + 1); - setOptionByChoiceIndex('Markings Color', { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 3, 6: 6, 7: 7 }[charData.hairColor]); - } - setOptionByChoiceName('Blindfold', ''); - setOptionByChoiceName('Headdress', 'None'); - setOptionByChoiceName('Earrings', 'None'); - setOptionByChoiceName('Nose Ring', 'None'); - setOptionByChoiceName('Necklace', 'None'); - setOptionByChoiceName('Horns', 'None'); - setOptionByChoiceName('Tattoo', 'None'); - setOptionByChoiceName('Tattoo Color', 'None'); - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 7618 : 7634); - } else { - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 7610 : 7619); - } - break; - case 11: // Draenei - setOptionByChoiceName('Circlet', 'None'); - setOptionByChoiceId('Jewelry Color', charData.gender === 0 ? 8707 : 8646); - 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], - ); - } else { - setOptionByChoiceName( - 'Horns', - { 0: 'Sweeping', 1: 'Curled', 2: 'Curved', 3: 'Thick', 4: 'Wide', 5: 'Grand', 6: 'Short' }[charData.facialStyle], - ); - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 6977 : 6979); - } else { - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 6976 : 6978); - } - break; - case 2: // Orc - setOptionByChoiceName('Scars', 'None'); - setOptionByChoiceName('Grime', 'None'); - setOptionByChoiceName('Tattoo', 'None'); - 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('Earrings', 'None'); - setOptionByChoiceName('Nose Ring', 'None'); - setOptionByChoiceName('Tusks', 'Natural'); - setOptionByChoiceName('Upright', 'Hunched'); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } else { - setOptionByChoiceIndex('Earrings', { 0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 2, 6: 4 }[charData.facialStyle]); - setOptionByChoiceIndex('Nose Ring', { 0: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 0 }[charData.facialStyle]); - setOptionByChoiceName('Necklace', 'None'); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 9289 : 9313); - } - break; - 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], - ); - } 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], - ); - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 5344 : 5345); - } - break; - case 6: // Tauren - setOptionByChoiceIndex('Horn Style', charData.hairStyle); - setOptionByChoiceIndex('Horn Color', charData.hairColor); - setOptionByChoiceName('Foremane', 'Short'); - setOptionByChoiceName('Face Paint', 'None'); - setOptionByChoiceName('Headdress', 'None'); - setOptionByChoiceName('Necklace', 'None'); - setOptionByChoiceIndex('Jewelry Color', 0); - setOptionByChoiceName('Flower', 'None'); - 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], - ); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } else { - setOptionByChoiceIndex('Hair', charData.facialStyle); - setOptionByChoiceName('Earrings', 'None'); - setOptionByChoiceName('Nose Ring', 'None'); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 7281 : 7289); - } - break; - case 8: // Troll - setOptionByChoiceName('Body Paint', 'None'); - 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], - ); - setOptionByChoiceIndex('Face Paint Color', charData.hairColor + 1); - setOptionByChoiceName('Earrings', 'None'); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } else { - setOptionByChoiceIndex('Tusks', charData.facialStyle); - setOptionByChoiceName('Face Paint', 'None'); - setOptionByChoiceIndex('Face Paint Color', 0); - setOptionByChoiceName('Earrings', 'Hoops'); - setOptionByChoiceIndex('Eye Color', 0); // TODO - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 8451 : 8468); - } - break; - case 10: // Blood Elf - setOptionByChoiceName('Ears', 'Long'); - setOptionByChoiceName('Horns', 'None'); - setOptionByChoiceName('Blindfold', 'None'); - setOptionByChoiceName('Tattoo', 'None'); - setOptionByChoiceIndex('Tattoo Color', 0); - if (charData.gender === 0) { - setOptionByChoiceIndex('Facial Hair', charData.facialStyle); - } else { - setOptionByChoiceIndex('Earrings', charData.facialStyle); - setOptionByChoiceIndex('Jewelry Color', 0); - setOptionByChoiceName('Necklace', 'None'); - setOptionByChoiceName('Armbands', 'None'); - setOptionByChoiceName('Bracelets', 'None'); - } - if (charData.class === 6) { - // Death Knight - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 6586 : 6605); - } else { - setOptionByChoiceId('Eye Color', charData.gender === 0 ? 6570 : 6589); - } - break; - } - if ([4, 6].includes(charData.race)) { - // Races that can choose the druid class - setOptionByChoiceIndex('Bear Form', 0); - setOptionByChoiceIndex('Cat Form', 0); - setOptionByChoiceIndex('Aquatic Form', 0); - setOptionByChoiceIndex('Travel Form', 0); - setOptionByChoiceIndex('Flight Form', 0); - setOptionByChoiceIndex('Moonkin Form', 0); - } + // Race-specific customization options + 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("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], + ); + } 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], + ); + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 4534 : 4535); + } + break; + case 3: // Dwarf + setOptionByChoiceName("Tattoo", "None"); + 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], + ); + 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], + ); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 5559 : 5587); + } + break; + case 7: // Gnome + if (charData.gender === 0) { + setOptionByChoiceIndex("Mustache", charData.facialStyle > 1 ? charData.facialStyle - 1 : 0); + setOptionByChoiceIndex("Beard", charData.facialStyle < 7 ? charData.facialStyle : 0); + setOptionByChoiceIndex("Eyebrows", charData.facialStyle < 6 ? charData.facialStyle : 1); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } else { + setOptionByChoiceIndex("Earrings", charData.facialStyle); + setOptionByChoiceId("Earring Color", 8796); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 5629 : 5643); + } + break; + case 4: // Night Elf + setOptionByChoiceName("Vines", "None"); + setOptionByChoiceIndex("Vine Color", 0); + 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("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]); + } else { + setOptionByChoiceName("Eyebrows", "Long"); + setOptionByChoiceIndex("Markings", charData.facialStyle + 1); + setOptionByChoiceIndex("Markings Color", { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 3, 6: 6, 7: 7 }[charData.hairColor]); + } + setOptionByChoiceName("Blindfold", ""); + setOptionByChoiceName("Headdress", "None"); + setOptionByChoiceName("Earrings", "None"); + setOptionByChoiceName("Nose Ring", "None"); + setOptionByChoiceName("Necklace", "None"); + setOptionByChoiceName("Horns", "None"); + setOptionByChoiceName("Tattoo", "None"); + setOptionByChoiceName("Tattoo Color", "None"); + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 7618 : 7634); + } else { + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 7610 : 7619); + } + break; + case 11: // Draenei + setOptionByChoiceName("Circlet", "None"); + setOptionByChoiceId("Jewelry Color", charData.gender === 0 ? 8707 : 8646); + 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], + ); + } else { + setOptionByChoiceName( + "Horns", + { 0: "Sweeping", 1: "Curled", 2: "Curved", 3: "Thick", 4: "Wide", 5: "Grand", 6: "Short" }[charData.facialStyle], + ); + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 6977 : 6979); + } else { + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 6976 : 6978); + } + break; + case 2: // Orc + setOptionByChoiceName("Scars", "None"); + setOptionByChoiceName("Grime", "None"); + setOptionByChoiceName("Tattoo", "None"); + 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("Earrings", "None"); + setOptionByChoiceName("Nose Ring", "None"); + setOptionByChoiceName("Tusks", "Natural"); + setOptionByChoiceName("Upright", "Hunched"); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } else { + setOptionByChoiceIndex("Earrings", { 0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 2, 6: 4 }[charData.facialStyle]); + setOptionByChoiceIndex("Nose Ring", { 0: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 0 }[charData.facialStyle]); + setOptionByChoiceName("Necklace", "None"); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 9289 : 9313); + } + break; + 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], + ); + } 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], + ); + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 5344 : 5345); + } + break; + case 6: // Tauren + setOptionByChoiceIndex("Horn Style", charData.hairStyle); + setOptionByChoiceIndex("Horn Color", charData.hairColor); + setOptionByChoiceName("Foremane", "Short"); + setOptionByChoiceName("Face Paint", "None"); + setOptionByChoiceName("Headdress", "None"); + setOptionByChoiceName("Necklace", "None"); + setOptionByChoiceIndex("Jewelry Color", 0); + setOptionByChoiceName("Flower", "None"); + 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], + ); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } else { + setOptionByChoiceIndex("Hair", charData.facialStyle); + setOptionByChoiceName("Earrings", "None"); + setOptionByChoiceName("Nose Ring", "None"); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 7281 : 7289); + } + break; + case 8: // Troll + setOptionByChoiceName("Body Paint", "None"); + 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], + ); + setOptionByChoiceIndex("Face Paint Color", charData.hairColor + 1); + setOptionByChoiceName("Earrings", "None"); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } else { + setOptionByChoiceIndex("Tusks", charData.facialStyle); + setOptionByChoiceName("Face Paint", "None"); + setOptionByChoiceIndex("Face Paint Color", 0); + setOptionByChoiceName("Earrings", "Hoops"); + setOptionByChoiceIndex("Eye Color", 0); // TODO + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 8451 : 8468); + } + break; + case 10: // Blood Elf + setOptionByChoiceName("Ears", "Long"); + setOptionByChoiceName("Horns", "None"); + setOptionByChoiceName("Blindfold", "None"); + setOptionByChoiceName("Tattoo", "None"); + setOptionByChoiceIndex("Tattoo Color", 0); + if (charData.gender === 0) { + setOptionByChoiceIndex("Facial Hair", charData.facialStyle); + } else { + setOptionByChoiceIndex("Earrings", charData.facialStyle); + setOptionByChoiceIndex("Jewelry Color", 0); + setOptionByChoiceName("Necklace", "None"); + setOptionByChoiceName("Armbands", "None"); + setOptionByChoiceName("Bracelets", "None"); + } + if (charData.class === 6) { + // Death Knight + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 6586 : 6605); + } else { + setOptionByChoiceId("Eye Color", charData.gender === 0 ? 6570 : 6589); + } + break; + } + if ([4, 6].includes(charData.race)) { + // Races that can choose the druid class + setOptionByChoiceIndex("Bear Form", 0); + setOptionByChoiceIndex("Cat Form", 0); + setOptionByChoiceIndex("Aquatic Form", 0); + setOptionByChoiceIndex("Travel Form", 0); + setOptionByChoiceIndex("Flight Form", 0); + setOptionByChoiceIndex("Moonkin Form", 0); + } - return options; - } + return options; + } - private async getTalents(realm: string, character: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + private async getTalents(realm: string, character: number): Promise { + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` SELECT spell, specMask FROM character_talent WHERE guid = ? `, - values: [character], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [character], + timeout: this.armory.config.dbQueryTimeout, + }); - const talents: number[][] = [[], []]; - for (const row of rows as RowDataPacket[]) { - if (row.specMask === 1 || row.specMask === 3) { - talents[0].push(row.spell); - } - if (row.specMask === 2 || row.specMask === 3) { - talents[1].push(row.spell); - } - } + const talents: number[][] = [[], []]; + for (const row of rows as RowDataPacket[]) { + if (row.specMask === 1 || row.specMask === 3) { + talents[0].push(row.spell); + } + if (row.specMask === 2 || row.specMask === 3) { + talents[1].push(row.spell); + } + } - return talents; - } + return talents; + } - 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 ?? '') }; - }) - .toArray(); - return { - name: tab.nameLang0, - icon: this.processSpellIconTexture(icon.textureFilename), - spells: await Promise.all(spells), - }; - }) - .toArray(); - return await Promise.all(items); - } + 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 ?? "") }; + }) + .toArray(); + return { + name: tab.nameLang0, + icon: this.processSpellIconTexture(icon.textureFilename), + spells: await Promise.all(spells), + }; + }) + .toArray(); + return await Promise.all(items); + } - private processSpellIconTexture(texturePath: string): string { - return texturePath.toLowerCase().replace('interface\\icons\\', '').replace('interface\\spellbook\\', '').replace(/\.$/, ''); - } + private processSpellIconTexture(texturePath: string): string { + return texturePath.toLowerCase().replace("interface\\icons\\", "").replace("interface\\spellbook\\", "").replace(/\.$/, ""); + } - private async getGlyphs(realm: string, character: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + private async getGlyphs(realm: string, character: number): Promise { + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 FROM character_glyphs WHERE guid = ? `, - values: [character], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [character], + timeout: this.armory.config.dbQueryTimeout, + }); - 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); - for (const glyphId of glyphIds) { - const glyph = await this.armory.dbc.glyphProperties().find((g) => g.id === glyphId); - if (glyph === undefined) { - continue; - } - glyphs[row.talentGroup].push(glyph.spellId); - } - } + 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); + for (const glyphId of glyphIds) { + const glyph = await this.armory.dbc.glyphProperties().find((g) => g.id === glyphId); + if (glyph === undefined) { + continue; + } + glyphs[row.talentGroup].push(glyph.spellId); + } + } - return glyphs; - } + return glyphs; + } - 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); - return { - id: ach.id, - category: ach.category, - title: ach.titleLang0, - description: ach.descriptionLang0, - points: ach.points, - icon: this.processSpellIconTexture(icon?.textureFilename ?? ''), - }; - }) - .toArray(); - const achievements = await Promise.all(promises); + 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); + return { + id: ach.id, + category: ach.category, + title: ach.titleLang0, + description: ach.descriptionLang0, + points: ach.points, + icon: this.processSpellIconTexture(icon?.textureFilename ?? ""), + }; + }) + .toArray(); + const achievements = await Promise.all(promises); - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + const [rows, fields] = await this.armory.getCharactersDb(realm).query({ + sql: ` SELECT achievement, date FROM character_achievement WHERE guid = ? `, - values: [charData.guid], - timeout: this.armory.config.dbQueryTimeout, - }); - const earned = {}; - for (const row of rows as RowDataPacket[]) { - earned[row.achievement] = { - date: row.date, - }; - } + values: [charData.guid], + timeout: this.armory.config.dbQueryTimeout, + }); + const earned = {}; + for (const row of rows as RowDataPacket[]) { + earned[row.achievement] = { + date: row.date, + }; + } - return { - achievements, - earned, - }; - } + return { + achievements, + earned, + }; + } - private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number; today: number; yesterday: number }> { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + 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 WHERE guid = ? `, - values: [charGuid], - timeout: this.armory.config.dbQueryTimeout, - }); - const row = rows[0]; + values: [charGuid], + timeout: this.armory.config.dbQueryTimeout, + }); + const row = rows[0]; - return { - total: row.totalKills, - today: row.todayKills, - yesterday: row.yesterdayKills, - }; - } + return { + total: row.totalKills, + today: row.todayKills, + yesterday: row.yesterdayKills, + }; + } - private async getArenaTeams(realm: string, charGuid: number): Promise { - const [rows, fields] = await this.armory.getCharactersDb(realm).query({ - sql: ` + private async getArenaTeams(realm: string, charGuid: number): Promise { + 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, arena_team.backgroundColor AS background, arena_team.emblemStyle, arena_team.emblemColor, arena_team.borderStyle, arena_team.borderColor @@ -1020,13 +1020,13 @@ export class CharacterController { WHERE guid = ? ORDER BY arena_team.type ASC `, - values: [charGuid], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [charGuid], + timeout: this.armory.config.dbQueryTimeout, + }); - return (rows as RowDataPacket[]).map((row) => { - row.emblem = Utils.makeEmblemObject(row, false); - return row; - }); - } + return (rows as RowDataPacket[]).map((row) => { + row.emblem = Utils.makeEmblemObject(row, false); + return row; + }); + } } diff --git a/src/armory/controllers/GuildController.ts b/src/armory/controllers/GuildController.ts index 3fda8ca..6296ab5 100644 --- a/src/armory/controllers/GuildController.ts +++ b/src/armory/controllers/GuildController.ts @@ -1,188 +1,188 @@ -import * as express from 'express'; -import { encode } from 'html-entities'; -import { RowDataPacket } from 'mysql2/promise'; +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 } from "../Utils"; +import { Armory } from "../Armory"; +import { IRealmConfig } from "../Config"; +import { DataTablesSsp } from "../DataTablesSsp"; interface IGuildRank { - id: number; - name: string; + id: number; + name: string; } export class GuildController { - private armory: Armory; + private armory: Armory; - public constructor(armory: Armory) { - this.armory = armory; - } + public constructor(armory: Armory) { + this.armory = armory; + } - public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const guildName = req.params.name; + public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const guildName = req.params.name; - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - const guildData = await this.getGuildData(realm, guildName); - if (guildData === null) { - // Could not find guild - return next(404); - } + const guildData = await this.getGuildData(realm, guildName); + if (guildData === null) { + // Could not find guild + return next(404); + } - res.render('guild.hbs', { - title: `Armory - ${guildName}`, - realm: realm.name, - ...guildData, - }); - } + res.render("guild.hbs", { + title: `Armory - ${guildName}`, + realm: realm.name, + ...guildData, + }); + } - public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - const realmName = req.params.realm; - const guildId = parseInt(req.params.guild); + public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + const realmName = req.params.realm; + const guildId = parseInt(req.params.guild); - const realm = this.armory.getRealm(realmName); - if (realm === undefined) { - // Could not find realm - return next(404); - } + const realm = this.armory.getRealm(realmName); + if (realm === undefined) { + // Could not find realm + return next(404); + } - if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) { - // Could not find guild - return next(404); - } + if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) { + // Could not find guild + return next(404); + } - const db = this.armory.getCharactersDb(realm.name); - const charSet = await this.armory.getDatabaseCharset(realm.name); + const db = this.armory.getCharactersDb(realm.name); + const charSet = await this.armory.getDatabaseCharset(realm.name); - let ssp = new DataTablesSsp(req.query, db, 'guild_member', 'guid', [ - { name: 'name', table: 'characters', collation: `${charSet}_general_ci` }, - { name: 'rank' }, - { name: 'level', table: 'characters' }, - { 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 }, - ]); - ssp.joins = [{ table1: 'guild_member', column1: 'guid', table2: 'characters', column2: 'guid', kind: 'LEFT' }]; - ssp.extraDataColumns = ['`characters`.`gender`']; + let ssp = new DataTablesSsp(req.query, db, "guild_member", "guid", [ + { name: "name", table: "characters", collation: `${charSet}_general_ci` }, + { name: "rank" }, + { name: "level", table: "characters" }, + { 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 }, + ]); + 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', - }); - ssp = ssp.where( - `\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`, - ); - } + if (this.armory.config.hideGameMasters) { + 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 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); - (result as any).ranks = {}; - for (const rank of ranks) { - (result as any).ranks[rank.id] = encode(rank.name); - } + const ranks = await this.getGuildRanks(realm, guildId); + (result as any).ranks = {}; + for (const rank of ranks) { + (result as any).ranks[rank.id] = encode(rank.name); + } - res.json(result); - } + res.json(result); + } - private async getGuildData(realm: IRealmConfig, name: string): Promise { - const db = this.armory.getCharactersDb(realm.name); - let [rows, fields] = await db.query({ - sql: ` + private async getGuildData(realm: IRealmConfig, name: string): Promise { + const db = this.armory.getCharactersDb(realm.name); + 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 = ? `, - values: [name], - timeout: this.armory.config.dbQueryTimeout, - }); - if ((rows as RowDataPacket[]).length === 0) { - return null; - } - const guild = rows[0]; + values: [name], + timeout: this.armory.config.dbQueryTimeout, + }); + if ((rows as RowDataPacket[]).length === 0) { + return null; + } + const guild = rows[0]; - [rows, fields] = await db.query({ - sql: ` + [rows, fields] = await db.query({ + sql: ` SELECT name, race FROM characters WHERE guid = ? `, - values: [guild.leaderguid], - timeout: this.armory.config.dbQueryTimeout, - }); - const leader = rows[0]; + values: [guild.leaderguid], + timeout: this.armory.config.dbQueryTimeout, + }); + const leader = rows[0]; - [rows, fields] = await db.query({ - sql: ` + [rows, fields] = await db.query({ + sql: ` SELECT COUNT(guid) AS \`count\` FROM guild_member WHERE guildid = ? `, - values: [guild.guildid], - timeout: this.armory.config.dbQueryTimeout, - }); - const membersCount = rows[0].count; + values: [guild.guildid], + timeout: this.armory.config.dbQueryTimeout, + }); + const membersCount = rows[0].count; - return { - id: guild.guildid, - name: guild.name, - leader: leader.name, - faction: Utils.getFactionFromRaceId(leader.race), - emblem: Utils.makeEmblemObject(guild), - membersCount, - }; - } + return { + id: guild.guildid, + name: guild.name, + leader: leader.name, + faction: Utils.getFactionFromRaceId(leader.race), + emblem: Utils.makeEmblemObject(guild), + membersCount, + }; + } - private async getGuildId(realm: IRealmConfig, name: string): Promise { - const db = this.armory.getCharactersDb(realm.name); - const [rows, fields] = await db.query({ - sql: ` + private async getGuildId(realm: IRealmConfig, name: string): Promise { + const db = this.armory.getCharactersDb(realm.name); + const [rows, fields] = await db.query({ + sql: ` SELECT guildid FROM guild WHERE name = ? `, - values: [name], - timeout: this.armory.config.dbQueryTimeout, - }); - if ((rows as RowDataPacket[]).length === 0) { - return null; - } + values: [name], + timeout: this.armory.config.dbQueryTimeout, + }); + if ((rows as RowDataPacket[]).length === 0) { + return null; + } - return rows[0].guildid; - } + return rows[0].guildid; + } - private async guildExists(realm: IRealmConfig, id: number): Promise { - const db = this.armory.getCharactersDb(realm.name); - const [rows, fields] = await db.query({ - sql: ` + private async guildExists(realm: IRealmConfig, id: number): Promise { + const db = this.armory.getCharactersDb(realm.name); + const [rows, fields] = await db.query({ + sql: ` SELECT guildid FROM guild WHERE guildid `, - values: [id], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [id], + timeout: this.armory.config.dbQueryTimeout, + }); - return (rows as RowDataPacket[]).length !== 0; - } + return (rows as RowDataPacket[]).length !== 0; + } - private async getGuildRanks(realm: IRealmConfig, id: number): Promise { - const db = this.armory.getCharactersDb(realm.name); - const [rows, fields] = await db.query({ - sql: ` + private async getGuildRanks(realm: IRealmConfig, id: number): Promise { + const db = this.armory.getCharactersDb(realm.name); + const [rows, fields] = await db.query({ + sql: ` SELECT rid AS id, rname AS name FROM guild_rank WHERE guildid = ? `, - values: [id], - timeout: this.armory.config.dbQueryTimeout, - }); + values: [id], + timeout: this.armory.config.dbQueryTimeout, + }); - return rows as IGuildRank[]; - } + return rows as IGuildRank[]; + } } diff --git a/src/armory/controllers/IndexController.ts b/src/armory/controllers/IndexController.ts index 01ffe32..954ad01 100644 --- a/src/armory/controllers/IndexController.ts +++ b/src/armory/controllers/IndexController.ts @@ -1,64 +1,64 @@ -import * as express from 'express'; +import * as express from "express"; -import { Utils } from '../Utils'; -import { Armory } from '../Armory'; -import { DataTablesSsp } from '../DataTablesSsp'; +import { Utils } from "../Utils"; +import { Armory } from "../Armory"; +import { DataTablesSsp } from "../DataTablesSsp"; export class IndexController { - private armory: Armory; + private armory: Armory; - public constructor(armory: Armory) { - this.armory = armory; - } + public constructor(armory: Armory) { + this.armory = armory; + } - public async index(req: express.Request, res: express.Response): Promise { - res.render('index.hbs', { - title: 'Armory', - realms: this.armory.config.realms.map((r) => r.name), - }); - } + public async index(req: express.Request, res: express.Response): Promise { + res.render("index.hbs", { + title: "Armory", + realms: this.armory.config.realms.map((r) => r.name), + }); + } - public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise { - 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); - } + public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + 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 db = this.armory.getCharactersDb(realm.name); - const charSet = await this.armory.getDatabaseCharset(realm.name); + const db = this.armory.getCharactersDb(realm.name); + const charSet = await this.armory.getDatabaseCharset(realm.name); - let ssp = new DataTablesSsp(req.query, db, 'characters', 'guid', [ - { name: 'name', collation: `${charSet}_general_ci` }, - { table: 'guild', name: 'name' }, - { name: 'level' }, - { 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 }, - ]); - ssp.joins = [ - { table1: 'characters', column1: 'guid', table2: 'guild_member', column2: 'guid', kind: 'LEFT' }, - { table1: 'guild_member', column1: 'guildid', table2: 'guild', column2: 'guildid', kind: 'LEFT' }, - ]; - ssp.extraDataColumns = ['`characters`.`gender`']; + let ssp = new DataTablesSsp(req.query, db, "characters", "guid", [ + { name: "name", collation: `${charSet}_general_ci` }, + { table: "guild", name: "name" }, + { name: "level" }, + { 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 }, + ]); + ssp.joins = [ + { table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" }, + { table1: "guild_member", column1: "guildid", table2: "guild", column2: "guildid", 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', - }); - ssp = ssp.where( - `\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`, - ); - } + if (this.armory.config.hideGameMasters) { + 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 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); + (result as any).realm = realm.name; - res.json(result); - } + res.json(result); + } } diff --git a/src/armory/main.ts b/src/armory/main.ts index d4e88ba..cc8312e 100644 --- a/src/armory/main.ts +++ b/src/armory/main.ts @@ -1,11 +1,11 @@ -import 'dotenv/config'; -import { Armory } from './Armory'; +import "dotenv/config"; +import { Armory } from "./Armory"; async function main(): Promise { - require('source-map-support').install(); + require("source-map-support").install(); - const armory = new Armory(); - await armory.start(); + const armory = new Armory(); + await armory.start(); } main(); diff --git a/src/index.d.ts b/src/index.d.ts index 5f9d2e4..b862ee6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,5 +1,5 @@ declare module Express { - export interface Request { - id: string; - } + export interface Request { + id: string; + } } diff --git a/src/tools/fetchdata.ts b/src/tools/fetchdata.ts index e0c6181..10335c5 100644 --- a/src/tools/fetchdata.ts +++ b/src/tools/fetchdata.ts @@ -1,79 +1,79 @@ -import * as fs from 'fs'; +import * as fs from "fs"; const fsp = fs.promises; -import * as path from 'path'; +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-promise'; -import { Response } from 'node-fetch'; -import * as prettyMs from 'pretty-ms'; -import * as cliProgress from 'cli-progress'; -import promisepool = require('@supercharge/promise-pool'); +import * as pako from "pako"; +import fetch from "node-fetch"; +import * as mkdirp from "mkdirp"; +import * as glob from "glob-promise"; +import { Response } from "node-fetch"; +import * as prettyMs from "pretty-ms"; +import * as cliProgress from "cli-progress"; +import promisepool = require("@supercharge/promise-pool"); -import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from '../armory/data/DbcReader'; +import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/DbcReader"; -require('source-map-support').install(); +require("source-map-support").install(); -const baseUrl = 'https://wow.zamimg.com/modelviewer/live'; +const baseUrl = "https://wow.zamimg.com/modelviewer/live"; class Stopwatch { - private startTime: number; + private startTime: number; - public constructor() { - this.start(); - } + public constructor() { + this.start(); + } - public start(): void { - this.startTime = Date.now(); - } + public start(): void { + this.startTime = Date.now(); + } - public stop(text?: string): void { - const dt = Date.now() - this.startTime; - const txt = text ?? 'Done in {time}'; - console.log(txt.replace('{time}', prettyMs(dt))); - } + public stop(text?: string): void { + const dt = Date.now() - this.startTime; + const txt = text ?? "Done in {time}"; + console.log(txt.replace("{time}", prettyMs(dt))); + } } class Progress { - private bar: cliProgress.SingleBar; - private stopwatch: Stopwatch; + private bar: cliProgress.SingleBar; + private stopwatch: Stopwatch; - public constructor(text: string, operations: number) { - this.bar = this.createProgressBar(text, operations); - this.stopwatch = new Stopwatch(); - } + public constructor(text: string, operations: number) { + this.bar = this.createProgressBar(text, operations); + this.stopwatch = new Stopwatch(); + } - public increment(step?: number): void { - this.bar.increment(step); - } + public increment(step?: number): void { + this.bar.increment(step); + } - public stop(): void { - this.bar.stop(); - this.stopwatch.stop(); - } + public stop(): void { + this.bar.stop(); + this.stopwatch.stop(); + } - private createProgressBar(text: string, total: number): cliProgress.SingleBar { - const progress = new cliProgress.SingleBar( - { - format: `${text} {bar} {percentage}% ({value} / {total})`, - }, - cliProgress.Presets.shades_classic, - ); - progress.start(total, 0); - return progress; - } + private createProgressBar(text: string, total: number): cliProgress.SingleBar { + const progress = new cliProgress.SingleBar( + { + format: `${text} {bar} {percentage}% ({value} / {total})`, + }, + cliProgress.Presets.shades_classic, + ); + progress.start(total, 0); + return progress; + } } class HttpRequestError extends Error { - public response: Response; + public response: Response; - public constructor(response: Response) { - super(`Could not download ${response.url} (${response.status})`); - this.name = 'HttpRequestError'; - this.response = response; - Object.setPrototypeOf(this, HttpRequestError.prototype); - } + public constructor(response: Response) { + super(`Could not download ${response.url} (${response.status})`); + this.name = "HttpRequestError"; + this.response = response; + Object.setPrototypeOf(this, HttpRequestError.prototype); + } } let dbc: DbcManager; @@ -93,339 +93,339 @@ const texturesDownloadQueue = new Set(); const bonesDownloadQueue = new Set(); async function download(dir: string, file: string): Promise { - const dataDir = path.join(process.cwd(), 'data'); - const fullPath = `${dir}/${file}`; + const dataDir = path.join(process.cwd(), "data"); + const fullPath = `${dir}/${file}`; - try { - 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(); - } - } catch (err) { - throw err; - } + return fileStream.path.toString(); + } + } catch (err) { + throw err; + } } function queueTexturesAndModels(item: any): void { - if (item.TextureFiles !== null) { - for (const key in item.TextureFiles) { - for (const file of item.TextureFiles[key]) { - if (file.FileDataId !== 0) { - texturesDownloadQueue.add(file.FileDataId); - } - } - } - } + if (item.TextureFiles !== null) { + 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 key in item.ModelFiles) { - for (const file of item.ModelFiles[key]) { - if (file.FileDataId !== 0) { - modelsDownloadQueue.add(file.FileDataId); - } - } - } - } + if (item.ModelFiles !== null) { + for (const key in item.ModelFiles) { + for (const file of item.ModelFiles[key]) { + if (file.FileDataId !== 0) { + modelsDownloadQueue.add(file.FileDataId); + } + } + } + } - if (typeof item.Model === 'number' && item.Model !== 0) { - modelsDownloadQueue.add(item.Model); - } + if (typeof item.Model === "number" && item.Model !== 0) { + modelsDownloadQueue.add(item.Model); + } - if (item.Textures !== null) { - for (const key in item.Textures) { - if (item.Textures[key] !== 0) { - texturesDownloadQueue.add(item.Textures[key]); - } - } - } + if (item.Textures !== null) { + for (const key in item.Textures) { + if (item.Textures[key] !== 0) { + texturesDownloadQueue.add(item.Textures[key]); + } + } + } - if (item.Textures2 !== null) { - for (const key in item.Textures2) { - if (item.Textures2[key] !== 0) { - texturesDownloadQueue.add(item.Textures2[key]); - } - } - } + if (item.Textures2 !== null) { + for (const key in item.Textures2) { + if (item.Textures2[key] !== 0) { + texturesDownloadQueue.add(item.Textures2[key]); + } + } + } } async function downloadRaces(): Promise { - 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 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); + const progress = new Progress("Downloading races data...", raceGenderCombo.length); - await promisepool.PromisePool.for(raceGenderCombo) - .withConcurrency(4) - .process(async (race) => { - const characterJson = await download('meta/character', `${race}.json`); - modelsDownloadQueue.add(characterJson.Model); + await promisepool.PromisePool.for(raceGenderCombo) + .withConcurrency(4) + .process(async (race) => { + const characterJson = await download("meta/character", `${race}.json`); + modelsDownloadQueue.add(characterJson.Model); - 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 - ) { - modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID); - } - if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === 'number' && element.BoneSet.BoneFileDataID !== 0) { - bonesDownloadQueue.add(element.BoneSet.BoneFileDataID); - } - } - } - } + 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 + ) { + modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID); + } + if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === "number" && element.BoneSet.BoneFileDataID !== 0) { + bonesDownloadQueue.add(element.BoneSet.BoneFileDataID); + } + } + } + } - const textureFiles = Object.keys(customizationJson.TextureFiles) - .map((key) => customizationJson.TextureFiles[key]) - .flat(); - for (const file of textureFiles) { - texturesDownloadQueue.add(file.FileDataId); - } + const textureFiles = Object.keys(customizationJson.TextureFiles) + .map((key) => customizationJson.TextureFiles[key]) + .flat(); + for (const file of textureFiles) { + texturesDownloadQueue.add(file.FileDataId); + } - progress.increment(); - }); + progress.increment(); + }); - progress.stop(); + progress.stop(); } async function downloadArmors(): Promise { - 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); + const progress = new Progress("Downloading armor data...", rows.length); - await promisepool.PromisePool.for(rows) - .withConcurrency(50) - .process(async (row) => { - const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; - if (modifiedAppearance === undefined) { - progress.increment(); - return; - } - const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; - const metaPath = [invTypeShield, invTypeOffHand].includes(row.inventoryType) ? 'item' : `armor/${row.inventoryType}`; - try { - const itemJson = await download(`meta/${metaPath}`, `${appearance.itemDisplayInfoId}.json`); - queueTexturesAndModels(itemJson); - progress.increment(); - } catch (err) { - if (err instanceof HttpRequestError && err.response.status === 404) { - progress.increment(); - return; - } - throw err; - } - }); + await promisepool.PromisePool.for(rows) + .withConcurrency(50) + .process(async (row) => { + const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; + if (modifiedAppearance === undefined) { + progress.increment(); + return; + } + const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; + const metaPath = [invTypeShield, invTypeOffHand].includes(row.inventoryType) ? "item" : `armor/${row.inventoryType}`; + try { + const itemJson = await download(`meta/${metaPath}`, `${appearance.itemDisplayInfoId}.json`); + queueTexturesAndModels(itemJson); + progress.increment(); + } catch (err) { + if (err instanceof HttpRequestError && err.response.status === 404) { + progress.increment(); + return; + } + throw err; + } + }); - progress.stop(); + progress.stop(); } async function downloadWeapons(): Promise { - 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); + const progress = new Progress("Downloading weapon data...", rows.length); - await promisepool.PromisePool.for(rows) - .withConcurrency(50) - .process(async (row) => { - const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; - if (modifiedAppearance === undefined) { - progress.increment(); - return; - } - const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; - try { - const itemJson = await download(`meta/item`, `${appearance.itemDisplayInfoId}.json`); - queueTexturesAndModels(itemJson); - progress.increment(); - } catch (err) { - if (err instanceof HttpRequestError && err.response.status === 404) { - progress.increment(); - return; - } - throw err; - } - }); + await promisepool.PromisePool.for(rows) + .withConcurrency(50) + .process(async (row) => { + const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; + if (modifiedAppearance === undefined) { + progress.increment(); + return; + } + const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; + try { + const itemJson = await download(`meta/item`, `${appearance.itemDisplayInfoId}.json`); + queueTexturesAndModels(itemJson); + progress.increment(); + } catch (err) { + if (err instanceof HttpRequestError && err.response.status === 404) { + progress.increment(); + return; + } + throw err; + } + }); - progress.stop(); + progress.stop(); } async function readDbcData(): Promise { - console.log('Reading DBC data...'); + console.log("Reading DBC data..."); - dbc = new DbcManager(); - await dbc.loadAllFiles(); + dbc = new DbcManager(); + await dbc.loadAllFiles(); - dbcItemAppearanceById = {}; - for await (const row of dbc.itemAppearance()) { - dbcItemAppearanceById[row.id] = row; - } + dbcItemAppearanceById = {}; + for await (const row of dbc.itemAppearance()) { + dbcItemAppearanceById[row.id] = row; + } - dbcItemModifiedAppearanceByItemId = {}; - for await (const row of dbc.itemModifiedAppearance()) { - dbcItemModifiedAppearanceByItemId[row.itemId] = row; - } + dbcItemModifiedAppearanceByItemId = {}; + for await (const row of dbc.itemModifiedAppearance()) { + dbcItemModifiedAppearanceByItemId[row.itemId] = row; + } - dbcMountBySourceSpellId = {}; - for await (const row of dbc.mount()) { - dbcMountBySourceSpellId[row.sourceSpellId] = row; - } + dbcMountBySourceSpellId = {}; + for await (const row of dbc.mount()) { + dbcMountBySourceSpellId[row.sourceSpellId] = row; + } - dbcMountDisplayByMountId = {}; - for await (const row of dbc.mountDisplay()) { - if (!(row.mountId in dbcMountDisplayByMountId)) { - dbcMountDisplayByMountId[row.mountId] = row; - } - } + dbcMountDisplayByMountId = {}; + for await (const row of dbc.mountDisplay()) { + if (!(row.mountId in dbcMountDisplayByMountId)) { + dbcMountDisplayByMountId[row.mountId] = row; + } + } } async function downloadMounts(): Promise { - const mountSpells = await dbc - .spell() - .filter((spell) => spell.mechanic === spellMechanicMounted) - .toArray(); - const progress = new Progress('Downloading mount data...', mountSpells.length); + 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) - .withConcurrency(50) - .process(async (spell) => { - const mount = dbcMountBySourceSpellId[spell.id]; - if (mount === undefined) { - progress.increment(); - return; - } + await promisepool.PromisePool.for(mountSpells) + .withConcurrency(50) + .process(async (spell) => { + const mount = dbcMountBySourceSpellId[spell.id]; + if (mount === undefined) { + progress.increment(); + return; + } - const display = dbcMountDisplayByMountId[mount.id]; - const json = await download('meta/npc', `${display.creatureDisplayInfoId}.json`); - queueTexturesAndModels(json); + const display = dbcMountDisplayByMountId[mount.id]; + const json = await download("meta/npc", `${display.creatureDisplayInfoId}.json`); + queueTexturesAndModels(json); - progress.increment(); - }); + progress.increment(); + }); - progress.stop(); + progress.stop(); } async function downloadTextures(): Promise { - const progress = new Progress('Downloading textures...', texturesDownloadQueue.size); + const progress = new Progress("Downloading textures...", texturesDownloadQueue.size); - await promisepool.PromisePool.for(Array.from(texturesDownloadQueue)) - .withConcurrency(25) - .process(async (fileDataId) => { - await download('textures', `${fileDataId}.png`); - progress.increment(); - }); + await promisepool.PromisePool.for(Array.from(texturesDownloadQueue)) + .withConcurrency(25) + .process(async (fileDataId) => { + await download("textures", `${fileDataId}.png`); + progress.increment(); + }); - progress.stop(); + progress.stop(); } async function downloadModels(): Promise { - const progress = new Progress('Downloading models...', modelsDownloadQueue.size); + const progress = new Progress("Downloading models...", modelsDownloadQueue.size); - await promisepool.PromisePool.for(Array.from(modelsDownloadQueue)) - .withConcurrency(25) - .process(async (fileDataId) => { - await download('mo3', `${fileDataId}.mo3`); - progress.increment(); - }); + await promisepool.PromisePool.for(Array.from(modelsDownloadQueue)) + .withConcurrency(25) + .process(async (fileDataId) => { + await download("mo3", `${fileDataId}.mo3`); + progress.increment(); + }); - progress.stop(); + progress.stop(); } async function downloadBones(): Promise { - const progress = new Progress('Downloading bones...', bonesDownloadQueue.size); + const progress = new Progress("Downloading bones...", bonesDownloadQueue.size); - await promisepool.PromisePool.for(Array.from(bonesDownloadQueue)) - .withConcurrency(25) - .process(async (fileDataId) => { - try { - await download('bone', `${fileDataId}.bone`); - progress.increment(); - } catch (err) { - if (err instanceof HttpRequestError && err.response.status === 404) { - progress.increment(); - return; - } - throw err; - } - }); + await promisepool.PromisePool.for(Array.from(bonesDownloadQueue)) + .withConcurrency(25) + .process(async (fileDataId) => { + try { + await download("bone", `${fileDataId}.bone`); + progress.increment(); + } catch (err) { + if (err instanceof HttpRequestError && err.response.status === 404) { + progress.increment(); + return; + } + throw err; + } + }); - progress.stop(); + progress.stop(); } async function parseModels(): Promise { - const files = await glob('data/mo3/*.mo3'); - const progress = new Progress('Reading model files for texture references...', files.length); + const files = await glob("data/mo3/*.mo3"); + const progress = new Progress("Reading model files for texture references...", files.length); - await promisepool.PromisePool.for(files) - .withConcurrency(20) - .process(async (file) => { - const buffer = await fsp.readFile(file); + await promisepool.PromisePool.for(files) + .withConcurrency(20) + .process(async (file) => { + const buffer = await fsp.readFile(file); - const texturesOffset = buffer.readUInt32LE(60); - const uncompressedSize = buffer.readUInt32LE(112); - const compressedData = buffer.slice(116); - const data = Buffer.from(pako.inflate(compressedData)); - if (data.length !== uncompressedSize) { - throw `Unexpected data size ${data.length}, expected ${uncompressedSize}`; - } + const texturesOffset = buffer.readUInt32LE(60); + const uncompressedSize = buffer.readUInt32LE(112); + const compressedData = buffer.slice(116); + const data = Buffer.from(pako.inflate(compressedData)); + if (data.length !== uncompressedSize) { + throw `Unexpected data size ${data.length}, expected ${uncompressedSize}`; + } - const nbTextures = data.readInt32LE(texturesOffset); - let offset = texturesOffset + 4; - for (let i = 0; i < nbTextures; ++i) { - const textureId = data.readUInt32LE(offset + 4 + 4); - if (textureId !== 0) { - texturesDownloadQueue.add(textureId); - } + const nbTextures = data.readInt32LE(texturesOffset); + let offset = texturesOffset + 4; + for (let i = 0; i < nbTextures; ++i) { + const textureId = data.readUInt32LE(offset + 4 + 4); + if (textureId !== 0) { + texturesDownloadQueue.add(textureId); + } - offset += 4 + 4 + 4; - } + offset += 4 + 4 + 4; + } - progress.increment(); - }); + progress.increment(); + }); - progress.stop(); + progress.stop(); } async function main(): Promise { - const sw = new Stopwatch(); + const sw = new Stopwatch(); - await readDbcData(); - await downloadRaces(); // Download info for all races - await downloadArmors(); // Download info for all armors - await downloadWeapons(); // Download info for all weapons - await downloadMounts(); // Download info for all mounts - await downloadModels(); // Download all queued models - await parseModels(); // Read model files to find texture references - await downloadTextures(); // Download all queued textures - await downloadBones(); // Download all queued bones + await readDbcData(); + await downloadRaces(); // Download info for all races + await downloadArmors(); // Download info for all armors + await downloadWeapons(); // Download info for all weapons + await downloadMounts(); // Download info for all mounts + await downloadModels(); // Download all queued models + await parseModels(); // Read model files to find texture references + await downloadTextures(); // Download all queued textures + await downloadBones(); // Download all queued bones - sw.stop('Everything done in {time}'); + sw.stop("Everything done in {time}"); } main(); diff --git a/static/js/emblems.js b/static/js/emblems.js index 8d278eb..cfce3aa 100644 --- a/static/js/emblems.js +++ b/static/js/emblems.js @@ -1,118 +1,118 @@ 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) { - const $emblem = $(el); - const canvas = $emblem.find('canvas')[0]; - const ctx = canvas.getContext('2d'); + const $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 = $('
').addClass('images').appendTo($emblem); - const bgUpper = $('').attr('src', imgUrl('Background', 'U', emblem.background)).appendTo($images)[0]; - const bgLower = $('').attr('src', imgUrl('Background', 'L', emblem.background)).appendTo($images)[0]; - const iconUpper = $('').attr('src', imgUrl('Emblem', 'U', emblem.icon, emblem.iconColor)).appendTo($images)[0]; - const iconLower = $('').attr('src', imgUrl('Emblem', 'L', emblem.icon, emblem.iconColor)).appendTo($images)[0]; - const borderUpper = $('').attr('src', imgUrl('Border', 'U', emblem.border, emblem.borderColor)).appendTo($images)[0]; - const borderLower = $('').attr('src', imgUrl('Border', 'L', emblem.border, emblem.borderColor)).appendTo($images)[0]; + const $images = $("
").addClass("images").appendTo($emblem); + const bgUpper = $("").attr("src", imgUrl("Background", "U", emblem.background)).appendTo($images)[0]; + const bgLower = $("").attr("src", imgUrl("Background", "L", emblem.background)).appendTo($images)[0]; + const iconUpper = $("").attr("src", imgUrl("Emblem", "U", emblem.icon, emblem.iconColor)).appendTo($images)[0]; + const iconLower = $("").attr("src", imgUrl("Emblem", "L", emblem.icon, emblem.iconColor)).appendTo($images)[0]; + const borderUpper = $("").attr("src", imgUrl("Border", "U", emblem.border, emblem.borderColor)).appendTo($images)[0]; + const borderLower = $("").attr("src", imgUrl("Border", "L", emblem.border, emblem.borderColor)).appendTo($images)[0]; - const drawEmblemLayer = (ctx, layer) => { - const [upper, lower] = layer; + const drawEmblemLayer = (ctx, layer) => { + const [upper, lower] = layer; - const w = upper.width / 2; - const uh = upper.height; - const lh = lower.height; + const w = upper.width / 2; + const uh = upper.height; + const lh = lower.height; - ctx.drawImage(upper, 0, 0, w, uh, w, 0, w, uh); - ctx.drawImage(lower, 0, 0, w, lh, w, upper.height, w, lh); - ctx.save(); - ctx.scale(-1, 1); - ctx.drawImage(upper, 0, 0, w, uh, 0, 0, -w, uh); - ctx.drawImage(lower, 0, 0, w, lh, 0, upper.height, -w, lh); - ctx.restore(); - }; + ctx.drawImage(upper, 0, 0, w, uh, w, 0, w, uh); + ctx.drawImage(lower, 0, 0, w, lh, w, upper.height, w, lh); + ctx.save(); + ctx.scale(-1, 1); + ctx.drawImage(upper, 0, 0, w, uh, 0, 0, -w, uh); + ctx.drawImage(lower, 0, 0, w, lh, 0, upper.height, -w, lh); + ctx.restore(); + }; - waitForEmblemImages($emblem).then(() => { - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(26, 0); - ctx.lineTo(64, 28); - ctx.lineTo(102, 0); - ctx.lineTo(128, 0); - ctx.lineTo(128, 96); - ctx.lineTo(0, 96); - ctx.closePath(); - ctx.clip(); - drawEmblemLayer(ctx, [bgUpper, bgLower]); - drawEmblemLayer(ctx, [iconUpper, iconLower]); - drawEmblemLayer(ctx, [borderUpper, borderLower]); - }); + waitForEmblemImages($emblem).then(() => { + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(26, 0); + ctx.lineTo(64, 28); + ctx.lineTo(102, 0); + ctx.lineTo(128, 0); + ctx.lineTo(128, 96); + ctx.lineTo(0, 96); + ctx.closePath(); + ctx.clip(); + drawEmblemLayer(ctx, [bgUpper, bgLower]); + drawEmblemLayer(ctx, [iconUpper, iconLower]); + drawEmblemLayer(ctx, [borderUpper, borderLower]); + }); } function createArenaEmblem(teamSize, emblem, el) { - const $emblem = $(el); - const canvas = $emblem.find('canvas')[0]; - const ctx = canvas.getContext('2d'); + const $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 = $('
').addClass('images').appendTo($emblem); - const banner = $('').attr('src', imgUrl(teamSize)).appendTo($images)[0]; - const bannerCrop = $('').attr('src', imgUrl(teamSize, 'Crop')).appendTo($images)[0]; - const border = $('').attr('src', imgUrl(teamSize, 'Border', emblem.border)).appendTo($images)[0]; - const icon = $('').attr('src', imgUrl(undefined, 'Emblem', emblem.icon)).appendTo($images)[0]; + const $images = $("
").addClass("images").appendTo($emblem); + const banner = $("").attr("src", imgUrl(teamSize)).appendTo($images)[0]; + const bannerCrop = $("").attr("src", imgUrl(teamSize, "Crop")).appendTo($images)[0]; + const border = $("").attr("src", imgUrl(teamSize, "Border", emblem.border)).appendTo($images)[0]; + const icon = $("").attr("src", imgUrl(undefined, "Emblem", emblem.icon)).appendTo($images)[0]; - const colorToHex = (color) => '#' + parseInt(color).toString(16).substring(2); + const colorToHex = (color) => "#" + parseInt(color).toString(16).substring(2); - waitForEmblemImages($emblem).then(() => { - const srcH = 224; - const h = 128; - const w = (banner.width / srcH) * h; - const iconW = icon.width * 0.35; - const iconH = icon.height * 0.35; - ctx.drawImage(banner, 0, 0, banner.width, srcH, 0, 0, w, h); - ctx.drawImage(tintImage(bannerCrop, colorToHex(emblem.background)), 0, 0, banner.width, srcH, 0, 0, w, h); - ctx.drawImage(tintImage(border, colorToHex(emblem.borderColor)), 0, 0, border.width, srcH, 0, 0, w, h); - ctx.drawImage(tintImage(icon, colorToHex(emblem.iconColor)), w * 0.385 - iconW / 2, h * 0.325 - iconH / 2, iconW, iconH); - }); + waitForEmblemImages($emblem).then(() => { + const srcH = 224; + const h = 128; + const w = (banner.width / srcH) * h; + const iconW = icon.width * 0.35; + const iconH = icon.height * 0.35; + ctx.drawImage(banner, 0, 0, banner.width, srcH, 0, 0, w, h); + ctx.drawImage(tintImage(bannerCrop, colorToHex(emblem.background)), 0, 0, banner.width, srcH, 0, 0, w, h); + ctx.drawImage(tintImage(border, colorToHex(emblem.borderColor)), 0, 0, border.width, srcH, 0, 0, w, h); + ctx.drawImage(tintImage(icon, colorToHex(emblem.iconColor)), w * 0.385 - iconW / 2, h * 0.325 - iconH / 2, iconW, iconH); + }); } -const tintCanvas = document.createElement('canvas'); -const tintContext = tintCanvas.getContext('2d'); +const tintCanvas = document.createElement("canvas"); +const tintContext = tintCanvas.getContext("2d"); function tintImage(image, color, opacity = 1.0) { - const ctx = tintContext; + const ctx = tintContext; - ctx.canvas.width = image.width; - ctx.canvas.height = image.height; + ctx.canvas.width = image.width; + ctx.canvas.height = image.height; - // First draw the image to the buffer - ctx.drawImage(image, 0, 0); + // First draw the image to the buffer + ctx.drawImage(image, 0, 0); - // Multiply with a rectangle of the specified color - ctx.fillStyle = color; - ctx.globalCompositeOperation = 'multiply'; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + // Multiply with a rectangle of the specified color + ctx.fillStyle = color; + ctx.globalCompositeOperation = "multiply"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - // Finally, fix masking issues and globalAlpha - ctx.globalAlpha = opacity; - ctx.globalCompositeOperation = 'destination-in'; - ctx.drawImage(image, 0, 0); + // Finally, fix masking issues and globalAlpha + ctx.globalAlpha = opacity; + ctx.globalCompositeOperation = "destination-in"; + ctx.drawImage(image, 0, 0); - return ctx.canvas; + return ctx.canvas; } diff --git a/static/js/sync-url.js b/static/js/sync-url.js index ae3845b..a32d3dc 100644 --- a/static/js/sync-url.js +++ b/static/js/sync-url.js @@ -1,6 +1,6 @@ window.parent.postMessage( - { - url: window.location.pathname.replace(handlebarsData.websiteRoot, ''), - }, - '*', + { + url: window.location.pathname.replace(handlebarsData.websiteRoot, ""), + }, + "*", ); diff --git a/tsconfig.json b/tsconfig.json index e6ec662..1af51b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,6 @@ "outDir": "build", "moduleResolution": "node" }, - "exclude": [ - "node_modules" - ], + "exclude": ["node_modules"], "include": ["src/**/*.ts"] }