feat(character/talents): add talents page
This commit is contained in:
parent
f32b70fb0b
commit
f979241e37
12 changed files with 5165 additions and 79 deletions
|
|
@ -69,6 +69,7 @@ export class Armory {
|
|||
const charsController = new CharacterController(this);
|
||||
await charsController.load();
|
||||
app.get("/character/:realm/:name", charsController.character.bind(charsController));
|
||||
app.get("/character/:realm/:name/talents", charsController.talents.bind(charsController));
|
||||
|
||||
this.gc();
|
||||
app.listen(listenPort, "0.0.0.0", () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as express from "express";
|
|||
import { RowDataPacket } from "mysql2/promise";
|
||||
|
||||
import { Armory } from "../Armory";
|
||||
import { IRealmConfig } from "../Config";
|
||||
|
||||
interface ICharacterData {
|
||||
guid: number;
|
||||
|
|
@ -32,6 +33,30 @@ interface ICustomizationOption {
|
|||
}
|
||||
|
||||
const ITEM_CLASS_GEM = 3;
|
||||
const RaceDisplayName = {
|
||||
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",
|
||||
};
|
||||
|
||||
export class CharacterController {
|
||||
private armory: Armory;
|
||||
|
|
@ -85,22 +110,24 @@ export class CharacterController {
|
|||
}
|
||||
|
||||
public async character(req: express.Request, res: express.Response): Promise<void> {
|
||||
const realm = req.params.realm;
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
if (this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase()) === undefined) {
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
const charData = await this.getCharacterData(realmName, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
const equipmentData = await this.getEquipmentData(realm, 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];
|
||||
|
|
@ -111,13 +138,11 @@ export class CharacterController {
|
|||
|
||||
res.render("character.html", {
|
||||
title: `Armory - ${charName}`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: JSON.stringify({
|
||||
name: charData.name,
|
||||
race: charData.race,
|
||||
class: charData.class,
|
||||
gender: charData.gender,
|
||||
level: charData.level,
|
||||
online: charData.online === 1,
|
||||
class: charData.class,
|
||||
characterModelItems: await this.getModelViewerItems(equipmentData, charData.class),
|
||||
customizationOptions: customization,
|
||||
equipment,
|
||||
|
|
@ -127,6 +152,50 @@ export class CharacterController {
|
|||
this.armory.gc();
|
||||
}
|
||||
|
||||
public async talents(req: express.Request, res: express.Response): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm.name, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
res.render("character-talents.html", {
|
||||
title: `Armory - ${charName} - Talents`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: JSON.stringify({
|
||||
talents: await this.getTalents(realm.name, charData.guid),
|
||||
trees: await this.getTalentTrees(charData.class),
|
||||
glyphs: await this.getGlyphs(realm.name, charData.guid),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private makeSharedDataObject(realm: IRealmConfig, charData: ICharacterData) {
|
||||
return {
|
||||
realm: realm.name,
|
||||
character: charData.name,
|
||||
race: RaceDisplayName[charData.race],
|
||||
class: ClassDisplayName[charData.class],
|
||||
level: charData.level,
|
||||
online: charData.online === 1,
|
||||
};
|
||||
}
|
||||
|
||||
private getRealm(realm: string): IRealmConfig {
|
||||
return this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -468,4 +537,77 @@ export class CharacterController {
|
|||
|
||||
return options;
|
||||
}
|
||||
|
||||
private async getTalents(realm: string, character: number): Promise<number[][]> {
|
||||
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||
SELECT spell, specMask
|
||||
FROM character_talent
|
||||
WHERE guid = ?
|
||||
`, [character]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 async getGlyphs(realm: string, character: number): Promise<any[][]> {
|
||||
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
|
||||
FROM character_glyphs
|
||||
WHERE guid = ?
|
||||
`, [character]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import * as path from "path";
|
|||
|
||||
import * as camelCase from "camelcase";
|
||||
|
||||
export interface IGlyphProperties {
|
||||
id: number;
|
||||
spellId: number;
|
||||
}
|
||||
|
||||
export interface IItemDbc {
|
||||
id: number;
|
||||
classId: number;
|
||||
|
|
@ -45,6 +50,7 @@ export interface IMountXDisplayDbc {
|
|||
export interface ISpellDbc {
|
||||
id: number;
|
||||
mechanic: number;
|
||||
spellIconId: number;
|
||||
}
|
||||
|
||||
export interface ISpellItemEnchantmentDbc {
|
||||
|
|
@ -52,6 +58,32 @@ export interface ISpellItemEnchantmentDbc {
|
|||
srcItemId: number;
|
||||
}
|
||||
|
||||
export interface ISpellIcon {
|
||||
id: number;
|
||||
textureFilename: string;
|
||||
}
|
||||
|
||||
export interface ITalent {
|
||||
id: number;
|
||||
tabId: number;
|
||||
tierId: number;
|
||||
columnIndex: number;
|
||||
spellRank0: number;
|
||||
spellRank1: number;
|
||||
spellRank2: number;
|
||||
spellRank3: number;
|
||||
spellRank4: number;
|
||||
prereqTalent0: number;
|
||||
prereqRank0: number;
|
||||
}
|
||||
|
||||
export interface ITalentTab {
|
||||
id: number;
|
||||
nameLang0: string;
|
||||
spellIconId: number;
|
||||
classMask: number;
|
||||
}
|
||||
|
||||
interface IAsyncGeneratorWithArrayMethods<T> {
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T>;
|
||||
toArray(): Promise<T[]>;
|
||||
|
|
@ -161,17 +193,17 @@ class DbcReader<T> {
|
|||
|
||||
public async *read(): AsyncGenerator<T> {
|
||||
const stream = fs.createReadStream(this.filePath);
|
||||
const itr = this.readLines(stream);
|
||||
const itr = this.parseCsv(stream);
|
||||
const headerLine = await itr.next();
|
||||
if (headerLine.done === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headerCols = this.parseCsvLine(headerLine.value)
|
||||
const headerCols = headerLine.value
|
||||
.map(header => camelCase(header).replace(/[\[\]]/g, ""));
|
||||
|
||||
for await (const line of itr) {
|
||||
const cols = this.parseCsvLine(line)
|
||||
for await (const arr of itr) {
|
||||
const cols = arr
|
||||
.map(value => {
|
||||
const parsed: number = parseInt(value, 10);
|
||||
return isNaN(parsed) ? value : parsed;
|
||||
|
|
@ -186,78 +218,71 @@ class DbcReader<T> {
|
|||
}
|
||||
}
|
||||
|
||||
private async *readLines(stream: fs.ReadStream): AsyncGenerator<string> {
|
||||
let previous = "";
|
||||
|
||||
for await (const chunk of stream) {
|
||||
previous += chunk;
|
||||
let unixEolIndex: number;
|
||||
let winEolIndex: number;
|
||||
|
||||
while ((unixEolIndex = previous.indexOf("\n")) >= 0 || (winEolIndex = previous.indexOf("\r\n")) >= 0) {
|
||||
const unix = unixEolIndex >= 0;
|
||||
const line = previous.slice(0, (unix ? unixEolIndex : winEolIndex) - 1);
|
||||
yield line;
|
||||
previous = previous.slice(unix ? (unixEolIndex + 1) : (winEolIndex + 2));
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.length > 0) {
|
||||
yield previous;
|
||||
}
|
||||
}
|
||||
|
||||
private parseCsvLine(line: string): string[] {
|
||||
private async *parseCsv(stream: fs.ReadStream): AsyncGenerator<string[]> {
|
||||
// Adapted from https://stackoverflow.com/a/14991797
|
||||
const arr = [];
|
||||
let col = 0;
|
||||
let quote = false; // 'true' means we're inside a quoted field
|
||||
|
||||
// Iterate over each character, keep track of current row and column (of the returned array)
|
||||
for (let col = 0, c = 0; c < line.length; c++) {
|
||||
let cc = line[c], nc = line[c + 1]; // Current character, next character
|
||||
arr[col] = arr[col] || ""; // Create a new column (start with empty string) if necessary
|
||||
for await (const chunk of stream) {
|
||||
const str = chunk.toString();
|
||||
// Iterate over each character, keep track of current column (of the returned array)
|
||||
for (let c = 0; c < str.length; ++c) {
|
||||
let ch = str[c], nch = str[c + 1]; // Current character, next character
|
||||
if (!(col in arr)) {
|
||||
arr[col] = ""; // Create a new column (start with empty string) if necessary
|
||||
}
|
||||
|
||||
// If the current character is a quotation mark, and we're inside a
|
||||
// quoted field, and the next character is also a quotation mark,
|
||||
// add a quotation mark to the current column and skip the next character
|
||||
if (cc == '"' && quote && nc == '"') {
|
||||
arr[col] += cc; ++c;
|
||||
continue;
|
||||
// If the current character is a quotation mark, and we're inside a
|
||||
// quoted field, and the next character is also a quotation mark,
|
||||
// add a quotation mark to the current column and skip the next character
|
||||
if (ch == '"' && quote && nch == '"') {
|
||||
arr[col] += ch;
|
||||
++c;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's just one quotation mark, begin/end quoted field
|
||||
if (ch == '"') {
|
||||
quote = !quote;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a comma and we're not in a quoted field, move on to the next column
|
||||
if (ch == ',' && !quote) {
|
||||
++col;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
||||
// and move on to the next row and move to column 0 of that new row
|
||||
if (ch == '\r' && nch == '\n' && !quote) {
|
||||
yield arr;
|
||||
arr.length = 0; // Clear the row
|
||||
col = 0;
|
||||
++c;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a newline (LF or CR) and we're not in a quoted field,
|
||||
// move on to the next row and move to column 0 of that new row
|
||||
if (!quote && (ch == '\r' || ch == '\n')) {
|
||||
yield arr;
|
||||
arr.length = 0; // Clear the row
|
||||
col = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, append the current character to the current column
|
||||
arr[col] += ch;
|
||||
}
|
||||
|
||||
// If it's just one quotation mark, begin/end quoted field
|
||||
if (cc == '"') {
|
||||
quote = !quote;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a comma and we're not in a quoted field, move on to the next column
|
||||
if (cc == ',' && !quote) {
|
||||
++col;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a newline (CRLF) and we're not in a quoted field, move on to the next row
|
||||
if (cc == '\r' && nc == '\n' && !quote) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If it's a newline (LF or CR) and we're not in a quoted field, move on to the next row
|
||||
if ((cc == '\n' && !quote) ||
|
||||
(cc == '\r' && !quote)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, append the current character to the current column
|
||||
arr[col] += cc;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
const dir = path.join(process.cwd(), "data");
|
||||
export const DbcFiles = {
|
||||
glyphProperties: path.join(dir, "GlyphProperties_3.3.5_12340.csv"),
|
||||
item: path.join(dir, "Item_3.3.5_12340.csv"),
|
||||
itemRetail: path.join(dir, "Item_9.2.0_41462.csv"),
|
||||
itemAppearance: path.join(dir, "ItemAppearance_9.2.0_41462.csv"),
|
||||
|
|
@ -267,9 +292,13 @@ export const DbcFiles = {
|
|||
mountDisplay: path.join(dir, "MountXDisplay_9.2.0_41462.csv"),
|
||||
spell: path.join(dir, "Spell_3.3.5_12340.csv"),
|
||||
spellItemEnchantment: path.join(dir, "SpellItemEnchantment_3.3.5_12340.csv"),
|
||||
spellIcon: path.join(dir, "SpellIcon_3.3.5_12340.csv"),
|
||||
talent: path.join(dir, "Talent_3.3.5_12340.csv"),
|
||||
talentTab: path.join(dir, "TalentTab_3.3.5_12340.csv"),
|
||||
};
|
||||
|
||||
const dbcFields = {
|
||||
glyphProperties: ["id", "spellId"],
|
||||
item: ["id", "classId", "displayInfoId", "inventoryType"],
|
||||
itemRetail: ["id", "inventoryType"],
|
||||
itemAppearance: ["id", "itemDisplayInfoId"],
|
||||
|
|
@ -277,11 +306,15 @@ const dbcFields = {
|
|||
itemDisplayInfo: ["id", "inventoryIcon0"],
|
||||
mount: ["id", "sourceSpellId"],
|
||||
mountDisplay: ["id", "creatureDisplayInfoId", "mountId"],
|
||||
spell: ["id", "mechanic"],
|
||||
spell: ["id", "mechanic", "spellIconId"],
|
||||
spellItemEnchantment: ["id", "srcItemId"],
|
||||
spellIcon: ["id", "textureFilename"],
|
||||
talent: ["id", "tabId", "tierId", "columnIndex", "spellRank0", "spellRank1", "spellRank2", "spellRank3", "spellRank4", "prereqTalent0", "prereqRank0"],
|
||||
talentTab: ["id", "nameLang0", "spellIconId", "classMask"],
|
||||
};
|
||||
|
||||
export class DbcManager {
|
||||
private _glyphProperties: IGlyphProperties[];
|
||||
private _item: IItemDbc[];
|
||||
private _itemRetail: IItemRetailDbc[];
|
||||
private _itemAppearance: IItemAppearanceDbc[];
|
||||
|
|
@ -291,8 +324,12 @@ export class DbcManager {
|
|||
private _mountDisplay: IMountXDisplayDbc[];
|
||||
private _spell: ISpellDbc[];
|
||||
private _spellItemEnchantment: ISpellItemEnchantmentDbc[];
|
||||
private _spellIcon: ISpellIcon[];
|
||||
private _talent: ITalent[];
|
||||
private _talentTab: ITalentTab[];
|
||||
|
||||
public async loadAllFiles(): Promise<void> {
|
||||
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
||||
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
||||
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
||||
this._itemAppearance = await this.read<IItemAppearanceDbc>(DbcFiles.itemAppearance, dbcFields.itemAppearance).toArray();
|
||||
|
|
@ -302,6 +339,13 @@ export class DbcManager {
|
|||
this._mountDisplay = await this.read<IMountXDisplayDbc>(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray();
|
||||
this._spell = await this.read<ISpellDbc>(DbcFiles.spell, dbcFields.spell).toArray();
|
||||
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(DbcFiles.spellItemEnchantment, dbcFields.spellItemEnchantment).toArray();
|
||||
this._spellIcon = await this.read<ISpellIcon>(DbcFiles.spellIcon, dbcFields.spellIcon).toArray();
|
||||
this._talent = await this.read<ITalent>(DbcFiles.talent, dbcFields.talent).toArray();
|
||||
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();
|
||||
}
|
||||
|
||||
public glyphProperties() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.glyphProperties, this._glyphProperties, dbcFields.glyphProperties);
|
||||
}
|
||||
|
||||
public item() {
|
||||
|
|
@ -340,6 +384,18 @@ export class DbcManager {
|
|||
return this.getLoadedDataOrRead(DbcFiles.spellItemEnchantment, this._spellItemEnchantment, dbcFields.spellItemEnchantment);
|
||||
}
|
||||
|
||||
public spellIcon() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.spellIcon, this._spellIcon, dbcFields.spellIcon);
|
||||
}
|
||||
|
||||
public talent() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.talent, this._talent, dbcFields.talent);
|
||||
}
|
||||
|
||||
public talentTab() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.talentTab, this._talentTab, dbcFields.talentTab);
|
||||
}
|
||||
|
||||
private read<T>(file: string, keepFields: string[] = []): AsyncGenWrapper<T> {
|
||||
const reader = new DbcReader<T>(file, keepFields);
|
||||
return new AsyncGenWrapper(reader.read());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue