diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3635a6f..e469842 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,4 +10,4 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - run: npm ci - - run: npm run lint -- --max-warnings=3 + - run: npm run lint -- --max-warnings=4 diff --git a/src/armory/Armory.ts b/src/armory/Armory.ts index 1b23806..536cc2b 100644 --- a/src/armory/Armory.ts +++ b/src/armory/Armory.ts @@ -106,6 +106,13 @@ export class Armory { }, hasInProgressQuests: function(quests: any[]) { return quests.some(quest => quest.status === 'In Progress'); + }, + getDiffClass: function(quest: any) { + 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 ''; } }, }), @@ -162,6 +169,7 @@ export class Armory { app.get("/character/:realm/:name/pvp", this.wrapRoute(charsController.pvp.bind(charsController))); app.get("/character/:realm/:name/reputation", this.wrapRoute(charsController.reputation.bind(charsController))); 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))); diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index 86f3178..b3004c7 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -392,11 +392,95 @@ export class CharacterController { 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 + categories: questsByCategory, + otherCharacters: allCharacters.filter(c => c.guid !== charData.guid) + }, + }); + } + + public async questsCompare(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + 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; + } 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) }, }); } @@ -1485,4 +1569,17 @@ export class CharacterController { return row; }); } + + private async getAllCharacters(currentRealm: string): Promise> { + 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 + })); + } } diff --git a/static/character-quests-compare.hbs b/static/character-quests-compare.hbs new file mode 100644 index 0000000..cec7caa --- /dev/null +++ b/static/character-quests-compare.hbs @@ -0,0 +1,129 @@ + +{{> icons }} + + +{{> character-header }} + +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+

Quest Comparison

+
+ + {{#if (eq data.char2.realm data.char1.realm)}} + {{data.char1.name}} + {{else}} + {{data.char1.realm}}/{{data.char1.name}} + {{/if}} + + vs + + + {{#if (eq data.char2.realm data.char1.realm)}} + {{data.char2.name}} + {{else}} + {{data.char2.realm}}/{{data.char2.name}} + {{/if}} + + +
+
+ +{{#each data.categories as |quests category|}} +
+

+ {{@key}} + +

+ +
+ + + + + + + + + + + {{#each this}} + + + + + + + {{/each}} + +
QuestLevel{{../data.char1.name}}{{../data.char2.name}}
+ {{this.title}} + {{this.questLevel}}{{this.char1Status}}{{this.char2Status}}
+
+
+{{/each}} + + \ No newline at end of file diff --git a/static/character-quests.hbs b/static/character-quests.hbs index 3ef3b7a..f6a85cf 100644 --- a/static/character-quests.hbs +++ b/static/character-quests.hbs @@ -6,6 +6,33 @@ {{> character-header }} +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ {{#each data.categories as |quests category|}}
{{#if (hasInProgressQuests quests)}} @@ -93,5 +120,22 @@ document.addEventListener('DOMContentLoaded', function() { this.classList.toggle('expanded'); }); }); + + const compareSelect = document.getElementById('compareCharacter'); + const compareButton = document.getElementById('compareButton'); + + compareSelect.addEventListener('change', function() { + compareButton.disabled = !this.value; + }); + + compareButton.addEventListener('click', function() { + const selected = compareSelect.value; + if (selected) { + const [realm, name] = selected.split('/'); + const sanitizedRealm = encodeURIComponent(realm); + const sanitizedName = encodeURIComponent(name); + window.location.href = `${window.location.pathname}/compare/${sanitizedRealm}/${sanitizedName}`; + } + }); }); \ No newline at end of file diff --git a/static/css/character-quests.css b/static/css/character-quests.css index e673081..69f913c 100644 --- a/static/css/character-quests.css +++ b/static/css/character-quests.css @@ -105,4 +105,40 @@ .category-header:not(.expanded) .collapse-icon { transform: rotate(-90deg); +} + +.quest-comparison-header { + margin-bottom: 20px; + text-align: center; + max-width: 500px; +} + +.quest-comparison-header .characters { + font-size: 1.2em; + margin-top: 10px; +} + +.quest-comparison-header .char1, +.quest-comparison-header .char2 { + font-weight: bold; +} + +tr.quest-diff { + background-color: rgba(255, 255, 0, 0.2) !important; +} + +tr.quest-missing-1 td:nth-child(3) { + background-color: rgba(255, 0, 0, 0.2) !important; +} + +tr.quest-missing-2 td:nth-child(4) { + background-color: rgba(255, 0, 0, 0.2) !important; +} + +.quest-compare-section { + max-width: 500px; +} + +.quest-compare-section .select select { + min-width: 200px; } \ No newline at end of file