feat(character/achievements): add achievements page
This commit is contained in:
parent
ed424d63d0
commit
ab3b84f743
24 changed files with 2419 additions and 33 deletions
87
data/AchievementCategory_3.3.5_12340.csv
Normal file
87
data/AchievementCategory_3.3.5_12340.csv
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
ID,Parent,Name_lang[0],Name_lang[1],Name_lang[2],Name_lang[3],Name_lang[4],Name_lang[5],Name_lang[6],Name_lang[7],Name_lang[8],Name_lang[9],Name_lang[10],Name_lang[11],Name_lang[12],Name_lang[13],Name_lang[14],Name_lang[15],Name_lang_mask,Ui_order
|
||||||
|
92,-1,General,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
123,122,Arenas,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
130,1,Character,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
135,128,Creatures,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
140,130,Wealth,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
152,21,Rated Arenas,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
160,155,Lunar Festival,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
165,95,Arena,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
170,169,Cooking,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
178,132,Secondary Skills,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
14777,97,Eastern Kingdoms,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
14808,168,Classic,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
14821,14807,Classic,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
14861,96,Classic,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
14864,201,Classic,,,,,,,,,,,,,,,,16712190,1
|
||||||
|
96,-1,Quests,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
124,122,Battlegrounds,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
136,128,Honorable Kills,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
141,1,Combat,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
145,130,Consumables,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
153,21,Battlegrounds,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
171,169,Fishing,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
173,132,Professions,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
187,155,Love is in the Air,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14778,97,Kalimdor,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14801,95,Alterac Valley,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14805,168,The Burning Crusade,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14822,14807,The Burning Crusade,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14862,96,The Burning Crusade,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
14865,201,The Burning Crusade,,,,,,,,,,,,,,,,16712190,2
|
||||||
|
97,-1,Exploration,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
125,122,Dungeons,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
128,1,Kills,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
137,128,Killing Blows,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
147,130,Reputation,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
154,21,World,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
159,155,Noblegarden,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
172,169,First Aid,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14779,97,Outland,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14802,95,Arathi Basin,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14806,168,Lich King Dungeon,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14823,14807,Wrath of the Lich King,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14863,96,Wrath of the Lich King,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
14866,201,Wrath of the Lich King,,,,,,,,,,,,,,,,16712190,3
|
||||||
|
95,-1,Player vs. Player,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
122,1,Deaths,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
126,122,World,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
163,155,Children's Week,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
191,130,Gear,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
14780,97,Northrend,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
14803,95,Eye of the Storm,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
14921,168,Lich King Heroic,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
14963,14807,Secrets of Ulduar,,,,,,,,,,,,,,,,16712190,4
|
||||||
|
127,122,Resurrection,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
133,1,Quests,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
161,155,Midsummer,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
168,-1,Dungeons & Raids,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
14804,95,Warsong Gulch,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
14922,168,Lich King 10-Player Raid,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
15021,14807,Call of the Crusade,,,,,,,,,,,,,,,,16712190,5
|
||||||
|
162,155,Brewfest,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
169,-1,Professions,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
14807,1,Dungeons & Raids,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
14881,95,Strand of the Ancients,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
14923,168,Lich King 25-Player Raid,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
15062,14807,Fall of the Lich King,,,,,,,,,,,,,,,,16712190,6
|
||||||
|
132,1,Skills,,,,,,,,,,,,,,,,16712190,7
|
||||||
|
158,155,Hallow's End,,,,,,,,,,,,,,,,16712190,7
|
||||||
|
201,-1,Reputation,,,,,,,,,,,,,,,,16712190,7
|
||||||
|
14901,95,Wintergrasp,,,,,,,,,,,,,,,,16712190,7
|
||||||
|
14961,168,Secrets of Ulduar 10-Player Raid,,,,,,,,,,,,,,,,16712190,7
|
||||||
|
134,1,Travel,,,,,,,,,,,,,,,,16712190,8
|
||||||
|
155,-1,World Events,,,,,,,,,,,,,,,,16712190,8
|
||||||
|
14962,168,Secrets of Ulduar 25-Player Raid,,,,,,,,,,,,,,,,16712190,8
|
||||||
|
14981,155,Pilgrim's Bounty,,,,,,,,,,,,,,,,16712190,8
|
||||||
|
15003,95,Isle of Conquest,,,,,,,,,,,,,,,,16712190,8
|
||||||
|
81,-1,Feats of Strength,,,,,,,,,,,,,,,,16712190,9
|
||||||
|
131,1,Social,,,,,,,,,,,,,,,,16712190,9
|
||||||
|
156,155,Winter Veil,,,,,,,,,,,,,,,,16712190,9
|
||||||
|
15001,168,Call of the Crusade 10-Player Raid,,,,,,,,,,,,,,,,16712190,9
|
||||||
|
1,-1,Statistics,,,,,,,,,,,,,,,,16712190,10
|
||||||
|
21,1,Player vs. Player,,,,,,,,,,,,,,,,16712190,10
|
||||||
|
14941,155,Argent Tournament,,,,,,,,,,,,,,,,16712190,10
|
||||||
|
15002,168,Call of the Crusade 25-Player Raid,,,,,,,,,,,,,,,,16712190,10
|
||||||
|
15041,168,Fall of the Lich King 10-Player Raid,,,,,,,,,,,,,,,,16712190,11
|
||||||
|
15042,168,Fall of the Lich King 25-Player Raid,,,,,,,,,,,,,,,,16712190,12
|
||||||
|
1818
data/Achievement_3.3.5_12340.csv
Normal file
1818
data/Achievement_3.3.5_12340.csv
Normal file
File diff suppressed because it is too large
Load diff
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "azerothcore-armory",
|
"name": "azerothcore-armory",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "azerothcore-armory",
|
"name": "azerothcore-armory",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supercharge/promise-pool": "^2.1.0",
|
"@supercharge/promise-pool": "^2.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "azerothcore-armory",
|
"name": "azerothcore-armory",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ export class Armory {
|
||||||
await charsController.load();
|
await charsController.load();
|
||||||
app.get("/character/:realm/:name", charsController.character.bind(charsController));
|
app.get("/character/:realm/:name", charsController.character.bind(charsController));
|
||||||
app.get("/character/:realm/:name/talents", charsController.talents.bind(charsController));
|
app.get("/character/:realm/:name/talents", charsController.talents.bind(charsController));
|
||||||
|
app.get("/character/:realm/:name/achievements", charsController.achievements.bind(charsController));
|
||||||
|
app.get("/character/:realm/:character/achievements/data", charsController.achievementsData.bind(charsController));
|
||||||
|
|
||||||
this.gc();
|
this.gc();
|
||||||
app.listen(listenPort, "0.0.0.0", () => {
|
app.listen(listenPort, "0.0.0.0", () => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { RowDataPacket } from "mysql2/promise";
|
||||||
|
|
||||||
import { Armory } from "../Armory";
|
import { Armory } from "../Armory";
|
||||||
import { IRealmConfig } from "../Config";
|
import { IRealmConfig } from "../Config";
|
||||||
|
import { IAchievement } from "../data/DbcReader";
|
||||||
|
|
||||||
interface ICharacterData {
|
interface ICharacterData {
|
||||||
guid: number;
|
guid: number;
|
||||||
|
|
@ -75,6 +76,7 @@ export class CharacterController {
|
||||||
private itemSocketBonuses: { [key: number]: number };
|
private itemSocketBonuses: { [key: number]: number };
|
||||||
private mountSpells: number[];
|
private mountSpells: number[];
|
||||||
private mountBySpellId: { [key: number]: IMount };
|
private mountBySpellId: { [key: number]: IMount };
|
||||||
|
private achievementById: { [key: number]: IAchievement };
|
||||||
|
|
||||||
public constructor(armory: Armory) {
|
public constructor(armory: Armory) {
|
||||||
this.armory = armory;
|
this.armory = armory;
|
||||||
|
|
@ -137,6 +139,11 @@ export class CharacterController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.achievementById = {};
|
||||||
|
for await (const achievement of this.armory.dbc.achievement()) {
|
||||||
|
this.achievementById[achievement.id] = achievement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async character(req: express.Request, res: express.Response): Promise<void> {
|
public async character(req: express.Request, res: express.Response): Promise<void> {
|
||||||
|
|
@ -214,10 +221,59 @@ export class CharacterController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async achievements(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-achievements.html", {
|
||||||
|
title: `Armory - ${charName} - Achievements`,
|
||||||
|
...this.makeSharedDataObject(realm, charData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async achievementsData(req: express.Request, res: express.Response): Promise<void> {
|
||||||
|
const realmName = req.params.realm;
|
||||||
|
const character = parseInt(req.params.character) || -1;
|
||||||
|
|
||||||
|
const realm = this.getRealm(realmName);
|
||||||
|
if (realm === undefined) {
|
||||||
|
// Could not find realm
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const charData = await this.getCharacterData(realm.name, character);
|
||||||
|
if (charData === null) {
|
||||||
|
// Could not find character
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
categories: await this.armory.dbc.achievementCategory().toArray(),
|
||||||
|
...await this.getAchievements(realm.name, charData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private makeSharedDataObject(realm: IRealmConfig, charData: ICharacterData) {
|
private makeSharedDataObject(realm: IRealmConfig, charData: ICharacterData) {
|
||||||
return {
|
return {
|
||||||
realm: realm.name,
|
realm: realm.name,
|
||||||
character: charData.name,
|
name: charData.name,
|
||||||
|
guid: charData.guid,
|
||||||
race: RaceDisplayName[charData.race],
|
race: RaceDisplayName[charData.race],
|
||||||
class: ClassDisplayName[charData.class],
|
class: ClassDisplayName[charData.class],
|
||||||
level: charData.level,
|
level: charData.level,
|
||||||
|
|
@ -229,12 +285,13 @@ export class CharacterController {
|
||||||
return this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
|
return this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCharacterData(realm: string, charName: string): Promise<ICharacterData> {
|
private async getCharacterData(realm: string, character: string | number): Promise<ICharacterData> {
|
||||||
|
const where = typeof character === "string" ? "LOWER(name) = LOWER(?)" : "guid = ?";
|
||||||
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||||
SELECT guid, name, race, class, gender, level, skin, face, hairStyle, hairColor, facialStyle, playerFlags, online
|
SELECT guid, name, race, class, gender, level, skin, face, hairStyle, hairColor, facialStyle, playerFlags, online
|
||||||
FROM characters
|
FROM characters
|
||||||
WHERE LOWER(name) = LOWER(?)
|
WHERE ${where}
|
||||||
`, [charName]);
|
`, [character]);
|
||||||
|
|
||||||
if ((rows as RowDataPacket[]).length === 0) {
|
if ((rows as RowDataPacket[]).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -655,4 +712,41 @@ export class CharacterController {
|
||||||
|
|
||||||
return glyphs;
|
return glyphs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getAchievements(realm: string, charData: ICharacterData): Promise<{ achievements: any[]; earned: { [key: number]: any }; }> {
|
||||||
|
const faction = [1, 3, 4, 7, 11].includes(charData.race) ? 1 : 0;
|
||||||
|
|
||||||
|
const promises = await this.armory.dbc.achievement()
|
||||||
|
.filter(ach => ach.faction === -1 || ach.faction === faction)
|
||||||
|
.map(async (ach) => {
|
||||||
|
const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === ach.iconId);
|
||||||
|
return {
|
||||||
|
id: ach.id,
|
||||||
|
category: ach.category,
|
||||||
|
title: ach.titleLang0,
|
||||||
|
description: ach.descriptionLang0,
|
||||||
|
points: ach.points,
|
||||||
|
icon: this.processSpellIconTexture(icon?.textureFilename ?? ""),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
const achievements = await Promise.all(promises);
|
||||||
|
|
||||||
|
const [rows, fields] = await this.armory.getCharactersDb(realm).query(`
|
||||||
|
SELECT achievement, date
|
||||||
|
FROM character_achievement
|
||||||
|
WHERE guid = ?
|
||||||
|
`, [charData.guid]);
|
||||||
|
const earned = {};
|
||||||
|
for (const row of rows as RowDataPacket[]) {
|
||||||
|
earned[row.achievement] = {
|
||||||
|
date: row.date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
achievements,
|
||||||
|
earned,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,23 @@ export interface IGlyphProperties {
|
||||||
spellId: number;
|
spellId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAchievement {
|
||||||
|
id: number;
|
||||||
|
faction: number;
|
||||||
|
titleLang0: string;
|
||||||
|
descriptionLang0: string;
|
||||||
|
category: number;
|
||||||
|
points: number;
|
||||||
|
flags: number;
|
||||||
|
iconId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAchievementCategory {
|
||||||
|
id: number;
|
||||||
|
parent: number;
|
||||||
|
nameLang0: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IItemDbc {
|
export interface IItemDbc {
|
||||||
id: number;
|
id: number;
|
||||||
classId: number;
|
classId: number;
|
||||||
|
|
@ -203,11 +220,7 @@ class DbcReader<T> {
|
||||||
.map(header => camelCase(header).replace(/[\[\]]/g, ""));
|
.map(header => camelCase(header).replace(/[\[\]]/g, ""));
|
||||||
|
|
||||||
for await (const arr of itr) {
|
for await (const arr of itr) {
|
||||||
const cols = arr
|
const cols = arr.map(value => isNaN(value as any) ? value : parseInt(value, 10));
|
||||||
.map(value => {
|
|
||||||
const parsed: number = parseInt(value, 10);
|
|
||||||
return isNaN(parsed) ? value : parsed;
|
|
||||||
});
|
|
||||||
const row = {};
|
const row = {};
|
||||||
headerCols.forEach((header, headerIdx) => {
|
headerCols.forEach((header, headerIdx) => {
|
||||||
if (this.fields.length === 0 || this.fields.includes(header)) {
|
if (this.fields.length === 0 || this.fields.includes(header)) {
|
||||||
|
|
@ -282,6 +295,8 @@ class DbcReader<T> {
|
||||||
|
|
||||||
const dir = path.join(process.cwd(), "data");
|
const dir = path.join(process.cwd(), "data");
|
||||||
export const DbcFiles = {
|
export const DbcFiles = {
|
||||||
|
achievement: path.join(dir, "Achievement_3.3.5_12340.csv"),
|
||||||
|
achievementCategory: path.join(dir, "AchievementCategory_3.3.5_12340.csv"),
|
||||||
glyphProperties: path.join(dir, "GlyphProperties_3.3.5_12340.csv"),
|
glyphProperties: path.join(dir, "GlyphProperties_3.3.5_12340.csv"),
|
||||||
item: path.join(dir, "Item_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"),
|
itemRetail: path.join(dir, "Item_9.2.0_41462.csv"),
|
||||||
|
|
@ -298,6 +313,8 @@ export const DbcFiles = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbcFields = {
|
const dbcFields = {
|
||||||
|
achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"],
|
||||||
|
achievementCategory: ["id", "parent", "nameLang0"],
|
||||||
glyphProperties: ["id", "spellId"],
|
glyphProperties: ["id", "spellId"],
|
||||||
item: ["id", "classId", "displayInfoId", "inventoryType"],
|
item: ["id", "classId", "displayInfoId", "inventoryType"],
|
||||||
itemRetail: ["id", "inventoryType"],
|
itemRetail: ["id", "inventoryType"],
|
||||||
|
|
@ -314,6 +331,8 @@ const dbcFields = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DbcManager {
|
export class DbcManager {
|
||||||
|
private _achievement: IAchievement[];
|
||||||
|
private _achievementCategory: IAchievementCategory[];
|
||||||
private _glyphProperties: IGlyphProperties[];
|
private _glyphProperties: IGlyphProperties[];
|
||||||
private _item: IItemDbc[];
|
private _item: IItemDbc[];
|
||||||
private _itemRetail: IItemRetailDbc[];
|
private _itemRetail: IItemRetailDbc[];
|
||||||
|
|
@ -329,6 +348,8 @@ export class DbcManager {
|
||||||
private _talentTab: ITalentTab[];
|
private _talentTab: ITalentTab[];
|
||||||
|
|
||||||
public async loadAllFiles(): Promise<void> {
|
public async loadAllFiles(): Promise<void> {
|
||||||
|
this._achievement = await this.read<IAchievement>(DbcFiles.achievement, dbcFields.achievement).toArray();
|
||||||
|
this._achievementCategory = await this.read<IAchievementCategory>(DbcFiles.achievementCategory, dbcFields.achievementCategory).toArray();
|
||||||
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
this._glyphProperties = await this.read<IGlyphProperties>(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray();
|
||||||
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
this._item = await this.read<IItemDbc>(DbcFiles.item, dbcFields.item).toArray();
|
||||||
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
this._itemRetail = await this.read<IItemRetailDbc>(DbcFiles.itemRetail, dbcFields.itemRetail).toArray();
|
||||||
|
|
@ -344,6 +365,14 @@ export class DbcManager {
|
||||||
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();
|
this._talentTab = await this.read<ITalentTab>(DbcFiles.talentTab, dbcFields.talentTab).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public achievement() {
|
||||||
|
return this.getLoadedDataOrRead(DbcFiles.achievement, this._achievement, dbcFields.achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public achievementCategory() {
|
||||||
|
return this.getLoadedDataOrRead(DbcFiles.achievementCategory, this._achievementCategory, dbcFields.achievementCategory);
|
||||||
|
}
|
||||||
|
|
||||||
public glyphProperties() {
|
public glyphProperties() {
|
||||||
return this.getLoadedDataOrRead(DbcFiles.glyphProperties, this._glyphProperties, dbcFields.glyphProperties);
|
return this.getLoadedDataOrRead(DbcFiles.glyphProperties, this._glyphProperties, dbcFields.glyphProperties);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
243
static/character-achievements.html
Normal file
243
static/character-achievements.html
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/character-achievements.css">
|
||||||
|
{{> icons }}
|
||||||
|
<script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script>
|
||||||
|
|
||||||
|
{{> character-header }}
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column has-text-centered title is-size-3">
|
||||||
|
<span>Achievement Points:</span>
|
||||||
|
<span id="total-points"></span>
|
||||||
|
<img id="total-points-icon" src="/img/UI-Achievement-TinyShield.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-one-fifth-desktop is-full-tablet">
|
||||||
|
<div id="categories"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-full-tablet-only">
|
||||||
|
<div class="has-text-centered summary title is-size-4">Recent Achievements</div>
|
||||||
|
|
||||||
|
<table id="achievements">
|
||||||
|
<tr id="achievement-template" class="achievement">
|
||||||
|
<td>
|
||||||
|
<div class="iconlarge">
|
||||||
|
<div class="icon"></div>
|
||||||
|
<div class="border"></div>
|
||||||
|
<a target="_blank"></a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a class="name title is-size-5"></a>
|
||||||
|
<div class="description"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="points">
|
||||||
|
<span class="value"></span>
|
||||||
|
<span class="earned-date"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column summary is-two-fifths-desktop is-full-tablet">
|
||||||
|
<div class="has-text-centered title is-size-4">Progress Overview</div>
|
||||||
|
<div id="progress-bars">
|
||||||
|
<div id="progress-template" class="column">
|
||||||
|
<span class="title is-size-5"></span> <span class="value"></span>
|
||||||
|
<br>
|
||||||
|
<progress class="progress is-primary"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
let achievementsData = {};
|
||||||
|
const $achievementTemplate = $("#achievement-template").detach().removeAttr("id");
|
||||||
|
const $progressTemplate = $("#progress-template").detach().removeAttr("id");
|
||||||
|
|
||||||
|
function makeCategoryLink(cat) {
|
||||||
|
const $a = $("<a>")
|
||||||
|
.attr("href", "#")
|
||||||
|
.text(cat.nameLang0)
|
||||||
|
.on("click", () => {
|
||||||
|
const $children = $a.data("children");
|
||||||
|
if ($children !== undefined) {
|
||||||
|
if (!$children.is(":visible")) {
|
||||||
|
// Close the currently selected category
|
||||||
|
$("#categories .open")
|
||||||
|
.slideUp()
|
||||||
|
.removeClass("open");
|
||||||
|
|
||||||
|
// Open the clicked category
|
||||||
|
$children
|
||||||
|
.slideDown()
|
||||||
|
.addClass("open");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openCategory(cat);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAchievement(achievement) {
|
||||||
|
const isEarned = achievement.id in achievementsData.earned;
|
||||||
|
if (!isEarned && achievement.category === 81) {
|
||||||
|
// Show unlocked feats of strength only
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $achievement = $achievementTemplate
|
||||||
|
.clone(false)
|
||||||
|
.appendTo($("#achievements"));
|
||||||
|
$achievement.find(".name").text(achievement.title);
|
||||||
|
$achievement.find(".description").text(achievement.description);
|
||||||
|
$achievement.find(".points .value").text(achievement.points);
|
||||||
|
if (achievement.points === 0) {
|
||||||
|
$achievement.find(".points").addClass("no-points");
|
||||||
|
}
|
||||||
|
$achievement.find(".iconlarge .icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/large/${achievement.icon}.jpg")`);
|
||||||
|
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
|
||||||
|
|
||||||
|
if (isEarned) {
|
||||||
|
const earned = achievementsData.earned[achievement.id];
|
||||||
|
const date = new Date(earned.date * 1000);
|
||||||
|
$achievement.find(".earned-date").text(date.toLocaleDateString());
|
||||||
|
$achievement.addClass("earned");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $achievement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCategory(cat) {
|
||||||
|
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id);
|
||||||
|
|
||||||
|
$("#achievements").empty();
|
||||||
|
$(".summary").hide();
|
||||||
|
|
||||||
|
for (const achievement of categoryAchievements) {
|
||||||
|
makeAchievement(achievement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSummary() {
|
||||||
|
$("#achievements").empty();
|
||||||
|
const achievements = Object.keys(achievementsData.earned)
|
||||||
|
.filter(key => achievementsData.earned.hasOwnProperty(key))
|
||||||
|
.map(key => parseInt(key, 10))
|
||||||
|
.map(achievement => {
|
||||||
|
return {
|
||||||
|
achievement,
|
||||||
|
...achievementsData.earned[achievement],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const mostRecent = achievements.sort((a, b) => b.date - a.date).slice(0, 4);
|
||||||
|
for (const row of mostRecent) {
|
||||||
|
const achievement = achievementsData.achievements.find(ach => ach.id === row.achievement);
|
||||||
|
makeAchievement(achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".summary").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProgressBarRow(barsData) {
|
||||||
|
const $row = $("<div>").addClass("columns");
|
||||||
|
$("#progress-bars").append($row);
|
||||||
|
|
||||||
|
for (const bar of barsData) {
|
||||||
|
let $bar = $progressTemplate.clone(false);
|
||||||
|
$bar.find(".title").text(bar.name);
|
||||||
|
$bar.find(".value").text(`${bar.earned} / ${bar.total}`);
|
||||||
|
$bar.find(".progress").attr("value", bar.earned).attr("max", bar.total);
|
||||||
|
$row.append($bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProgressBars() {
|
||||||
|
let totalAchievements = 0;
|
||||||
|
let totalAchievementsEarned = 0;
|
||||||
|
|
||||||
|
const findProgress = (catId) => {
|
||||||
|
const cat = achievementsData.categories.find(cat => cat.id === catId);
|
||||||
|
const subCategories = achievementsData.categories.filter(cat => cat.parent === catId);
|
||||||
|
const categoryAchievements = achievementsData.achievements.filter(ach => ach.category === cat.id || subCategories.some(cat => cat.id === ach.category));
|
||||||
|
let earned = 0;
|
||||||
|
for (const ach of categoryAchievements) {
|
||||||
|
if (ach.id in achievementsData.earned) {
|
||||||
|
++earned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalAchievementsEarned += earned;
|
||||||
|
totalAchievements += categoryAchievements.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: cat.nameLang0,
|
||||||
|
earned,
|
||||||
|
total: categoryAchievements.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
makeProgressBarRow([{ ...findProgress(92) }, { ...findProgress(96) }]);
|
||||||
|
makeProgressBarRow([{ ...findProgress(97) }, { ...findProgress(95) }]);
|
||||||
|
makeProgressBarRow([{ ...findProgress(168) }, { ...findProgress(169) }]);
|
||||||
|
makeProgressBarRow([{ ...findProgress(201) }, { ...findProgress(155) }]);
|
||||||
|
const $totalRow = makeProgressBarRow([{ name: "Achievements Earned", earned: totalAchievementsEarned, total: totalAchievements }]);
|
||||||
|
$totalRow.prependTo($("#progress-bars")); // Move the total bar to the top
|
||||||
|
}
|
||||||
|
|
||||||
|
$.getJSON(`/character/{{realm}}/{{guid}}/achievements/data`, (data) => {
|
||||||
|
achievementsData = data;
|
||||||
|
|
||||||
|
const $summaryLink = $("<a>")
|
||||||
|
.attr("href", "#")
|
||||||
|
.text("Summary")
|
||||||
|
.on("click", openSummary)
|
||||||
|
.appendTo($("#categories"));
|
||||||
|
|
||||||
|
for (const cat of achievementsData.categories.filter(c => c.parent < 0)) {
|
||||||
|
if (cat.id === 1) {
|
||||||
|
// Skip Statistics category
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const children = achievementsData.categories.filter(c => c.parent === cat.id);
|
||||||
|
|
||||||
|
const $cat = makeCategoryLink(cat);
|
||||||
|
$("#categories").append($cat);
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
const $children = $("<div>")
|
||||||
|
.hide()
|
||||||
|
.addClass("subcategories");
|
||||||
|
$cat.data("children", $children);
|
||||||
|
for (const child of children) {
|
||||||
|
const $child = makeCategoryLink(child);
|
||||||
|
$child.appendTo($children);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#categories").append($children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalPoints = 0;
|
||||||
|
for (const ach of achievementsData.achievements) {
|
||||||
|
if (ach.id in achievementsData.earned) {
|
||||||
|
totalPoints += ach.points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("#total-points").text(totalPoints);
|
||||||
|
|
||||||
|
makeProgressBars();
|
||||||
|
openSummary();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -20,11 +20,11 @@
|
||||||
<span class="name"></span>
|
<span class="name"></span>
|
||||||
<span class="points"></span>
|
<span class="points"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="cols">
|
||||||
<div class="column"></div>
|
<div class="col"></div>
|
||||||
<div class="column"></div>
|
<div class="col"></div>
|
||||||
<div class="column"></div>
|
<div class="col"></div>
|
||||||
<div class="column"></div>
|
<div class="col"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTree(data, learnedTalents, container) {
|
function createTree(data, learnedTalents, container) {
|
||||||
const $columns = $(container).find(".column");
|
const $columns = $(container).find(".col");
|
||||||
let points = 0;
|
let points = 0;
|
||||||
|
|
||||||
for (let i = 0; i < 4; ++i) {
|
for (let i = 0; i < 4; ++i) {
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@
|
||||||
$("#mounts-container").hide();
|
$("#mounts-container").hide();
|
||||||
} else {
|
} else {
|
||||||
const $dismount = $("#mount-template").clone(false).removeAttr("id");
|
const $dismount = $("#mount-template").clone(false).removeAttr("id");
|
||||||
$dismount.find(".icon").css("background-image", `url("/img/UI-GearManager-LeaveItem-small.png")`);
|
$dismount.find(".icon").css("background-image", `url("/img/UI-GearManager-LeaveItem.png")`);
|
||||||
$dismount.find("a")
|
$dismount.find("a")
|
||||||
.on("click", () => {
|
.on("click", () => {
|
||||||
setMount(0);
|
setMount(0);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
html {
|
html {
|
||||||
background-color: #202124;
|
/* Removes the vertical scrollbar caused by Bulma */
|
||||||
color: white;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
static/css/bulma.min.css
vendored
Normal file
1
static/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
101
static/css/character-achievements.css
Normal file
101
static/css/character-achievements.css
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
#categories {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#categories a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategories {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#achievement-template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#achievements {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 1em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement td {
|
||||||
|
text-align: center !important;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement.earned {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .name {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .name,
|
||||||
|
.achievement .points,
|
||||||
|
.achievement .description {
|
||||||
|
color: #afafaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .icon {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement.earned .icon {
|
||||||
|
filter: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement.earned .name,
|
||||||
|
.achievement.earned .points,
|
||||||
|
.achievement.earned .description {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .points {
|
||||||
|
background-image: url("/img/UI-Achievement-Shields.png");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-position: -65px -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement.earned .points {
|
||||||
|
background-position: 0px -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .points .value {
|
||||||
|
line-height: 32px;
|
||||||
|
position: relative;
|
||||||
|
top: 10px;
|
||||||
|
left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .earned-date {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 32px;
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .points.no-points {
|
||||||
|
background-image: url("/img/UI-Achievement-Shields-NoPoints.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement .points.no-points .value {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#total-points-icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
@ -79,12 +79,12 @@
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.talent-tree .columns {
|
.talent-tree .cols {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.talent-tree .columns .column {
|
.talent-tree .cols .col {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
top: 6px;
|
top: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
z-index: -2;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wowhead-tooltip .whtt-sellprice {
|
.wowhead-tooltip .whtt-sellprice {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
top: 4px;
|
top: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconmedium .border {
|
.iconmedium .border {
|
||||||
|
|
@ -50,7 +49,7 @@
|
||||||
top: 6px;
|
top: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
z-index: -1;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconlarge .border {
|
.iconlarge .border {
|
||||||
|
|
@ -68,8 +67,14 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconlarge a:hover {
|
.iconlarge a:hover {
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconmedium a,
|
||||||
|
.iconlarge a {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,7 @@
|
||||||
#results td {
|
#results td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#results_processing {
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
static/img/UI-Achievement-Shields-NoPoints.png
Normal file
BIN
static/img/UI-Achievement-Shields-NoPoints.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
BIN
static/img/UI-Achievement-Shields.png
Normal file
BIN
static/img/UI-Achievement-Shields.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
static/img/UI-Achievement-TinyShield.png
Normal file
BIN
static/img/UI-Achievement-TinyShield.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 912 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
static/img/UI-GearManager-LeaveItem.png
Normal file
BIN
static/img/UI-GearManager-LeaveItem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -6,13 +6,15 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{{title}}</title>
|
<title>{{title}}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/bulma.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/armory.css">
|
<link rel="stylesheet" type="text/css" href="/css/armory.css">
|
||||||
|
|
||||||
<script type="application/javascript" src="/js/sync-url.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<script type="application/javascript" src="/js/sync-url.js"></script>
|
||||||
<script type="application/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script type="application/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
{{{ body }}}
|
<section class="section">
|
||||||
|
{{{ body }}}
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<a href="/">Back to Armory</a>
|
<a href="/">Back to Armory</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<a href="/character/{{realm}}/{{character}}">Character</a> 
|
<a href="/character/{{realm}}/{{name}}">Character</a> 
|
||||||
<a href="/character/{{realm}}/{{character}}/talents">Talents</a> 
|
<a href="/character/{{realm}}/{{name}}/talents">Talents</a> 
|
||||||
<a href="/character/{{realm}}/{{character}}/achievements">Achievements</a>
|
<a href="/character/{{realm}}/{{name}}/achievements">Achievements</a>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="char-name">
|
<div class="char-name">
|
||||||
<span class="char-name">{{character}}</span>
|
<span class="char-name">{{name}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Level <span class="char-level">{{level}}</span>
|
Level <span class="char-level">{{level}}</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue