Add Skills, Reputation, and Quests pages
This commit is contained in:
commit
f43bd87d51
23 changed files with 4413 additions and 6 deletions
|
|
@ -14,6 +14,7 @@ import { IndexController } from "./controllers/IndexController";
|
|||
import { CharacterController } from "./controllers/CharacterController";
|
||||
import { GuildController } from "./controllers/GuildController";
|
||||
import { ArenaController } from "./controllers/ArenaController";
|
||||
import { IQuest, IQuestComparison } from './types/QuestTypes';
|
||||
|
||||
export class Armory {
|
||||
public characterCustomization: CharacterCustomization;
|
||||
|
|
@ -81,6 +82,7 @@ export class Armory {
|
|||
|
||||
const locals = {
|
||||
aowow: this.config.aowowUrl,
|
||||
wowhead: this.config.wowheadUrl,
|
||||
websiteUrl: this.config.websiteUrl,
|
||||
websiteName: this.config.websiteName,
|
||||
websiteRoot: this.config.websiteRoot,
|
||||
|
|
@ -101,6 +103,45 @@ export class Armory {
|
|||
helpers: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
...require("handlebars-helpers")(),
|
||||
hasInProgressQuests: function(quests: IQuest[]): boolean {
|
||||
return quests.some(quest => quest.status === 'In Progress');
|
||||
},
|
||||
getDiffClass: function(quest: IQuestComparison): string {
|
||||
if (!quest.char1Status && !quest.char2Status) return '';
|
||||
if (!quest.char1Status) return 'quest-missing-1';
|
||||
if (!quest.char2Status) return 'quest-missing-2';
|
||||
if (quest.char1Status !== quest.char2Status) return 'quest-diff';
|
||||
return '';
|
||||
},
|
||||
getDiffCount: function(quests: IQuestComparison[]): number {
|
||||
return quests.reduce((count, quest) => {
|
||||
if (!quest.char1Status && quest.char2Status) return count + 1;
|
||||
if (quest.char1Status && !quest.char2Status) return count + 1;
|
||||
if (quest.char1Status !== quest.char2Status) return count + 1;
|
||||
return count;
|
||||
}, 0);
|
||||
},
|
||||
getActiveQuests: function(categories: { [key: string]: IQuestComparison[] }): (IQuestComparison & { category: string })[] {
|
||||
const activeQuests: (IQuestComparison & { category: string })[] = [];
|
||||
Object.entries(categories).forEach(([category, quests]) => {
|
||||
quests.forEach(quest => {
|
||||
if (quest.char1Status === 'In Progress' || quest.char2Status === 'In Progress') {
|
||||
activeQuests.push({
|
||||
...quest,
|
||||
category
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return activeQuests.sort((a, b) => {
|
||||
// First sort by category
|
||||
const categoryCompare = a.category.localeCompare(b.category);
|
||||
if (categoryCompare !== 0) return categoryCompare;
|
||||
|
||||
// Then by title within same category
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -150,9 +191,19 @@ export class Armory {
|
|||
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)));
|
||||
if (this.config.featureFlags.enableSkillsPage) {
|
||||
app.get("/character/:realm/:name/skills", this.wrapRoute(charsController.skills.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)));
|
||||
if (this.config.featureFlags.enableReputationPage) {
|
||||
app.get("/character/:realm/:name/reputation", this.wrapRoute(charsController.reputation.bind(charsController)));
|
||||
}
|
||||
if (this.config.featureFlags.enableQuestsPage) {
|
||||
app.get("/character/:realm/:name/quests", this.wrapRoute(charsController.quests.bind(charsController)));
|
||||
app.get("/character/:realm/:name/quests/compare/:otherRealm/:otherName", this.wrapRoute(charsController.questsCompare.bind(charsController)));
|
||||
}
|
||||
|
||||
const guildsController = new GuildController(this);
|
||||
app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController)));
|
||||
|
|
|
|||
|
|
@ -23,8 +23,15 @@ export interface IIframeModeConfig {
|
|||
url: string;
|
||||
}
|
||||
|
||||
export interface IFeatureFlags {
|
||||
enableQuestsPage: boolean;
|
||||
enableReputationPage: boolean;
|
||||
enableSkillsPage: boolean;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
public aowowUrl: string;
|
||||
public wowheadUrl: string;
|
||||
public websiteUrl: string;
|
||||
public websiteName: string;
|
||||
public websiteRoot: string;
|
||||
|
|
@ -36,6 +43,7 @@ export class Config {
|
|||
public realms: IRealmConfig[];
|
||||
public worldDatabase: IDatabaseConfig;
|
||||
public dbQueryTimeout: number;
|
||||
public featureFlags: IFeatureFlags;
|
||||
|
||||
private static envPrefix = "ACORE_ARMORY";
|
||||
private static checkedMissingField = false;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { RowDataPacket } from "mysql2/promise";
|
|||
import { Armory } from "../Armory";
|
||||
import { IRealmConfig } from "../Config";
|
||||
import { IEmblem, Utils } from "../Utils";
|
||||
import { IAchievement as IAchievementDbc } from "../data/DbcReader";
|
||||
import { IAchievement as IAchievementDbc, ISkillDbc, IAreas } from "../data/DbcReader";
|
||||
|
||||
interface ICharacterData {
|
||||
guid: number;
|
||||
|
|
@ -21,6 +21,11 @@ interface ICharacterData {
|
|||
playerFlags: number;
|
||||
online: number;
|
||||
guild: string;
|
||||
map: number;
|
||||
zone: number;
|
||||
zoneName: string;
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
}
|
||||
|
||||
interface IEquipmentData {
|
||||
|
|
@ -72,6 +77,33 @@ interface IArenaTeam {
|
|||
emblem?: IEmblem;
|
||||
}
|
||||
|
||||
interface ISkills {
|
||||
id: number;
|
||||
categoryId: number
|
||||
skill: string;
|
||||
value: number;
|
||||
max: number ;
|
||||
}
|
||||
|
||||
interface IReputation {
|
||||
id: number;
|
||||
name: string;
|
||||
standing: string;
|
||||
value: number;
|
||||
valueInGrade: number;
|
||||
max: number;
|
||||
expansionId: number;
|
||||
}
|
||||
|
||||
interface IQuest {
|
||||
id: number;
|
||||
title: string;
|
||||
status: 'Completed' | 'In Progress';
|
||||
minLevel: number;
|
||||
questLevel: number;
|
||||
questSortID: number;
|
||||
}
|
||||
|
||||
const ItemClassGem = 3;
|
||||
const SpellMechanicMounted = 21;
|
||||
const RaceDisplayName = {
|
||||
|
|
@ -101,6 +133,7 @@ const ClassDisplayName = {
|
|||
|
||||
export class CharacterController {
|
||||
private armory: Armory;
|
||||
private areaById: { [key: number]: IAreas };
|
||||
private itemInventoryTypes: { [key: number]: number };
|
||||
private itemIcons: { [key: number]: number };
|
||||
private gemItems: { [key: number]: boolean };
|
||||
|
|
@ -108,6 +141,7 @@ export class CharacterController {
|
|||
private itemSocketBonuses: { [key: number]: number };
|
||||
private mountSpells: number[];
|
||||
private mountBySpellId: { [key: number]: IMount };
|
||||
private skillById: { [key: number]: ISkillDbc };
|
||||
private achievementById: { [key: number]: IAchievementDbc };
|
||||
|
||||
public constructor(armory: Armory) {
|
||||
|
|
@ -180,6 +214,15 @@ export class CharacterController {
|
|||
for await (const achievement of this.armory.dbc.achievement()) {
|
||||
this.achievementById[achievement.id] = achievement;
|
||||
}
|
||||
|
||||
this.skillById = {};
|
||||
for await (const skill of this.armory.dbc.skill()) {
|
||||
this.skillById[skill.id] = skill;
|
||||
}
|
||||
this.areaById = {};
|
||||
for await (const area of this.armory.dbc.areas()) {
|
||||
this.areaById[area.id] = area;
|
||||
}
|
||||
}
|
||||
|
||||
public async character(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
|
|
@ -257,6 +300,413 @@ export class CharacterController {
|
|||
});
|
||||
}
|
||||
|
||||
public async skills(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
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 charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
return next(404);
|
||||
}
|
||||
const skills = await this.getSkills(realm.name, charData.guid);
|
||||
const professions = skills.filter((skill) => skill.categoryId === 11);
|
||||
const secondarySkills = skills.filter((skill) => skill.categoryId === 9);
|
||||
const weaponSkills = skills.filter((skill) => skill.categoryId === 6);
|
||||
const classSkills = skills.filter((skill) => skill.categoryId === 7);
|
||||
const armorSkills = skills.filter((skill) => skill.categoryId === 8);
|
||||
const languages = skills.filter((skill) => skill.categoryId === 10);
|
||||
res.render("character-skills.hbs", {
|
||||
title: `Armory - ${charData.name} - Skills`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: {
|
||||
skills: skills,
|
||||
professions: professions,
|
||||
secondarySkills: secondarySkills,
|
||||
weaponSkills: weaponSkills,
|
||||
classSkills: classSkills,
|
||||
armorSkills: armorSkills,
|
||||
languages: languages,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async reputation(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.armory.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const reputations = await this.getReputations(realm.name, charData.guid);
|
||||
// Group reputations by expansion/category
|
||||
const classicReps = reputations.filter(rep => rep.expansionId === 0);
|
||||
const tbcReps = reputations.filter(rep => rep.expansionId === 1);
|
||||
const wotlkReps = reputations.filter(rep => rep.expansionId === 2);
|
||||
|
||||
res.render("character-reputation.hbs", {
|
||||
title: `Armory - ${charData.name} - Reputation`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: {
|
||||
classic: classicReps,
|
||||
tbc: tbcReps,
|
||||
wotlk: wotlkReps
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async quests(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.armory.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const quests = await this.getQuests(realm.name, charData.guid);
|
||||
|
||||
// Group quests by zone/profession
|
||||
const questsByCategory = quests.reduce((acc, quest) => {
|
||||
const category = quest.questSortID > 0 ?
|
||||
this.getZoneName(quest.questSortID):
|
||||
this.getProfessionName(quest.questSortID);
|
||||
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(quest);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Get list of all characters
|
||||
const allCharacters = await this.getAllCharacters(realm.name);
|
||||
|
||||
res.render("character-quests.hbs", {
|
||||
title: `Armory - ${charData.name} - Quests`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: {
|
||||
categories: questsByCategory,
|
||||
otherCharacters: allCharacters.filter(c => c.guid !== charData.guid)
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async questsCompare(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
const otherRealmName = req.params.otherRealm;
|
||||
const otherCharName = req.params.otherName;
|
||||
|
||||
const realm = this.armory.getRealm(realmName);
|
||||
const otherRealm = this.armory.getRealm(otherRealmName);
|
||||
if (realm === undefined || otherRealm === undefined) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
const otherCharData = await this.getCharacterData(otherRealm, otherCharName);
|
||||
if (charData === null || otherCharData === null) {
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charQuests = await this.getQuests(realm.name, charData.guid);
|
||||
const otherQuests = await this.getQuests(otherRealm.name, otherCharData.guid);
|
||||
|
||||
// Group quests by category
|
||||
interface IQuestComparison {
|
||||
id: number;
|
||||
title: string;
|
||||
questLevel: number;
|
||||
char1Status?: 'Completed' | 'In Progress';
|
||||
char2Status?: 'Completed' | 'In Progress';
|
||||
}
|
||||
|
||||
const categories: { [key: string]: IQuestComparison[] } = {};
|
||||
|
||||
const addQuestsToCategories = (quests: IQuest[], source: 'char1Status' | 'char2Status') => {
|
||||
quests.forEach(quest => {
|
||||
const category = quest.questSortID > 0 ?
|
||||
this.getZoneName(quest.questSortID) :
|
||||
this.getProfessionName(quest.questSortID);
|
||||
|
||||
if (!categories[category]) {
|
||||
categories[category] = [];
|
||||
}
|
||||
|
||||
const existingQuest = categories[category].find(q => q.id === quest.id);
|
||||
if (existingQuest) {
|
||||
existingQuest[source] = quest.status;
|
||||
// Remove quest if both characters have completed it
|
||||
if (existingQuest.char1Status === 'Completed' && existingQuest.char2Status === 'Completed') {
|
||||
categories[category] = categories[category].filter(q => q.id !== quest.id);
|
||||
}
|
||||
} else {
|
||||
categories[category].push({
|
||||
id: quest.id,
|
||||
title: quest.title,
|
||||
questLevel: quest.questLevel,
|
||||
[source]: quest.status
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addQuestsToCategories(charQuests, 'char1Status');
|
||||
addQuestsToCategories(otherQuests, 'char2Status');
|
||||
|
||||
// Get list of all characters
|
||||
const allCharacters = await this.getAllCharacters(realm.name);
|
||||
|
||||
res.render("character-quests-compare.hbs", {
|
||||
title: `Armory - Quest Compare - ${charData.name} vs ${otherCharData.name}`,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
data: {
|
||||
categories: categories,
|
||||
char1: {
|
||||
name: charData.name,
|
||||
realm: realm.name
|
||||
},
|
||||
char2: {
|
||||
name: otherCharData.name,
|
||||
realm: otherRealm.name
|
||||
},
|
||||
otherCharacters: allCharacters.filter(c => c.guid !== charData.guid)
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getReputations(realm: string, character: number): Promise<IReputation[]> {
|
||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
||||
sql: `
|
||||
SELECT faction, standing, flags
|
||||
FROM character_reputation
|
||||
WHERE guid = ?
|
||||
AND flags NOT IN (0, 4, 8)
|
||||
AND standing != 0
|
||||
`,
|
||||
values: [character],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
const reputations = [];
|
||||
for (const row of rows as RowDataPacket[]) {
|
||||
const factionInfo = await this.armory.dbc.faction().find(f => f.id === row.faction);
|
||||
if (factionInfo && factionInfo.reputationId >= 0) {
|
||||
reputations.push({
|
||||
id: row.faction,
|
||||
name: factionInfo.name,
|
||||
standing: this.getReputationStanding(row.standing),
|
||||
value: row.standing,
|
||||
valueInGrade: this.getReputationInGrade(row.standing),
|
||||
max: this.getReputationMax(row.standing),
|
||||
expansionId: this.getExpansionId(row.faction)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return reputations;
|
||||
}
|
||||
|
||||
private getReputationStanding(value: number): string {
|
||||
if (value < -6000) return 'Hated'; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < -3000) return 'Hostile'; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 0) return 'Unfriendly'; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 1000) return 'Neutral'; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 6000) return 'Friendly';
|
||||
if (value < 19000) return 'Honored';
|
||||
if (value < 40000) return 'Revered';
|
||||
return 'Exalted'; // These are probably not correct, but I don't have a character to test against
|
||||
}
|
||||
|
||||
private getReputationMax(value: number): number {
|
||||
if (value < -6000) return -6000; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < -3000) return -3000; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 0) return 0; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 1000) return 3000;
|
||||
if (value < 6000) return 6000;
|
||||
if (value < 12000) return 12000;
|
||||
if (value < 21000) return 21000;
|
||||
return 40000; // This might not be right, but I dont have an exalted character to test against
|
||||
}
|
||||
|
||||
private getReputationInGrade(value: number): number {
|
||||
if (value < -6000) return value + 3000; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < -3000) return value; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 0) return value; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 1000) return value + 2000; // These are probably not correct, but I don't have a character to test against
|
||||
if (value < 6000) return value - 1000;
|
||||
if (value < 12000) return value - 5900; // For some reason I needed to remove an extra 100 from this one to get it to match the client
|
||||
if (value < 21000) return value - 16900; // For some reason I needed to remove an extra 100 from this one to get it to match the client
|
||||
return value - 21100; // This might not be right, but I dont have an exalted character to test against
|
||||
}
|
||||
|
||||
private getExpansionId(factionId: number): number {
|
||||
// Classic factions
|
||||
if (factionId < 900) return 0;
|
||||
// TBC factions
|
||||
if (factionId < 1100) return 1;
|
||||
// WotLK factions
|
||||
return 2;
|
||||
}
|
||||
|
||||
private async getQuests(realm: string, character: number): Promise<IQuest[]> {
|
||||
// Get completed and rewarded quests
|
||||
const [completedRows] = await this.armory.getCharactersDb(realm).query({
|
||||
sql: `
|
||||
SELECT quest FROM (
|
||||
SELECT quest FROM character_queststatus
|
||||
WHERE guid = ? AND status = 1
|
||||
UNION
|
||||
SELECT quest FROM character_queststatus_rewarded
|
||||
WHERE guid = ?
|
||||
) AS completed_quests
|
||||
`,
|
||||
values: [character, character],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
// Get in progress quests
|
||||
const [inProgressRows] = await this.armory.getCharactersDb(realm).query({
|
||||
sql: `
|
||||
SELECT quest
|
||||
FROM character_queststatus
|
||||
WHERE guid = ? AND status = 3
|
||||
`,
|
||||
values: [character],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
const quests: IQuest[] = [];
|
||||
|
||||
// Process completed quests
|
||||
for (const row of completedRows as RowDataPacket[]) {
|
||||
const questInfo = await this.getQuestInfo(row.quest);
|
||||
if (questInfo) {
|
||||
quests.push({
|
||||
id: row.quest,
|
||||
title: questInfo.title,
|
||||
status: 'Completed',
|
||||
minLevel: questInfo.minLevel,
|
||||
questLevel: questInfo.questLevel,
|
||||
questSortID: questInfo.questSortID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process in progress quests
|
||||
for (const row of inProgressRows as RowDataPacket[]) {
|
||||
const questInfo = await this.getQuestInfo(row.quest);
|
||||
if (questInfo) {
|
||||
quests.push({
|
||||
id: row.quest,
|
||||
title: questInfo.title,
|
||||
status: 'In Progress',
|
||||
minLevel: questInfo.minLevel,
|
||||
questLevel: questInfo.questLevel,
|
||||
questSortID: questInfo.questSortID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return quests;
|
||||
}
|
||||
|
||||
private async getQuestInfo(questId: number): Promise<IQuest> {
|
||||
const [rows] = await this.armory.worldDb.query({
|
||||
sql: `
|
||||
SELECT ID, LogTitle as title, MinLevel as minLevel, QuestLevel as questLevel, QuestSortID as questSortID
|
||||
FROM quest_template
|
||||
WHERE ID = ?
|
||||
`,
|
||||
values: [questId],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
private getZoneName(zoneId: number): string {
|
||||
this.areaById[zoneId]?.zoneName;
|
||||
return this.areaById[zoneId]?.zoneName || `Zone ${zoneId}`;
|
||||
}
|
||||
|
||||
private getProfessionName(professionId: number): string {
|
||||
const questTypes = {
|
||||
// Negative IDs (Classes and Professions)
|
||||
// Classes
|
||||
"-61": "Warlock",
|
||||
"-81": "Warrior",
|
||||
"-82": "Shaman",
|
||||
"-141": "Paladin",
|
||||
"-161": "Mage",
|
||||
"-162": "Rogue",
|
||||
"-261": "Hunter",
|
||||
"-262": "Priest",
|
||||
"-263": "Druid",
|
||||
"-372": "Death Knight",
|
||||
// Professions
|
||||
"-24": "Herbalism",
|
||||
"-101": "Fishing",
|
||||
"-121": "Blacksmithing",
|
||||
"-181": "Alchemy",
|
||||
"-182": "Leatherworking",
|
||||
"-201": "Engineering",
|
||||
"-264": "Tailoring",
|
||||
"-304": "Cooking",
|
||||
"-324": "First Aid",
|
||||
"-371": "Inscription",
|
||||
"-373": "Jewelcrafting",
|
||||
"-762": "Riding",
|
||||
// Misc
|
||||
"-1": "Epic",
|
||||
"-21": "Wailing Caverns",
|
||||
"-22": "Seasonal",
|
||||
"-23": "Undercity",
|
||||
"-25": "Battlegrounds",
|
||||
"-41": "Uldaman",
|
||||
"-221": "Treasure Map",
|
||||
"-241": "Tournament",
|
||||
"-284": "Special",
|
||||
"-344": "Legendary",
|
||||
"-364": "Darkmoon Faire",
|
||||
"-365": "Ahn'Qiraj War",
|
||||
"-366": "Lunar Festival",
|
||||
"-367": "Reputation",
|
||||
"-368": "Invasion",
|
||||
"-369": "Midsummer",
|
||||
"-370": "Brewfest",
|
||||
"-374": "Noblegarden",
|
||||
"-375": "Pilgrim's Bounty",
|
||||
"-376": "Love is in the Air"
|
||||
};
|
||||
return questTypes[professionId] || `Category ${professionId}`;
|
||||
}
|
||||
|
||||
private getQuestExpansionId(questLevel: number): number {
|
||||
if (questLevel <= 60) return 0; // Classic
|
||||
if (questLevel <= 70) return 1; // TBC
|
||||
return 2; // WotLK
|
||||
}
|
||||
|
||||
public async achievements(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
|
@ -337,6 +787,11 @@ export class CharacterController {
|
|||
level: charData.level,
|
||||
online: charData.online === 1,
|
||||
guild: charData.guild,
|
||||
zone: charData.zone,
|
||||
zoneName: this.getZoneName(charData.zone),
|
||||
enableQuestsPage: this.armory.config.featureFlags.enableQuestsPage,
|
||||
enableSkillsPage: this.armory.config.featureFlags.enableSkillsPage,
|
||||
enableReputationPage: this.armory.config.featureFlags.enableReputationPage
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +799,7 @@ export class CharacterController {
|
|||
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
|
||||
const [rows] = 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\`
|
||||
SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`map\`, \`zone\`, \`position_x\`, \`position_y\`, \`guild\`.\`name\` AS \`guild\`
|
||||
FROM \`characters\`
|
||||
LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\`
|
||||
LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\`
|
||||
|
|
@ -968,6 +1423,31 @@ export class CharacterController {
|
|||
return talents;
|
||||
}
|
||||
|
||||
private async getSkills(realm: string, character: number): Promise<ISkills[]> {
|
||||
const [rows] = await this.armory.getCharactersDb(realm).query({
|
||||
sql: `
|
||||
SELECT skill, value, max
|
||||
FROM character_skills
|
||||
WHERE guid = ?
|
||||
`,
|
||||
values: [character],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
const skills: { id: number, categoryId: number, skill: string; value: number; max: number }[] = [];
|
||||
for (const row of rows as RowDataPacket[]) {
|
||||
skills.push({
|
||||
id: row.skill,
|
||||
categoryId: this.skillById[row.skill].categoryId,
|
||||
skill: this.skillById[row.skill].name,
|
||||
value: row.value,
|
||||
max: row.max,
|
||||
});
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
private async getTalentTrees(classId: number) {
|
||||
const items = await this.armory.dbc
|
||||
.talentTab()
|
||||
|
|
@ -1103,4 +1583,17 @@ export class CharacterController {
|
|||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
private async getAllCharacters(currentRealm: string): Promise<Array<{name: string, realmName: string, guid: number}>> {
|
||||
const [rows] = await this.armory.getCharactersDb(currentRealm).query({
|
||||
sql: 'SELECT name, guid FROM characters WHERE level > 1 order by name asc',
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
|
||||
return (rows as RowDataPacket[]).map(row => ({
|
||||
name: row.name,
|
||||
guid: row.guid,
|
||||
realmName: currentRealm
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@ export interface IMountXDisplayDbc {
|
|||
mountId: number;
|
||||
}
|
||||
|
||||
export interface ISkillDbc {
|
||||
id: number;
|
||||
categoryId: number;
|
||||
skillCostId: number;
|
||||
name: string;
|
||||
spellIcon: number;
|
||||
altVerb: string;
|
||||
canLink: number;
|
||||
}
|
||||
|
||||
export interface ISpellDbc {
|
||||
id: number;
|
||||
mechanic: number;
|
||||
|
|
@ -102,6 +112,19 @@ export interface ITalentTab {
|
|||
classMask: number;
|
||||
}
|
||||
|
||||
export interface IFactionDbc {
|
||||
id: number;
|
||||
reputationId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IAreas {
|
||||
id: number;
|
||||
zoneName: string;
|
||||
mapId: number;
|
||||
areaId: number;
|
||||
}
|
||||
|
||||
interface IAsyncGeneratorWithArrayMethods<T> {
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T>;
|
||||
toArray(): Promise<T[]>;
|
||||
|
|
@ -305,6 +328,8 @@ const dir = path.join(process.cwd(), "data");
|
|||
export const DbcFiles = {
|
||||
achievement: path.join(dir, "Achievement_3.3.5_12340.csv"),
|
||||
achievementCategory: path.join(dir, "AchievementCategory_3.3.5_12340.csv"),
|
||||
areas: path.join(dir, "Areas.csv"),
|
||||
faction: path.join(dir, "Factions.csv"),
|
||||
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"),
|
||||
|
|
@ -313,6 +338,7 @@ export const DbcFiles = {
|
|||
itemDisplayInfo: path.join(dir, "ItemDisplayInfo_3.3.5_12340.csv"),
|
||||
mount: path.join(dir, "Mount_9.2.0_41462.csv"),
|
||||
mountDisplay: path.join(dir, "MountXDisplay_9.2.0_41462.csv"),
|
||||
skill: path.join(dir, "Skills.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"),
|
||||
|
|
@ -323,6 +349,7 @@ export const DbcFiles = {
|
|||
const dbcFields = {
|
||||
achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"],
|
||||
achievementCategory: ["id", "parent", "nameLang0"],
|
||||
areas: ["id", "zoneName", "mapId", "areaId"],
|
||||
glyphProperties: ["id", "spellId"],
|
||||
item: ["id", "classId", "subclassId", "displayInfoId", "inventoryType"],
|
||||
itemRetail: ["id", "inventoryType"],
|
||||
|
|
@ -331,6 +358,7 @@ const dbcFields = {
|
|||
itemDisplayInfo: ["id", "inventoryIcon0"],
|
||||
mount: ["id", "sourceSpellId"],
|
||||
mountDisplay: ["id", "creatureDisplayInfoId", "mountId"],
|
||||
skill: ["id", "name"],
|
||||
spell: ["id", "mechanic", "spellIconId"],
|
||||
spellItemEnchantment: ["id", "srcItemId"],
|
||||
spellIcon: ["id", "textureFilename"],
|
||||
|
|
@ -348,12 +376,15 @@ const dbcFields = {
|
|||
"prereqRank0",
|
||||
],
|
||||
talentTab: ["id", "nameLang0", "spellIconId", "classMask"],
|
||||
faction: ["id", "reputationId", "name"],
|
||||
};
|
||||
|
||||
export class DbcManager {
|
||||
private _achievement: IAchievement[];
|
||||
private _achievementCategory: IAchievementCategory[];
|
||||
private _areas: IAreas[];
|
||||
private _glyphProperties: IGlyphProperties[];
|
||||
private _faction: IFactionDbc[];
|
||||
private _item: IItemDbc[];
|
||||
private _itemRetail: IItemRetailDbc[];
|
||||
private _itemAppearance: IItemAppearanceDbc[];
|
||||
|
|
@ -361,6 +392,7 @@ export class DbcManager {
|
|||
private _itemDisplayInfo: IItemDisplayInfoDbc[];
|
||||
private _mount: IMountDbc[];
|
||||
private _mountDisplay: IMountXDisplayDbc[];
|
||||
private _skill: ISkillDbc[];
|
||||
private _spell: ISpellDbc[];
|
||||
private _spellItemEnchantment: ISpellItemEnchantmentDbc[];
|
||||
private _spellIcon: ISpellIcon[];
|
||||
|
|
@ -373,6 +405,7 @@ export class DbcManager {
|
|||
DbcFiles.achievementCategory,
|
||||
dbcFields.achievementCategory,
|
||||
).toArray();
|
||||
this._areas = await this.read<IAreas>(DbcFiles.areas).toArray();
|
||||
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
||||
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
||||
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
||||
|
|
@ -384,6 +417,7 @@ export class DbcManager {
|
|||
this._itemDisplayInfo = await this.read<IItemDisplayInfoDbc>(DbcFiles.itemDisplayInfo, dbcFields.itemDisplayInfo).toArray();
|
||||
this._mount = await this.read<IMountDbc>(DbcFiles.mount, dbcFields.mount).toArray();
|
||||
this._mountDisplay = await this.read<IMountXDisplayDbc>(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray();
|
||||
this._skill = await this.read<ISkillDbc>(DbcFiles.skill).toArray();
|
||||
this._spell = await this.read<ISpellDbc>(DbcFiles.spell, dbcFields.spell).toArray();
|
||||
this._spellItemEnchantment = await this.read<ISpellItemEnchantmentDbc>(
|
||||
DbcFiles.spellItemEnchantment,
|
||||
|
|
@ -402,6 +436,14 @@ export class DbcManager {
|
|||
return this.getLoadedDataOrRead(DbcFiles.achievementCategory, this._achievementCategory, dbcFields.achievementCategory);
|
||||
}
|
||||
|
||||
public areas() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.areas, this._areas);
|
||||
}
|
||||
|
||||
public faction() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.faction, this._faction, dbcFields.faction);
|
||||
}
|
||||
|
||||
public glyphProperties() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.glyphProperties, this._glyphProperties, dbcFields.glyphProperties);
|
||||
}
|
||||
|
|
@ -434,6 +476,10 @@ export class DbcManager {
|
|||
return this.getLoadedDataOrRead(DbcFiles.mountDisplay, this._mountDisplay, dbcFields.mountDisplay);
|
||||
}
|
||||
|
||||
public skill() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.skill, this._skill, dbcFields.skill);
|
||||
}
|
||||
|
||||
public spell() {
|
||||
return this.getLoadedDataOrRead(DbcFiles.spell, this._spell, dbcFields.spell);
|
||||
}
|
||||
|
|
|
|||
17
src/armory/types/QuestTypes.ts
Normal file
17
src/armory/types/QuestTypes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export interface IQuest {
|
||||
id: number;
|
||||
title: string;
|
||||
status: 'Completed' | 'In Progress';
|
||||
minLevel: number;
|
||||
questLevel: number;
|
||||
questSortID: number;
|
||||
}
|
||||
|
||||
export interface IQuestComparison {
|
||||
id: number;
|
||||
title: string;
|
||||
questLevel: number;
|
||||
char1Status?: 'Completed' | 'In Progress';
|
||||
char2Status?: 'Completed' | 'In Progress';
|
||||
category?: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue