feat(character): add mounts

This commit is contained in:
Axel Cocat 2022-01-21 14:52:19 +01:00
parent d0eb86daa7
commit b19bd0b0a3
11 changed files with 239 additions and 121 deletions

View file

@ -33,7 +33,14 @@ interface ICustomizationOption {
choiceId: number; choiceId: number;
} }
const ITEM_CLASS_GEM = 3; interface IMount {
creatureDisplayId: number;
spell: number;
icon: string;
}
const ItemClassGem = 3;
const SpellMechanicMounted = 21;
const RaceDisplayName = { const RaceDisplayName = {
1: "Human", 1: "Human",
2: "Orc", 2: "Orc",
@ -66,6 +73,8 @@ export class CharacterController {
private gemItems: { [key: number]: boolean }; private gemItems: { [key: number]: boolean };
private enchantSrcItems: { [key: number]: number }; private enchantSrcItems: { [key: number]: number };
private itemSocketBonuses: { [key: number]: number }; private itemSocketBonuses: { [key: number]: number };
private mountSpells: number[];
private mountBySpellId: { [key: number]: IMount };
public constructor(armory: Armory) { public constructor(armory: Armory) {
this.armory = armory; this.armory = armory;
@ -94,7 +103,7 @@ export class CharacterController {
} }
this.gemItems = {}; 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; this.gemItems[row.id] = true;
} }
@ -108,6 +117,26 @@ export class CharacterController {
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
this.itemSocketBonuses[row.entry] = row.socketBonus; 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<void> { public async character(req: express.Request, res: express.Response): Promise<void> {
@ -136,6 +165,7 @@ export class CharacterController {
(row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments); (row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments);
return row; return row;
}); });
const mounts = await this.getMounts(realmName, charData.guid);
res.render("character.html", { res.render("character.html", {
title: `Armory - ${charName}`, title: `Armory - ${charName}`,
@ -148,6 +178,7 @@ export class CharacterController {
characterModelItems: await this.getModelViewerItems(equipmentData, charData.class), characterModelItems: await this.getModelViewerItems(equipmentData, charData.class),
customizationOptions: customization, customizationOptions: customization,
equipment, equipment,
mounts,
}), }),
}); });
@ -221,6 +252,18 @@ export class CharacterController {
return rows as RowDataPacket[] as IEquipmentData[]; return rows as RowDataPacket[] as IEquipmentData[];
} }
private async getMounts(realm: string, charGuid: number): Promise<IMount[]> {
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<number[][]> { private async getModelViewerItems(equipmentData: IEquipmentData[], charClass: number): Promise<number[][]> {
if (charClass !== 3) { if (charClass !== 3) {
// Keep ranged weapon only if the character is a hunter // Keep ranged weapon only if the character is a hunter

View file

@ -300,8 +300,10 @@ async function readDbcData(): Promise<void> {
dbcMountDisplayByMountId = {}; dbcMountDisplayByMountId = {};
for await (const row of dbc.mountDisplay()) { for await (const row of dbc.mountDisplay()) {
if (!(row.mountId in dbcMountDisplayByMountId)) {
dbcMountDisplayByMountId[row.mountId] = row; dbcMountDisplayByMountId[row.mountId] = row;
} }
}
} }
async function downloadMounts(): Promise<void> { async function downloadMounts(): Promise<void> {

View file

@ -1,17 +1,5 @@
<link rel="stylesheet" href="/css/character-talents.css"> <link rel="stylesheet" type="text/css" href="/css/character-talents.css">
<style> {{> icons }}
.iconmedium a {
background: url("{{aowow}}/static/images/Icon/medium/hilite/default.png") no-repeat 38px 0;
}
.iconmedium .border {
background-image: url("{{aowow}}/static/images/Icon/medium/border/default.png");
}
.iconmedium .icon-border {
background-image: url("{{aowow}}/static/images/TalentCalc/border.gif");
}
</style>
<script type="application/javascript"> <script type="application/javascript">
const aowow_tooltips = { "renamelinks": true, }; const aowow_tooltips = { "renamelinks": true, };
</script> </script>
@ -19,8 +7,8 @@
{{> character-header }} {{> character-header }}
<div id="talent-template" class="iconmedium empty"> <div id="talent-template" class="iconmedium icontalent empty">
<div class="talent"></div> <div class="icon"></div>
<div class="border"></div> <div class="border"></div>
<div class="icon-border"></div> <div class="icon-border"></div>
<a target="_blank"></a> <a target="_blank"></a>
@ -140,7 +128,7 @@
const $template = $("#talent-template"); const $template = $("#talent-template");
const $talent = $template.clone(false); const $talent = $template.clone(false);
$talent.removeAttr("id"); $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; const rank = spell.id in learnedTalents ? (learnedTalents[spell.id].rank - 1) : 0;
$talent.find("a").attr("href", `{{aowow}}/?spell=${spell["spellRank" + rank]}`); $talent.find("a").attr("href", `{{aowow}}/?spell=${spell["spellRank" + rank]}`);
$talent.data("id", spell.id); $talent.data("id", spell.id);

View file

@ -1,13 +1,5 @@
<link rel="stylesheet" href="/css/character.css"> <link rel="stylesheet" type="text/css" href="/css/character.css">
<style> {{> icons }}
.iconlarge .border {
background-image: url("{{aowow}}/static/images/Icon/large/border/default.png");
}
.iconlarge a {
background: url("{{aowow}}/static/images/Icon/large/hilite/default.png") no-repeat 62px 0;
}
</style>
<script type="application/javascript" src="/js/viewer.min.js"></script> <script type="application/javascript" src="/js/viewer.min.js"></script>
<script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script> <script type="application/javascript" src="{{aowow}}/static/widgets/power.js"></script>
@ -25,17 +17,32 @@
<div style="flex: 12%;"></div> <div style="flex: 12%;"></div>
</div> </div>
<input id="cb-hide-helmet" type="checkbox"> <table id="char-options">
<label for="cb-hide-helmet">Hide helmet</label> <tr>
<br> <td>
<input id="cb-hide-helmet" type="checkbox">
<label for="cb-hide-helmet">Hide helmet</label>
<br>
<input id="cb-hide-cloak" type="checkbox"> <input id="cb-hide-cloak" type="checkbox">
<label for="cb-hide-cloak">Hide cloak</label> <label for="cb-hide-cloak">Hide cloak</label>
<br> <br>
<input id="cb-hide-tabard" type="checkbox"> <input id="cb-hide-tabard" type="checkbox">
<label for="cb-hide-tabard">Hide tabard</label> <label for="cb-hide-tabard">Hide tabard</label>
<br> </td>
<td id="mounts-container">
<button id="btn-mounts">Mounts</button>
<div id="mount-template" class="iconmedium">
<div class="icon"></div>
<div class="border"></div>
<a target="_blank"></a>
</div>
<div id="mounts" style="display: none;"></div>
</td>
</tr>
</table>
<div id="item-slot-template" class="iconlarge"> <div id="item-slot-template" class="iconlarge">
<div class="inventory-slot"></div> <div class="inventory-slot"></div>
@ -92,11 +99,16 @@
}, },
mount: { mount: {
type: ZamModelViewer.Wow.Types.NPC, type: ZamModelViewer.Wow.Types.NPC,
//id: 25280,
id: 0, id: 0,
}, },
}; };
const viewer = new ZamModelViewer(characterModel); let viewer;
function createViewer() {
viewer?.destroy();
viewer = new ZamModelViewer(characterModel);
}
createViewer();
$("#cb-hide-helmet").change(() => { $("#cb-hide-helmet").change(() => {
const hide = $("#cb-hide-helmet").prop("checked"); const hide = $("#cb-hide-helmet").prop("checked");
@ -124,6 +136,7 @@
function clearSlots(slots) { function clearSlots(slots) {
viewer.method("clearSlots", slots.join(",")); viewer.method("clearSlots", slots.join(","));
characterModel.items = characterModel.items.filter(item => !slots.includes(item[0]));
} }
function setItems(items) { function setItems(items) {
@ -134,6 +147,9 @@
}; };
}); });
viewer.method("setItems", [data]); 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) { function createItemSlot(item, invSlot, icon, rel, container) {
@ -172,4 +188,39 @@
createItemSlot(item?.itemEntry, slot, item?.icon?.toLowerCase(), rel.join("&"), side.element); 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");
});
</script> </script>

View file

@ -3,28 +3,11 @@
display: none; 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 { .iconmedium.empty .icon-border {
background-position: 0px 0px; background-position: 0px 0px;
} }
.iconmedium.empty .talent { .iconmedium.empty .icon {
filter: grayscale(100%); filter: grayscale(100%);
opacity: 0.7; opacity: 0.7;
} }
@ -37,14 +20,6 @@
background-position: -42px 0px; background-position: -42px 0px;
} }
.iconmedium .border {
width: 44px;
height: 44px;
left: 0px;
top: 0px;
background-repeat: no-repeat;
}
.iconmedium .icon-border { .iconmedium .icon-border {
width: 42px; width: 42px;
height: 42px; height: 42px;
@ -54,22 +29,10 @@
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.iconmedium a {
width: 38px;
height: 38px;
position: absolute;
left: 3px;
top: 3px;
}
.iconmedium a span { .iconmedium a span {
display: none; display: none;
} }
.iconmedium a:hover {
background-position: 0 0;
}
.iconmedium::after { .iconmedium::after {
content: attr(rank); content: attr(rank);
background-color: black; background-color: black;

View file

@ -1,14 +1,8 @@
#item-slot-template { #item-slot-template,
#mount-template {
display: none; display: none;
} }
.iconlarge {
width: 68px;
height: 68px;
position: relative;
display: inline-block;
}
.iconlarge .inventory-slot { .iconlarge .inventory-slot {
width: 56px; width: 56px;
height: 56px; height: 56px;
@ -16,38 +10,18 @@
top: 6px; top: 6px;
position: absolute; position: absolute;
background-repeat: no-repeat; background-repeat: no-repeat;
} z-index: -2;
.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;
} }
.wowhead-tooltip .whtt-sellprice { .wowhead-tooltip .whtt-sellprice {
display: none; display: none;
} }
#mounts {
max-width: 400px;
}
#char-options td {
vertical-align: top;
padding-right: 32px;
}

