feat(character/achievements): add achievements page

This commit is contained in:
Axel Cocat 2022-02-26 01:47:20 +01:00
parent ed424d63d0
commit ab3b84f743
24 changed files with 2419 additions and 33 deletions

View 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
1 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
2 92 -1 General 16712190 1
3 123 122 Arenas 16712190 1
4 130 1 Character 16712190 1
5 135 128 Creatures 16712190 1
6 140 130 Wealth 16712190 1
7 152 21 Rated Arenas 16712190 1
8 160 155 Lunar Festival 16712190 1
9 165 95 Arena 16712190 1
10 170 169 Cooking 16712190 1
11 178 132 Secondary Skills 16712190 1
12 14777 97 Eastern Kingdoms 16712190 1
13 14808 168 Classic 16712190 1
14 14821 14807 Classic 16712190 1
15 14861 96 Classic 16712190 1
16 14864 201 Classic 16712190 1
17 96 -1 Quests 16712190 2
18 124 122 Battlegrounds 16712190 2
19 136 128 Honorable Kills 16712190 2
20 141 1 Combat 16712190 2
21 145 130 Consumables 16712190 2
22 153 21 Battlegrounds 16712190 2
23 171 169 Fishing 16712190 2
24 173 132 Professions 16712190 2
25 187 155 Love is in the Air 16712190 2
26 14778 97 Kalimdor 16712190 2
27 14801 95 Alterac Valley 16712190 2
28 14805 168 The Burning Crusade 16712190 2
29 14822 14807 The Burning Crusade 16712190 2
30 14862 96 The Burning Crusade 16712190 2
31 14865 201 The Burning Crusade 16712190 2
32 97 -1 Exploration 16712190 3
33 125 122 Dungeons 16712190 3
34 128 1 Kills 16712190 3
35 137 128 Killing Blows 16712190 3
36 147 130 Reputation 16712190 3
37 154 21 World 16712190 3
38 159 155 Noblegarden 16712190 3
39 172 169 First Aid 16712190 3
40 14779 97 Outland 16712190 3
41 14802 95 Arathi Basin 16712190 3
42 14806 168 Lich King Dungeon 16712190 3
43 14823 14807 Wrath of the Lich King 16712190 3
44 14863 96 Wrath of the Lich King 16712190 3
45 14866 201 Wrath of the Lich King 16712190 3
46 95 -1 Player vs. Player 16712190 4
47 122 1 Deaths 16712190 4
48 126 122 World 16712190 4
49 163 155 Children's Week 16712190 4
50 191 130 Gear 16712190 4
51 14780 97 Northrend 16712190 4
52 14803 95 Eye of the Storm 16712190 4
53 14921 168 Lich King Heroic 16712190 4
54 14963 14807 Secrets of Ulduar 16712190 4
55 127 122 Resurrection 16712190 5
56 133 1 Quests 16712190 5
57 161 155 Midsummer 16712190 5
58 168 -1 Dungeons & Raids 16712190 5
59 14804 95 Warsong Gulch 16712190 5
60 14922 168 Lich King 10-Player Raid 16712190 5
61 15021 14807 Call of the Crusade 16712190 5
62 162 155 Brewfest 16712190 6
63 169 -1 Professions 16712190 6
64 14807 1 Dungeons & Raids 16712190 6
65 14881 95 Strand of the Ancients 16712190 6
66 14923 168 Lich King 25-Player Raid 16712190 6
67 15062 14807 Fall of the Lich King 16712190 6
68 132 1 Skills 16712190 7
69 158 155 Hallow's End 16712190 7
70 201 -1 Reputation 16712190 7
71 14901 95 Wintergrasp 16712190 7
72 14961 168 Secrets of Ulduar 10-Player Raid 16712190 7
73 134 1 Travel 16712190 8
74 155 -1 World Events 16712190 8
75 14962 168 Secrets of Ulduar 25-Player Raid 16712190 8
76 14981 155 Pilgrim's Bounty 16712190 8
77 15003 95 Isle of Conquest 16712190 8
78 81 -1 Feats of Strength 16712190 9
79 131 1 Social 16712190 9
80 156 155 Winter Veil 16712190 9
81 15001 168 Call of the Crusade 10-Player Raid 16712190 9
82 1 -1 Statistics 16712190 10
83 21 1 Player vs. Player 16712190 10
84 14941 155 Argent Tournament 16712190 10
85 15002 168 Call of the Crusade 25-Player Raid 16712190 10
86 15041 168 Fall of the Lich King 10-Player Raid 16712190 11
87 15042 168 Fall of the Lich King 25-Player Raid 16712190 12

File diff suppressed because it is too large Load diff

4
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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", () => {

View file

@ -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,
};
}
} }

View file

@ -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);
} }

View 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>&emsp;<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>

View file

@ -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) {

View file

@ -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);

View file

@ -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

File diff suppressed because one or more lines are too long

View 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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -9,3 +9,7 @@
#results td { #results td {
text-align: center; text-align: center;
} }
#results_processing {
height: 70px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -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>
<section class="section">
{{{ body }}} {{{ body }}}
</section>
</body> </body>

View file

@ -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>&ensp; <a href="/character/{{realm}}/{{name}}">Character</a>&ensp;
<a href="/character/{{realm}}/{{character}}/talents">Talents</a>&ensp; <a href="/character/{{realm}}/{{name}}/talents">Talents</a>&ensp;
<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>