diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index 2c91d27..9d59220 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -33,7 +33,14 @@ interface ICustomizationOption { choiceId: number; } -const ITEM_CLASS_GEM = 3; +interface IMount { + creatureDisplayId: number; + spell: number; + icon: string; +} + +const ItemClassGem = 3; +const SpellMechanicMounted = 21; const RaceDisplayName = { 1: "Human", 2: "Orc", @@ -66,6 +73,8 @@ export class CharacterController { private gemItems: { [key: number]: boolean }; private enchantSrcItems: { [key: number]: number }; private itemSocketBonuses: { [key: number]: number }; + private mountSpells: number[]; + private mountBySpellId: { [key: number]: IMount }; public constructor(armory: Armory) { this.armory = armory; @@ -94,7 +103,7 @@ export class CharacterController { } this.gemItems = {}; - for await (const row of this.armory.dbc.item().filter(item => item.classId === ITEM_CLASS_GEM)) { + for await (const row of this.armory.dbc.item().filter(item => item.classId === ItemClassGem)) { this.gemItems[row.id] = true; } @@ -108,6 +117,26 @@ export class CharacterController { for (const row of rows as RowDataPacket[]) { this.itemSocketBonuses[row.entry] = row.socketBonus; } + + const mountSpells = await this.armory.dbc.spell() + .filter(m => m.mechanic === SpellMechanicMounted) + .toArray(); + this.mountSpells = mountSpells.map(spell => spell.id); + this.mountBySpellId = {}; + for (const spell of mountSpells) { + const mount = await this.armory.dbc.mount().find(m => m.sourceSpellId === spell.id); + const icon = await this.armory.dbc.spellIcon().find(icon => icon.id === spell.spellIconId); + if (mount !== undefined) { + const display = await this.armory.dbc.mountDisplay().find(d => d.mountId === mount.id); + if (display !== undefined) { + this.mountBySpellId[spell.id] = { + creatureDisplayId: display.creatureDisplayInfoId, + spell: spell.id, + icon: this.processSpellIconTexture(icon?.textureFilename ?? ""), + }; + } + } + } } public async character(req: express.Request, res: express.Response): Promise { @@ -136,6 +165,7 @@ export class CharacterController { (row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments); return row; }); + const mounts = await this.getMounts(realmName, charData.guid); res.render("character.html", { title: `Armory - ${charName}`, @@ -148,6 +178,7 @@ export class CharacterController { characterModelItems: await this.getModelViewerItems(equipmentData, charData.class), customizationOptions: customization, equipment, + mounts, }), }); @@ -221,6 +252,18 @@ export class CharacterController { return rows as RowDataPacket[] as IEquipmentData[]; } + private async getMounts(realm: string, charGuid: number): Promise { + const [rows, fields] = await this.armory.getCharactersDb(realm).query(` + SELECT spell + FROM character_spell + WHERE guid = ? AND spell IN (?) + `, [charGuid, this.mountSpells]); + + return (rows as RowDataPacket[]) + .map(row => this.mountBySpellId[row.spell]) + .filter(m => m !== undefined); + } + private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number): Promise { if (charClass !== 3) { // Keep ranged weapon only if the character is a hunter diff --git a/src/tools/fetchdata.ts b/src/tools/fetchdata.ts index 931e3b4..5556e53 100644 --- a/src/tools/fetchdata.ts +++ b/src/tools/fetchdata.ts @@ -300,7 +300,9 @@ async function readDbcData(): Promise { dbcMountDisplayByMountId = {}; for await (const row of dbc.mountDisplay()) { - dbcMountDisplayByMountId[row.mountId] = row; + if (!(row.mountId in dbcMountDisplayByMountId)) { + dbcMountDisplayByMountId[row.mountId] = row; + } } } diff --git a/static/character-talents.html b/static/character-talents.html index 87f432a..8c687c5 100644 --- a/static/character-talents.html +++ b/static/character-talents.html @@ -1,17 +1,5 @@ - - + +{{> icons }} @@ -19,8 +7,8 @@ {{> character-header }} -
-
+
+
@@ -140,7 +128,7 @@ const $template = $("#talent-template"); const $talent = $template.clone(false); $talent.removeAttr("id"); - $talent.find(".talent").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${spell.icon}.jpg")`); + $talent.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${spell.icon}.jpg")`); const rank = spell.id in learnedTalents ? (learnedTalents[spell.id].rank - 1) : 0; $talent.find("a").attr("href", `{{aowow}}/?spell=${spell["spellRank" + rank]}`); $talent.data("id", spell.id); diff --git a/static/character.html b/static/character.html index c04b4b6..368fb3f 100644 --- a/static/character.html +++ b/static/character.html @@ -1,13 +1,5 @@ - - + +{{> icons }} @@ -25,17 +17,32 @@
- - -
+ + + + + + +
+ + +
- - -
+ + +
- - -
+ + +
+ +
+
+
+ +
+ +
@@ -92,11 +99,16 @@ }, mount: { type: ZamModelViewer.Wow.Types.NPC, - //id: 25280, id: 0, }, }; - const viewer = new ZamModelViewer(characterModel); + let viewer; + + function createViewer() { + viewer?.destroy(); + viewer = new ZamModelViewer(characterModel); + } + createViewer(); $("#cb-hide-helmet").change(() => { const hide = $("#cb-hide-helmet").prop("checked"); @@ -124,6 +136,7 @@ function clearSlots(slots) { viewer.method("clearSlots", slots.join(",")); + characterModel.items = characterModel.items.filter(item => !slots.includes(item[0])); } function setItems(items) { @@ -134,6 +147,9 @@ }; }); viewer.method("setItems", [data]); + + characterModel.items = characterModel.items.filter(item => !items.some(item2 => item[0] === item2[0])); + characterModel.items.push(...items); } function createItemSlot(item, invSlot, icon, rel, container) { @@ -172,4 +188,39 @@ createItemSlot(item?.itemEntry, slot, item?.icon?.toLowerCase(), rel.join("&"), side.element); } } + + function setMount(mountId) { + if (characterModel.mount.id !== mountId) { + characterModel.mount.id = mountId; + createViewer(); + } + } + + if (charData.mounts.length === 0) { + $("#mounts-container").hide(); + } else { + const $dismount = $("#mount-template").clone(false).removeAttr("id"); + $dismount.find(".icon").css("background-image", `url("/img/UI-GearManager-LeaveItem-small.png")`); + $dismount.find("a") + .on("click", () => { + setMount(0); + return false; + }); + $("#mounts").append($dismount); + + for (const mount of charData.mounts) { + const $mount = $("#mount-template").clone(false).removeAttr("id"); + $mount.find(".icon").css("background-image", `url("{{aowow}}/static/images/wow/icons/medium/${mount.icon}.jpg")`); + $mount.find("a") + .attr("href", `{{aowow}}/?spell=${mount.spell}`) + .on("click", () => { + setMount(mount.creatureDisplayId); + return false; + }); + $("#mounts").append($mount); + } + } + $("#btn-mounts").on("click", () => { + $("#mounts").slideToggle("fast"); + }); diff --git a/static/css/character-talents.css b/static/css/character-talents.css index 3b55eed..a5e70a9 100644 --- a/static/css/character-talents.css +++ b/static/css/character-talents.css @@ -3,28 +3,11 @@ display: none; } -.iconmedium { - width: 44px; - height: 44px; - position: relative; - display: inline-block; - overflow: visible; -} - -.iconmedium .talent { - width: 36px; - height: 36px; - left: 4px; - top: 4px; - position: absolute; - background-repeat: no-repeat; -} - .iconmedium.empty .icon-border { background-position: 0px 0px; } -.iconmedium.empty .talent { +.iconmedium.empty .icon { filter: grayscale(100%); opacity: 0.7; } @@ -37,14 +20,6 @@ background-position: -42px 0px; } -.iconmedium .border { - width: 44px; - height: 44px; - left: 0px; - top: 0px; - background-repeat: no-repeat; -} - .iconmedium .icon-border { width: 42px; height: 42px; @@ -54,22 +29,10 @@ background-repeat: no-repeat; } -.iconmedium a { - width: 38px; - height: 38px; - position: absolute; - left: 3px; - top: 3px; -} - .iconmedium a span { display: none; } -.iconmedium a:hover { - background-position: 0 0; -} - .iconmedium::after { content: attr(rank); background-color: black; diff --git a/static/css/character.css b/static/css/character.css index ee9c1d4..823c4d5 100644 --- a/static/css/character.css +++ b/static/css/character.css @@ -1,14 +1,8 @@ -#item-slot-template { +#item-slot-template, +#mount-template { display: none; } -.iconlarge { - width: 68px; - height: 68px; - position: relative; - display: inline-block; -} - .iconlarge .inventory-slot { width: 56px; height: 56px; @@ -16,38 +10,18 @@ top: 6px; position: absolute; background-repeat: no-repeat; -} - -.iconlarge .icon { - width: 56px; - height: 56px; - left: 6px; - top: 6px; - position: absolute; - background-repeat: no-repeat; -} - -.iconlarge .border { - width: 68px; - height: 68px; - position: absolute; - left: 0; - top: 0; - background-repeat: no-repeat; -} - -.iconlarge a { - width: 62px; - height: 62px; - position: absolute; - left: 3px; - top: 3px; -} - -.iconlarge a:hover { - background-position: 0 0; + z-index: -2; } .wowhead-tooltip .whtt-sellprice { display: none; } + +#mounts { + max-width: 400px; +} + +#char-options td { + vertical-align: top; + padding-right: 32px; +} diff --git a/static/css/icons.css b/static/css/icons.css new file mode 100644 index 0000000..6ba5fcd --- /dev/null +++ b/static/css/icons.css @@ -0,0 +1,75 @@ +.iconmedium { + width: 44px; + height: 44px; + position: relative; + display: inline-block; + overflow: visible; +} + +.iconmedium .icon { + width: 36px; + height: 36px; + left: 4px; + top: 4px; + position: absolute; + background-repeat: no-repeat; + z-index: -1; +} + +.iconmedium .border { + width: 44px; + height: 44px; + left: 0px; + top: 0px; + background-repeat: no-repeat; +} + +.iconmedium a { + width: 38px; + height: 38px; + position: absolute; + left: 3px; + top: 3px; +} + +.iconmedium a:hover { + background-position: 0 0; +} + +.iconlarge { + width: 68px; + height: 68px; + position: relative; + display: inline-block; +} + +.iconlarge .icon { + width: 56px; + height: 56px; + left: 6px; + top: 6px; + position: absolute; + background-repeat: no-repeat; + z-index: -1; +} + +.iconlarge .border { + width: 68px; + height: 68px; + position: absolute; + left: 0; + top: 0; + background-repeat: no-repeat; +} + +.iconlarge a { + width: 62px; + height: 62px; + position: absolute; + left: 3px; + top: 3px; +} + +.iconlarge a:hover { + background-position: 0 0; +} diff --git a/static/img/UI-GearManager-LeaveItem-small.png b/static/img/UI-GearManager-LeaveItem-small.png new file mode 100644 index 0000000..8b96931 Binary files /dev/null and b/static/img/UI-GearManager-LeaveItem-small.png differ diff --git a/static/index.html b/static/index.html index 79daf1b..8dd0f16 100644 --- a/static/index.html +++ b/static/index.html @@ -1,6 +1,6 @@ - - - + + +

Armory

diff --git a/static/layout.html b/static/layout.html index f77d476..1fdbda0 100644 --- a/static/layout.html +++ b/static/layout.html @@ -6,7 +6,7 @@ {{title}} - + diff --git a/static/partials/icons.html b/static/partials/icons.html new file mode 100644 index 0000000..d71037c --- /dev/null +++ b/static/partials/icons.html @@ -0,0 +1,22 @@ + +