75
static/css/icons.css Normal file
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,6 +1,6 @@
<link rel="stylesheet" href="/css/index.css"> <link rel="stylesheet" type="text/css" href="/css/index.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css"> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="/css/datatables.css"> <link rel="stylesheet" type="text/css" href="/css/datatables.css">
<script type="application/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script> <script type="application/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
<h1>Armory</h1> <h1>Armory</h1>

View file

@ -6,7 +6,7 @@
<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" href="/css/armory.css"> <link rel="stylesheet" type="text/css" href="/css/armory.css">
<script type="application/javascript" src="/js/sync-url.js"></script> <script type="application/javascript" src="/js/sync-url.js"></script>
</head> </head>

View file

@ -0,0 +1,22 @@
<link rel="stylesheet" type="text/css" href="/css/icons.css">
<style>
.iconlarge .border {
background-image: url("{{aowow}}/static/images/Icon/large/border/default.png");
}
.iconlarge a {
background: url("{{aowow}}/static/images/Icon/large/hilite/default.png") no-repeat 62px 0;
}
.iconmedium .border {
background-image: url("{{aowow}}/static/images/Icon/medium/border/default.png");
}
.iconmedium a {
background: url("{{aowow}}/static/images/Icon/medium/hilite/default.png") no-repeat 38px 0;
}
.iconmedium.icontalent .icon-border {
background-image: url("{{aowow}}/static/images/TalentCalc/border.gif");
}
</style>