feat: use character data from database
This commit is contained in:
parent
028d0390f4
commit
ac26336bc3
16 changed files with 159543 additions and 162 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@ build/
|
|||
data/*
|
||||
!data/*.csv
|
||||
!data/background.png
|
||||
|
||||
# Configuration
|
||||
config.json
|
||||
|
|
|
|||
14
config.default.json
Normal file
14
config.default.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"realms": [
|
||||
{
|
||||
"name": "AzerothCore",
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"password": "root",
|
||||
"database": "acore_characters"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
158708
data/Item_9.2.0_41462.csv
Normal file
158708
data/Item_9.2.0_41462.csv
Normal file
File diff suppressed because it is too large
Load diff
91
package-lock.json
generated
91
package-lock.json
generated
|
|
@ -1075,6 +1075,11 @@
|
|||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"denque": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz",
|
||||
"integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
|
|
@ -1367,6 +1372,14 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"requires": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
|
|
@ -1909,6 +1922,11 @@
|
|||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
|
||||
},
|
||||
"is-self-closing": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-self-closing/-/is-self-closing-1.0.1.tgz",
|
||||
|
|
@ -2041,6 +2059,11 @@
|
|||
"log-utils": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
|
|
@ -2051,7 +2074,6 @@
|
|||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
|
|
@ -2246,6 +2268,55 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz",
|
||||
"integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==",
|
||||
"requires": {
|
||||
"denque": "^2.0.1",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"long": "^4.0.0",
|
||||
"lru-cache": "^6.0.0",
|
||||
"named-placeholders": "^1.1.2",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"named-placeholders": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz",
|
||||
"integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==",
|
||||
"requires": {
|
||||
"lru-cache": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
|
||||
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
|
||||
"requires": {
|
||||
"pseudomap": "^1.0.2",
|
||||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
|
||||
}
|
||||
}
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
|
|
@ -2562,6 +2633,11 @@
|
|||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
|
||||
},
|
||||
"pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
|
@ -2857,6 +2933,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"seq-queue": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||
"integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4="
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
|
||||
|
|
@ -3025,6 +3106,11 @@
|
|||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sqlstring": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz",
|
||||
"integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg=="
|
||||
},
|
||||
"static-extend": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||
|
|
@ -3542,8 +3628,7 @@
|
|||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "azerothcore-armory",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"clean": "rimraf build/",
|
||||
"start": "node build/armory/main.js",
|
||||
"watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"",
|
||||
"nodemon": "nodemon -q -w build build/armory/main.js",
|
||||
"nodemon": "nodemon -q -w build -w config.json build/armory/main.js",
|
||||
"fetchdata": "node build/tools/fetchdata.js"
|
||||
},
|
||||
"author": "https://github.com/r-o-b-o-t-o",
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
"glob-promise": "^4.2.2",
|
||||
"handlebars-helpers": "^0.10.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-fetch": "^2.6.6",
|
||||
"pako": "^2.0.4",
|
||||
"pretty-ms": "^7.0.1",
|
||||
|
|
|
|||
70
src/armory/Armory.ts
Normal file
70
src/armory/Armory.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import * as path from "path";
|
||||
import { Express } from "express";
|
||||
import * as express from "express";
|
||||
import { Connection, createConnection } from "mysql2/promise";
|
||||
import { engine as handlebarsEngine } from "express-handlebars";
|
||||
|
||||
import { Config } from "./Config";
|
||||
import { DbcReader } from "./data/dbcReader";
|
||||
import { CharacterCustomization } from "./data/CharacterCustomization";
|
||||
import { IndexController } from "./controllers/IndexController";
|
||||
import { CharacterController } from "./controllers/CharacterController";
|
||||
|
||||
export class Armory {
|
||||
public characterCustomization: CharacterCustomization;
|
||||
public dbcReader: DbcReader;
|
||||
public config: Config;
|
||||
|
||||
private charsDbs: { [key: string]: Connection };
|
||||
|
||||
public constructor() {
|
||||
this.dbcReader = new DbcReader();
|
||||
this.characterCustomization = new CharacterCustomization();
|
||||
this.charsDbs = {};
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
const app: Express = express();
|
||||
const listenPort = 48733;
|
||||
|
||||
this.config = await Config.load();
|
||||
await this.dbcReader.loadAllFiles();
|
||||
await this.characterCustomization.loadData();
|
||||
|
||||
for (const realm of this.config.realms) {
|
||||
this.charsDbs[realm.name.toLowerCase()] = await createConnection(realm.database);
|
||||
}
|
||||
|
||||
app.engine(".html", handlebarsEngine({
|
||||
"extname": "html",
|
||||
"partialsDir": path.join(process.cwd(), "static", "partials"),
|
||||
"layoutsDir": path.join(process.cwd(), "static"),
|
||||
"defaultLayout": "layout.html",
|
||||
"helpers": require("handlebars-helpers")(),
|
||||
}));
|
||||
app.set("view engine", "handlebars");
|
||||
app.set("views", path.join(process.cwd(), "static"));
|
||||
|
||||
app.use("/js", express.static(`static/js`));
|
||||
app.use("/css", express.static(`static/css`));
|
||||
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/background.png`));
|
||||
|
||||
const indexController = new IndexController(this);
|
||||
app.get("/", indexController.index.bind(indexController));
|
||||
|
||||
const charsController = new CharacterController(this);
|
||||
app.get("/character/:realm/:name", charsController.character.bind(charsController));
|
||||
|
||||
app.listen(listenPort, "0.0.0.0", () => {
|
||||
console.log(`Server is listening on 0.0.0.0:${listenPort}.`);
|
||||
});
|
||||
}
|
||||
|
||||
public getCharactersDb(realm: string): Connection {
|
||||
return this.charsDbs[realm.toLowerCase()];
|
||||
}
|
||||
}
|
||||
60
src/armory/Config.ts
Normal file
60
src/armory/Config.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import * as fs from "fs";
|
||||
const fsp = fs.promises;
|
||||
|
||||
export interface IDatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export interface IRealmConfig {
|
||||
name: string;
|
||||
database: IDatabaseConfig;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
public realms: IRealmConfig[];
|
||||
|
||||
private static checkedMissingField: boolean = false;
|
||||
|
||||
public static async load(): Promise<Config> {
|
||||
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(config, defaultConfig);
|
||||
Config.checkedMissingField = true;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static checkAllMissingFields(obj: object, model: object, parentName: string = "") {
|
||||
const missing = Config.hasMissingFields(obj, model);
|
||||
if (parentName !== "") {
|
||||
parentName += ".";
|
||||
}
|
||||
for (const field of missing) {
|
||||
console.warn(`Field ${parentName}${field} is missing in config.json!`);
|
||||
}
|
||||
for (const key in model) {
|
||||
if (typeof model[key] === "object" && obj.hasOwnProperty(key)) {
|
||||
Config.checkAllMissingFields(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;
|
||||
}
|
||||
};
|
||||
400
src/armory/controllers/CharacterController.ts
Normal file
400
src/armory/controllers/CharacterController.ts
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import * as express from "express";
|
||||
import { RowDataPacket } from "mysql2/promise";
|
||||
|
||||
import { Armory } from "../Armory";
|
||||
|
||||
interface ICharacterData {
|
||||
guid: number;
|
||||
name: string;
|
||||
race: number;
|
||||
class: number;
|
||||
gender: number;
|
||||
level: number;
|
||||
skin: number;
|
||||
face: number;
|
||||
hairStyle: number;
|
||||
hairColor: number;
|
||||
facialStyle: number;
|
||||
online: number;
|
||||
}
|
||||
|
||||
interface IEquipmentData {
|
||||
slot: number;
|
||||
itemEntry: number;
|
||||
flags: number;
|
||||
enchantments: string;
|
||||
randomPropertyId: number;
|
||||
}
|
||||
|
||||
interface ICustomizationOption {
|
||||
optionId: number;
|
||||
choiceId: number;
|
||||
}
|
||||
|
||||
export class CharacterController {
|
||||
private armory: Armory;
|
||||
private itemInventoryTypes: { [key: number]: number };
|
||||
|
||||
public constructor(armory: Armory) {
|
||||
this.armory = armory;
|
||||
this.itemInventoryTypes = {};
|
||||
for (const item of armory.dbcReader.dbcItem) {
|
||||
const retailItem = armory.dbcReader.dbcItemRetail.find(row => row.id === item.id);
|
||||
if (retailItem !== undefined) {
|
||||
this.itemInventoryTypes[item.id] = retailItem.inventoryType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async character(req: express.Request, res: express.Response): Promise<void> {
|
||||
const realm = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
if (this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase()) === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
let equipmentData = await this.getEquipmentData(realm, charData.guid);
|
||||
if (charData.class !== 3) {
|
||||
// Keep ranged weapon only if the character is a hunter
|
||||
equipmentData = equipmentData.filter(row => row.slot !== 17);
|
||||
}
|
||||
const customization = await this.getCustomizationOptions(charData);
|
||||
|
||||
res.render("character.html", {
|
||||
title: `Armory - ${charName}`,
|
||||
data: JSON.stringify({
|
||||
name: charData.name,
|
||||
race: charData.race,
|
||||
class: charData.class,
|
||||
gender: charData.gender,
|
||||
level: charData.level,
|
||||
online: charData.online === 1,
|
||||
characterModelItems: this.getModelViewerItems(equipmentData),
|
||||
customizationOptions: customization,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private async getCharacterData(realm: string, charName: string): Promise<ICharacterData> {
|
||||
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||
SELECT guid, name, race, class, gender, level, skin, face, hairStyle, hairColor, facialStyle, online
|
||||
FROM characters
|
||||
WHERE LOWER(name) = LOWER(?)
|
||||
`, [charName]);
|
||||
|
||||
if ((rows as RowDataPacket[]).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
|
||||
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||
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)
|
||||
`, [charGuid]);
|
||||
return rows as RowDataPacket[] as IEquipmentData[];
|
||||
}
|
||||
|
||||
private getModelViewerItems(equipmentData: IEquipmentData[]): number[][] {
|
||||
const visibleEquipment = equipmentData.filter(item => [0, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 18].includes(item.slot));
|
||||
|
||||
const items: number[][] = [];
|
||||
for (const equipment of visibleEquipment) {
|
||||
const modifiedAppearance = this.armory.dbcReader.dbcItemModifiedAppearance.find(row => row.itemId === equipment.itemEntry);
|
||||
if (modifiedAppearance === undefined) {
|
||||
continue;
|
||||
}
|
||||
const appearance = this.armory.dbcReader.dbcItemAppearance.find(row => row.id === modifiedAppearance.itemAppearanceId);
|
||||
if (appearance === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private async getCustomizationOptions(charData: ICharacterData): Promise<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]);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
import * as express from "express";
|
||||
|
||||
import { Armory } from "../Armory";
|
||||
|
||||
export class IndexController {
|
||||
public async index(req: express.Request, res: express.Response): Promise<void> {
|
||||
res.render("index.html", {
|
||||
"title": "Armory",
|
||||
});
|
||||
private armory: Armory;
|
||||
|
||||
public constructor(armory: Armory) {
|
||||
this.armory = armory;
|
||||
}
|
||||
|
||||
public async character(req: express.Request, res: express.Response): Promise<void> {
|
||||
res.render("character.html", {
|
||||
"title": "Armory",
|
||||
public async index(req: express.Request, res: express.Response): Promise<void> {
|
||||
res.render("index.html", {
|
||||
title: "Armory",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/armory/data/CharacterCustomization.ts
Normal file
27
src/armory/data/CharacterCustomization.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import * as fs from "fs";
|
||||
const fsp = fs.promises;
|
||||
import * as path from "path";
|
||||
|
||||
export class CharacterCustomization {
|
||||
private data: { [key: number]: { [key: number]: any } };
|
||||
|
||||
public async loadData(): Promise<void> {
|
||||
this.data = {};
|
||||
const races = [1, 2, 3, 4, 5, 6, 7, 8, 10, 11];
|
||||
const genders = [0, 1];
|
||||
|
||||
for (const race of races) {
|
||||
this.data[race] = {};
|
||||
|
||||
for (const gender of genders) {
|
||||
const buffer = await fsp.readFile(path.join(process.cwd(), `data/meta/charactercustomization2/${race}_${gender}.json`));
|
||||
const customizationData = JSON.parse(buffer.toString());
|
||||
this.data[race][gender] = customizationData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getCharacterCustomizationData(race: number, gender: number): any {
|
||||
return this.data[race][gender];
|
||||
}
|
||||
}
|
||||
115
src/armory/data/DbcReader.ts
Normal file
115
src/armory/data/DbcReader.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as csv from "csv-parser";
|
||||
import * as camelCase from "camelcase";
|
||||
|
||||
export interface IItemDbc {
|
||||
id: number;
|
||||
classId: number;
|
||||
subclassId: number;
|
||||
soundOverrideSubclassId: number;
|
||||
material: number;
|
||||
displayInfoId: number;
|
||||
inventoryType: number;
|
||||
sheatheType: number;
|
||||
}
|
||||
|
||||
export interface IItemRetailDbc {
|
||||
id: number;
|
||||
classId: number;
|
||||
subclassId: number;
|
||||
material: number;
|
||||
inventoryType: number;
|
||||
sheatheType: number;
|
||||
soundOverrideSubclassId: number;
|
||||
iconFileDataId: number;
|
||||
itemGroupSoundsId: number;
|
||||
contentTuningId: number;
|
||||
modifiedCraftingReagentItemId: number;
|
||||
}
|
||||
|
||||
export interface IItemAppearanceDbc {
|
||||
id: number;
|
||||
displayType: number;
|
||||
itemDisplayInfoId: number;
|
||||
defaultIconFileDataId: number;
|
||||
uiOrder: number;
|
||||
playerConditionId: number;
|
||||
}
|
||||
|
||||
export interface IItemModifiedAppearanceDbc {
|
||||
id: number;
|
||||
itemId: number;
|
||||
itemAppearanceModifierId: number;
|
||||
itemAppearanceId: number;
|
||||
orderIndex: number;
|
||||
transmogSourceTypeEnum: number;
|
||||
}
|
||||
|
||||
export interface IMountDbc {
|
||||
nameLang: string;
|
||||
sourceTextLang: string;
|
||||
descriptionLang: string;
|
||||
id: number;
|
||||
mountTypeId: number;
|
||||
flags: number;
|
||||
sourceTypeEnum: number;
|
||||
sourceSpellId: number;
|
||||
playerConditionId: number;
|
||||
mountFlyRideHeight: number;
|
||||
uiModelSceneId: number;
|
||||
mountSpecialRiderAnimKitId: number;
|
||||
mountSpecialSpellVisualKitId: number;
|
||||
}
|
||||
|
||||
export interface IMountXDisplayDbc {
|
||||
id: number;
|
||||
creatureDisplayInfoId: number;
|
||||
playerConditionId: number;
|
||||
mountId: number;
|
||||
}
|
||||
|
||||
export interface ISpellDbc {
|
||||
id: number;
|
||||
mechanic: number;
|
||||
}
|
||||
|
||||
export class DbcReader {
|
||||
public dbcItem: IItemDbc[];
|
||||
public dbcItemRetail: IItemRetailDbc[];
|
||||
public dbcItemAppearance: IItemAppearanceDbc[];
|
||||
public dbcItemModifiedAppearance: IItemModifiedAppearanceDbc[];
|
||||
public dbcMount: IMountDbc[];
|
||||
public dbcMountDisplay: IMountXDisplayDbc[];
|
||||
public dbcSpell: ISpellDbc[];
|
||||
|
||||
public readDbcFile<T>(file: string): Promise<T[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const rows = [];
|
||||
|
||||
fs.createReadStream(file)
|
||||
.pipe(csv({
|
||||
mapHeaders: ({ header, index }) => camelCase(header).replace(/[\[\]]/g, ""),
|
||||
mapValues: ({ header, index, value }) => isNaN(value) ? value : Number(value),
|
||||
}))
|
||||
.on("error", rej)
|
||||
.on("data", (data) => rows.push(data))
|
||||
.on("end", () => {
|
||||
res(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async loadAllFiles(): Promise<void> {
|
||||
const dir = path.join(process.cwd(), "data");
|
||||
|
||||
this.dbcItem = await this.readDbcFile<IItemDbc>(path.join(dir, "Item_3.3.5_12340.csv"));
|
||||
this.dbcItemRetail = await this.readDbcFile<IItemRetailDbc>(path.join(dir, "Item_9.2.0_41462.csv"));
|
||||
this.dbcItemAppearance = await this.readDbcFile<IItemAppearanceDbc>(path.join(dir, "ItemAppearance_9.2.0_41462.csv"));
|
||||
this.dbcItemModifiedAppearance = await this.readDbcFile<IItemModifiedAppearanceDbc>(path.join(dir, "ItemModifiedAppearance_9.2.0_41462.csv"));
|
||||
this.dbcMount = await this.readDbcFile<IMountDbc>(path.join(dir, "Mount_9.2.0_41462.csv"));
|
||||
this.dbcMountDisplay = await this.readDbcFile<IMountXDisplayDbc>(path.join(dir, "MountXDisplay_9.2.0_41462.csv"));
|
||||
this.dbcSpell = await this.readDbcFile<ISpellDbc>(path.join(dir, "Spell_3.3.5_12340.csv"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,10 @@
|
|||
import * as path from "path";
|
||||
import { Express } from "express";
|
||||
import * as express from "express";
|
||||
import { engine as handlebarsEngine } from "express-handlebars";
|
||||
import { Armory } from "./Armory";
|
||||
|
||||
import { IndexController } from "./controllers/IndexController";
|
||||
async function main(): Promise<void> {
|
||||
require("source-map-support").install();
|
||||
|
||||
function main(): void {
|
||||
const app: Express = express();
|
||||
const listenPort = 48733;
|
||||
|
||||
app.engine(".html", handlebarsEngine({
|
||||
"extname": "html",
|
||||
"partialsDir": path.join(process.cwd(), "static", "partials"),
|
||||
"layoutsDir": path.join(process.cwd(), "static"),
|
||||
"defaultLayout": "layout.html",
|
||||
"helpers": require("handlebars-helpers")(),
|
||||
}));
|
||||
app.set("view engine", "handlebars");
|
||||
app.set("views", path.join(process.cwd(), "static"));
|
||||
|
||||
app.use("/js", express.static(`static/js`));
|
||||
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/background.png`));
|
||||
|
||||
const indexController = new IndexController();
|
||||
app.get("/", indexController.index.bind(indexController));
|
||||
app.get("/character", indexController.character.bind(indexController));
|
||||
|
||||
app.listen(listenPort, "0.0.0.0", () => {
|
||||
console.log(`Server is listening on 0.0.0.0:${listenPort}.`);
|
||||
});
|
||||
const armory = new Armory();
|
||||
armory.start();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import * as path from "path";
|
|||
import * as pako from "pako";
|
||||
import fetch from "node-fetch";
|
||||
import * as mkdirp from "mkdirp";
|
||||
import * as csv from "csv-parser";
|
||||
import * as glob from "glob-promise";
|
||||
import { Response } from "node-fetch";
|
||||
import * as prettyMs from "pretty-ms";
|
||||
import * as camelCase from "camelcase";
|
||||
import * as cliProgress from "cli-progress";
|
||||
import promisepool = require("@supercharge/promise-pool");
|
||||
|
||||
import { DbcReader, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/dbcReader";
|
||||
|
||||
require("source-map-support").install();
|
||||
|
||||
const baseUrl = "https://wow.zamimg.com/modelviewer/live";
|
||||
|
|
@ -73,77 +73,16 @@ class HttpRequestError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
interface IItemDbc {
|
||||
id: number;
|
||||
classId: number;
|
||||
subclassId: number;
|
||||
soundOverrideSubclassId: number;
|
||||
material: number;
|
||||
displayInfoId: number;
|
||||
inventoryType: number;
|
||||
sheatheType: number;
|
||||
}
|
||||
|
||||
interface IItemAppearanceDbc {
|
||||
id: number;
|
||||
displayType: number;
|
||||
itemDisplayInfoId: number;
|
||||
defaultIconFileDataId: number;
|
||||
uiOrder: number;
|
||||
playerConditionId: number;
|
||||
}
|
||||
|
||||
interface IItemModifiedAppearanceDbc {
|
||||
id: number;
|
||||
itemId: number;
|
||||
itemAppearanceModifierId: number;
|
||||
itemAppearanceId: number;
|
||||
orderIndex: number;
|
||||
transmogSourceTypeEnum: number;
|
||||
}
|
||||
|
||||
interface IMountDbc {
|
||||
nameLang: string;
|
||||
sourceTextLang: string;
|
||||
descriptionLang: string;
|
||||
id: number;
|
||||
mountTypeId: number;
|
||||
flags: number;
|
||||
sourceTypeEnum: number;
|
||||
sourceSpellId: number;
|
||||
playerConditionId: number;
|
||||
mountFlyRideHeight: number;
|
||||
uiModelSceneId: number;
|
||||
mountSpecialRiderAnimKitId: number;
|
||||
mountSpecialSpellVisualKitId: number;
|
||||
}
|
||||
|
||||
interface IMountXDisplayDbc {
|
||||
id: number;
|
||||
creatureDisplayInfoId: number;
|
||||
playerConditionId: number;
|
||||
mountId: number;
|
||||
}
|
||||
|
||||
interface ISpellDbc {
|
||||
id: number;
|
||||
mechanic: number;
|
||||
}
|
||||
|
||||
let dbcItem: IItemDbc[];
|
||||
let dbcItemAppearance: IItemAppearanceDbc[];
|
||||
let dbcReader: DbcReader;
|
||||
let dbcItemAppearanceById: { [key: number]: IItemAppearanceDbc };
|
||||
let dbcItemModifiedAppearance: IItemModifiedAppearanceDbc[];
|
||||
let dbcItemModifiedAppearanceByItemId: { [key: number]: IItemModifiedAppearanceDbc };
|
||||
let dbcMount: IMountDbc[];
|
||||
let dbcMountBySourceSpellId: { [key: number]: IMountDbc };
|
||||
let dbcMountDisplay: IMountXDisplayDbc[];
|
||||
let dbcMountDisplayByMountId: { [key: number]: IMountXDisplayDbc };
|
||||
let dbcSpell: ISpellDbc[];
|
||||
|
||||
const classIdArmor = 4;
|
||||
const classIdWeapon = 2;
|
||||
const invTypeShield = 14;
|
||||
const invTypeOffHand = 23;
|
||||
const spellMechanicMounted = 21;
|
||||
|
||||
const modelsDownloadQueue = new Set<number>();
|
||||
|
|
@ -276,7 +215,7 @@ async function downloadRaces(): Promise<void> {
|
|||
}
|
||||
|
||||
async function downloadArmors(): Promise<void> {
|
||||
const rows = dbcItem.filter((row) => row.classId === classIdArmor);
|
||||
const rows = dbcReader.dbcItem.filter((row) => row.classId === classIdArmor);
|
||||
|
||||
const progress = new Progress("Downloading armor data...", rows.length);
|
||||
|
||||
|
|
@ -290,7 +229,7 @@ async function downloadArmors(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
|
||||
const metaPath = row.inventoryType === invTypeShield ? "item" : `armor/${row.inventoryType}`; // Inventory Type 14 = Shield
|
||||
const metaPath = [invTypeShield, invTypeOffHand].includes(row.inventoryType) ? "item" : `armor/${row.inventoryType}`;
|
||||
try {
|
||||
const itemJson = await download(`meta/${metaPath}`, `${appearance.itemDisplayInfoId}.json`);
|
||||
queueTexturesAndModels(itemJson);
|
||||
|
|
@ -308,7 +247,7 @@ async function downloadArmors(): Promise<void> {
|
|||
}
|
||||
|
||||
async function downloadWeapons(): Promise<void> {
|
||||
const rows = dbcItem.filter((row) => row.classId === classIdWeapon);
|
||||
const rows = dbcReader.dbcItem.filter((row) => row.classId === classIdWeapon);
|
||||
|
||||
const progress = new Progress("Downloading weapon data...", rows.length);
|
||||
|
||||
|
|
@ -338,58 +277,35 @@ async function downloadWeapons(): Promise<void> {
|
|||
progress.stop();
|
||||
}
|
||||
|
||||
function readDbcFile<T>(file: string): Promise<T[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const rows = [];
|
||||
|
||||
fs.createReadStream(file)
|
||||
.pipe(csv({
|
||||
mapHeaders: ({ header, index }) => camelCase(header).replace(/[\[\]]/g, ""),
|
||||
mapValues: ({ header, index, value }) => isNaN(value) ? value : Number(value),
|
||||
}))
|
||||
.on("error", rej)
|
||||
.on("data", (data) => rows.push(data))
|
||||
.on("end", () => {
|
||||
res(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readDbcData(): Promise<void> {
|
||||
const dir = path.join(process.cwd(), "data");
|
||||
console.log("Reading DBC data...");
|
||||
|
||||
dbcItem = await readDbcFile<IItemDbc>(path.join(dir, "Item_3.3.5_12340.csv"));
|
||||
dbcReader = new DbcReader();
|
||||
await dbcReader.loadAllFiles();
|
||||
|
||||
dbcItemAppearance = await readDbcFile<IItemAppearanceDbc>(path.join(dir, "ItemAppearance_9.2.0_41462.csv"));
|
||||
dbcItemAppearanceById = {};
|
||||
for (const row of dbcItemAppearance) {
|
||||
for (const row of dbcReader.dbcItemAppearance) {
|
||||
dbcItemAppearanceById[row.id] = row;
|
||||
}
|
||||
|
||||
dbcItemModifiedAppearance = await readDbcFile<IItemModifiedAppearanceDbc>(path.join(dir, "ItemModifiedAppearance_9.2.0_41462.csv"));
|
||||
dbcItemModifiedAppearanceByItemId = {};
|
||||
for (const row of dbcItemModifiedAppearance) {
|
||||
for (const row of dbcReader.dbcItemModifiedAppearance) {
|
||||
dbcItemModifiedAppearanceByItemId[row.itemId] = row;
|
||||
}
|
||||
|
||||
dbcMount = await readDbcFile<IMountDbc>(path.join(dir, "Mount_9.2.0_41462.csv"));
|
||||
dbcMountBySourceSpellId = {};
|
||||
for (const row of dbcMount) {
|
||||
for (const row of dbcReader.dbcMount) {
|
||||
dbcMountBySourceSpellId[row.sourceSpellId] = row;
|
||||
}
|
||||
|
||||
dbcMountDisplay = await readDbcFile<IMountXDisplayDbc>(path.join(dir, "MountXDisplay_9.2.0_41462.csv"));
|
||||
dbcMountDisplayByMountId = {};
|
||||
for (const row of dbcMountDisplay) {
|
||||
for (const row of dbcReader.dbcMountDisplay) {
|
||||
dbcMountDisplayByMountId[row.mountId] = row;
|
||||
}
|
||||
|
||||
dbcSpell = await readDbcFile<ISpellDbc>(path.join(dir, "Spell_3.3.5_12340.csv"));
|
||||
}
|
||||
|
||||
async function downloadMounts(): Promise<void> {
|
||||
const mountSpells = dbcSpell.filter(spell => spell.mechanic === spellMechanicMounted);
|
||||
const mountSpells = dbcReader.dbcSpell.filter(spell => spell.mechanic === spellMechanicMounted);
|
||||
const progress = new Progress("Downloading mount data...", mountSpells.length);
|
||||
|
||||
await promisepool.PromisePool
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@
|
|||
debug: () => { },
|
||||
};
|
||||
|
||||
const charData = JSON.parse(`{{{data}}}`);
|
||||
const races = {
|
||||
1: "human",
|
||||
2: "orc",
|
||||
3: "dwarf",
|
||||
4: "nightelf",
|
||||
5: "scourge",
|
||||
6: "tauren",
|
||||
7: "gnome",
|
||||
8: "troll",
|
||||
10: "bloodelf",
|
||||
11: "draenei",
|
||||
};
|
||||
const genders = ["male", "female"];
|
||||
|
||||
const charactermodel = {
|
||||
type: ZamModelViewer.WOW,
|
||||
contentPath: "/data/",
|
||||
|
|
@ -15,27 +30,17 @@
|
|||
hd: true,
|
||||
aspect: 0.76,
|
||||
charCustomization: {
|
||||
"race": 4,
|
||||
"gender": 1,
|
||||
"options": [{ "optionId": 50, "choiceId": 826 }, { "optionId": 49, "choiceId": 805 }, { "optionId": 51, "choiceId": 843 }, { "optionId": 52, "choiceId": 864 }, { "optionId": 724, "choiceId": 7963 }, { "optionId": 726, "choiceId": 7995 }, { "optionId": 736, "choiceId": 8071 }, { "optionId": 732, "choiceId": 8052 }, { "optionId": 57, "choiceId": 901 }, { "optionId": 744, "choiceId": 8130 }, { "optionId": 683, "choiceId": 7619 }, { "optionId": 53, "choiceId": 880 }, { "optionId": 745, "choiceId": 8290 }, { "optionId": 714, "choiceId": 7868 }, { "optionId": 708, "choiceId": 7845 }, { "optionId": 712, "choiceId": 7861 }, { "optionId": 710, "choiceId": 7853 }, { "optionId": 56, "choiceId": 894 }, { "optionId": 377, "choiceId": 881 }, { "optionId": 55, "choiceId": 8172 }, { "optionId": 566, "choiceId": 6518 }, { "optionId": 624, "choiceId": 7144 }, { "optionId": 925, "choiceId": 15393 }, { "optionId": 642, "choiceId": 7248 }, { "optionId": 634, "choiceId": 7213 }, { "optionId": 638, "choiceId": 7230 }],
|
||||
"sheathMain": -1,
|
||||
"sheathOff": -1,
|
||||
race: charData.race,
|
||||
gender: charData.gender,
|
||||
options: charData.customizationOptions,
|
||||
sheathMain: -1,
|
||||
sheathOff: -1,
|
||||
},
|
||||
cls: 6, // class
|
||||
items: [
|
||||
[3, 63669],
|
||||
[5, 60785],
|
||||
[10, 63673],
|
||||
[7, 60787],
|
||||
[8, 60788],
|
||||
[6, 61034],
|
||||
[16, 61031],
|
||||
[21, 64535],
|
||||
[22, 64152],
|
||||
],
|
||||
cls: charData.class,
|
||||
items: charData.characterModelItems,
|
||||
models: {
|
||||
type: ZamModelViewer.Wow.Types.CHARACTER,
|
||||
id: "nightelffemale",
|
||||
id: `${races[charData.race]}${genders[charData.gender]}`,
|
||||
},
|
||||
mount: {
|
||||
type: ZamModelViewer.Wow.Types.NPC,
|
||||
|
|
|
|||
3
static/css/armory.css
Normal file
3
static/css/armory.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
html {
|
||||
background-color: #202124;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{title}}</title>
|
||||
|
||||
<!-- <link rel="stylesheet" href="/css/armory.css"> -->
|
||||
<link rel="stylesheet" href="/css/armory.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue