From 7cd2748d3379aee9244a2ebcffe64454703ec81e Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sat, 14 May 2022 16:39:31 +0200 Subject: [PATCH] feat(character): add transmog module compatibility (#21) --- .env.example | 1 + README.md | 7 ++-- config.default.json | 1 + src/armory/Config.ts | 1 + src/armory/controllers/CharacterController.ts | 36 +++++++++++++++---- static/character.hbs | 26 ++++++++++++-- 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index c600f23..b1b161d 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ ACORE_ARMORY_IFRAME_MODE__ENABLED=0 ACORE_ARMORY_IFRAME_MODE__URL="https://mywebsite.com/armory" ACORE_ARMORY_LOAD_DBCS=1 ACORE_ARMORY_HIDE_GAME_MASTERS=1 +ACORE_ARMORY_TRANSMOG_MODULE=0 ACORE_ARMORY_REALMS__0__NAME="AzerothCore" ACORE_ARMORY_REALMS__0__REALM_ID=1 ACORE_ARMORY_REALMS__0__AUTH_DATABASE="acore_auth" diff --git a/README.md b/README.md index 574df96..c7993dc 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ I also noticed that such a tool was frequently requested in the AzerothCore Disc | `iframeMode.url` | `ACORE_ARMORY_IFRAME_MODE__URL` | String | `"https://mywebsite.com/armory"` | Set this to the URL of the page that hosts the `iframe` | | `loadDbcs` | `ACORE_ARMORY_LOAD_DBCS` | Boolean | `true` | Loads the DBC data from the `data` directory into memory when starting up. It is highly recommended to set this to `true`. Only use `false` to keep memory usage low, on a test server for example | | `hideGameMasters` | `ACORE_ARMORY_HIDE_GAME_MASTERS` | Boolean | `true` | Hides Game Master characters if set to `true`. They will not be found in the search page, and their character pages will show a 404 error | +| `transmogModule` | `ACORE_ARMORY_TRANSMOG_MODULE` | Boolean | `false` | Set this to `true` if your server uses the [transmogrification module](https://github.com/azerothcore/mod-transmog) and you want to display the transmogrified items on the 3D model | | `realms` | `ACORE_ARMORY_REALMS__`... | Array of objects | | An array of realm configurations | | `realms[0].name` | `ACORE_ARMORY_REALMS__0__NAME` | String | `"AzerothCore"` | The name of the realm. Will be used in the URLs, shown on the character pages and in the search page if you have multiple realms | | `realms[0].realmId` | `ACORE_ARMORY_REALMS__0__REALM_ID` | Number | `1` | The realm's ID, this must match the `id` column of the `realmlist` table in the auth database | @@ -182,8 +183,8 @@ Open a web browser and navigate to http://localhost:48733 Other useful npm scripts: * `npm run clean`: cleans the build directory * `npm run watch`: watches for changes and rebuilds automatically, useful for development -* `npm run fetchdata`: downloads the data needed by the 3d model viewer -* `npm run cleardata`: clears the data downloaded for the 3d model viewer +* `npm run fetchdata`: downloads the data needed by the 3D model viewer +* `npm run cleardata`: clears the data downloaded for the 3D model viewer ### With Docker @@ -259,7 +260,7 @@ This repository is used in production over at [ChromieCraft](https://www.chromie - [X] Character page - [X] Online/offline status - [X] Equipment with tooltips - - [X] 3d model, including mounts + - [X] 3D model, including mounts and [transmogrifications](https://github.com/azerothcore/mod-transmog) - [X] Talent trees, including glyphs and dual spec support - [X] Achievements - [X] PvP statistics, including arena teams diff --git a/config.default.json b/config.default.json index f05a2cf..1e29b79 100644 --- a/config.default.json +++ b/config.default.json @@ -9,6 +9,7 @@ }, "loadDbcs": true, "hideGameMasters": true, + "transmogModule": false, "realms": [ { "name": "AzerothCore", diff --git a/src/armory/Config.ts b/src/armory/Config.ts index 170c638..585d544 100644 --- a/src/armory/Config.ts +++ b/src/armory/Config.ts @@ -31,6 +31,7 @@ export class Config { public iframeMode: IIframeModeConfig; public loadDbcs: boolean; public hideGameMasters: boolean; + public transmogModule: boolean; public realms: IRealmConfig[]; public worldDatabase: IDatabaseConfig; public dbQueryTimeout: number; diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index ac01118..83eb6aa 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -32,6 +32,7 @@ interface IEquipmentData { classId: number; subclassId: number; quality: number; + transmog?: number; } interface ICustomizationOption { @@ -120,7 +121,7 @@ export class CharacterController { } this.itemSocketBonuses = {}; - let [rows, fields] = await this.armory.worldDb.query({ + const [rows, fields] = await this.armory.worldDb.query({ sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0", timeout: this.armory.config.dbQueryTimeout, }); @@ -180,6 +181,8 @@ export class CharacterController { return row; }); const mounts = await this.getMounts(realmName, charData.guid); + const transmogs: number[][] = this.armory.config.transmogModule ? [] : undefined; + const characterModelItems = await this.getModelViewerItems(equipmentData, charData.class, transmogs); res.render("character.hbs", { title: `Armory - ${charData.name}`, @@ -189,7 +192,8 @@ export class CharacterController { gender: charData.gender, class: charData.class, flags: charData.playerFlags, - characterModelItems: await this.getModelViewerItems(equipmentData, charData.class), + characterModelItems, + characterModelTransmogs: transmogs, customizationOptions: customization, equipment, mounts, @@ -332,11 +336,16 @@ export class CharacterController { } private async getEquipmentData(realm: string, charGuid: number): Promise { + const transmogSelect = this.armory.config.transmogModule ? ", custom_transmogrification.FakeEntry AS transmog" : ""; + const transmogJoin = this.armory.config.transmogModule ? "LEFT JOIN custom_transmogrification ON custom_transmogrification.GUID = item_instance.guid" : ""; let [rows, fields] = await this.armory.getCharactersDb(realm).query({ sql: ` - SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId + SELECT + character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId + ${transmogSelect} FROM character_inventory JOIN item_instance ON item_instance.guid = character_inventory.item + ${transmogJoin} WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) `, values: [charGuid], @@ -378,7 +387,7 @@ export class CharacterController { return (rows as RowDataPacket[]).map((row) => this.mountBySpellId[row.spell]).filter((m) => m !== undefined); } - private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number): Promise { + private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number, transmogOut?: number[][]): Promise { if (charClass !== 3) { // Keep ranged weapon only if the character is a hunter equipmentData = equipmentData.filter((row) => row.slot !== 17); @@ -395,12 +404,27 @@ export class CharacterController { if (modifiedAppearance === undefined) { continue; } - const appearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId); + let appearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId); if (appearance === undefined) { continue; } - items.push([this.itemInventoryTypes[equipment.itemEntry], appearance.itemDisplayInfoId]); + let invType = this.itemInventoryTypes[equipment.itemEntry]; + items.push([invType, appearance.itemDisplayInfoId]); + + if (transmogOut !== undefined) { + if (equipment.transmog !== undefined) { + const modifiedAppearance = await this.armory.dbc.itemModifiedAppearance().find((row) => row.itemId === equipment.transmog); + if (modifiedAppearance !== undefined) { + const tmogAppearance = await this.armory.dbc.itemAppearance().find((row) => row.id === modifiedAppearance.itemAppearanceId); + if (tmogAppearance !== undefined) { + appearance = tmogAppearance; + invType = this.itemInventoryTypes[equipment.transmog]; + } + } + } + transmogOut.push([invType, appearance.itemDisplayInfoId]); + } } return items; diff --git a/static/character.hbs b/static/character.hbs index 87889e0..c8bab38 100644 --- a/static/character.hbs +++ b/static/character.hbs @@ -43,6 +43,12 @@ Hide tabard +
+ +
@@ -125,6 +131,20 @@ const hide = $("#cb-hide-tabard").prop("checked"); setSlotVisible(19, !hide); }); + if (charData.characterModelTransmogs === undefined) { + $("#cb-hide-transmogs").parent().hide(); + } + $("#cb-hide-transmogs").change(() => { + const hide = $("#cb-hide-transmogs").prop("checked"); + const items = hide ? charData.characterModelItems : charData.characterModelTransmogs; + for (const slot of (items.map((item) => item[0]))) { + clearSlots([slot]); + } + + const hideHelm = $("#cb-hide-helmet").prop("checked"); + const hideCloak = $("#cb-hide-cloak").prop("checked"); + setItems(items.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak)))); + }); let viewer; function createViewer() { @@ -138,7 +158,9 @@ function setSlotVisible(slot, visible) { if (visible) { - const item = charData.characterModelItems.find(([sl, appearance]) => sl === slot); + const transmogged = !$("#cb-hide-transmogs").prop("checked"); + const items = transmogged ? charData.characterModelTransmogs : charData.characterModelItems; + const item = items.find(([sl, appearance]) => sl === slot); if (item !== undefined) { setItems([item]); } @@ -343,7 +365,7 @@ sheathOff: -1, }, cls: charData.class, - items: charData.characterModelItems.filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak))), + items: (charData.characterModelTransmogs ?? charData.characterModelItems).filter(item => !((item[0] === 1 && hideHelm) || (item[0] === 16 && hideCloak))), models: { type: ZamModelViewer.Wow.Types.CHARACTER, id: `${races[charData.race]}${genders[charData.gender]}`,