diff --git a/src/app/features/item/item-template/item-constants.ts b/src/app/features/item/item-template/item-constants.ts
new file mode 100644
index 00000000000..17a678282a7
--- /dev/null
+++ b/src/app/features/item/item-template/item-constants.ts
@@ -0,0 +1,203 @@
+export const ITEM_CONSTANTS = {
+ 'timeUnits': {
+ sg: ['year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond'],
+ pl: ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'],
+ ab: ['yr', 'mo', 'wk', 'day', 'hr', 'min', 'sec', 'ms'],
+ },
+ 'pvpRank': [ // PVP_RANK_\d_\d(_FEMALE)?
+ null, 'Private / Scout', 'Corporal / Grunt',
+ 'Sergeant / Sergeant', 'Master Sergeant / Senior Sergeant', 'Sergeant Major / First Sergeant',
+ 'Knight / Stone Guard', 'Knight-Lieutenant / Blood Guard', 'Knight-Captain / Legionnare',
+ 'Knight-Champion / Centurion', 'Lieutenant Commander / Champion', 'Commander / Lieutenant General',
+ 'Marshal / General', 'Field Marshal / Warlord', 'Grand Marshal / High Warlord'
+ ],
+ 'si': { '-1': 'Alliance only', '-2': 'Horde only', 0: null, 1: 'Alliance', 2: 'Horde', 3: 'Both' },
+ 'resistances': [/* null, */ 'Holy Resistance', 'Fire Resistance', 'Nature Resistance', 'Frost Resistance', 'Shadow Resistance', 'Arcane Resistance'], // RESISTANCE?_NAME
+ 'sc': ['Physical', 'Holy', 'Fire', 'Nature', 'Frost', 'Shadow', 'Arcane'], // STRING_SCHOOL_*
+ 'cl': [null, 'Warrior', 'Paladin', 'Hunter', 'Rogue', 'Priest', 'Death Knight', 'Shaman', 'Mage', 'Warlock', null, 'Druid'], // ChrClasses.dbc
+ 'ra': { // ChrRaces.dbc
+ '-2': 'Horde',
+ '-1': 'Alliance',
+ 0: null,
+ 1: 'Human',
+ 2: 'Orc',
+ 3: 'Dwarf',
+ 4: 'Night Elf',
+ 5: 'Undead',
+ 6: 'Tauren',
+ 7: 'Gnome',
+ 8: 'Troll',
+ 9: null,
+ 10: 'Blood Elf',
+ 11: 'Draenei',
+ },
+ 'armor': '%s Armor', // ARMOR_TEMPLATE
+ 'block': '%s Block', // SHIELD_BLOCK_TEMPLATE
+ 'charges': '%d Charges', /* |4Charge: */ // ITEM_SPELL_CHARGES
+ 'locked': 'Locked', // LOCKED
+ 'ratingString': '%s @ L%s',
+ 'fap': 'Feral Attack Power',
+ 'durability': 'Durability %d / %d', // DURABILITY_TEMPLATE
+ 'itemLevel': 'Item Level %d', // ITEM_LEVEL
+ 'randEnchant': '<Random enchantment>', // ITEM_RANDOM_ENCHANT
+ 'readClick': '<Right Click To Read>', // ITEM_READABLE
+ 'openClick': '<Right Click To Open>', // ITEM_OPENABLE
+ 'setBonus': '(%d) Set: %s', // ITEM_SET_BONUS_GRAY
+ 'setName': '%s (%d/%d)', // ITEM_SET_NAME
+ 'reqMinLevel': 'Requires Level %d', // ITEM_MIN_LEVEL
+ 'reqLevelRange': 'Requires level %d to %d (%s)', // ITEM_LEVEL_RANGE_CURRENT
+ 'unique': ['Unique', 'Unique (%d)', 'Unique: %s (%d)' ], // ITEM_UNIQUE, ITEM_UNIQUE_MULTIPLE, ITEM_LIMIT_CATEGORY
+ 'uniqueEquipped': ['Unique-Equipped', null, 'Unique-Equipped: %s (%d)'], // ITEM_UNIQUE_EQUIPPABLE, null, ITEM_LIMIT_CATEGORY_MULTIPLE
+ 'dps': '(%.1f damage per second)', // DPS_TEMPLATE
+ 'damage': { // *DAMAGE_TEMPLATE*
+ // basic, basic /w school, add basic, add basic /w school
+ 'single': ['%d Damage', '%d %s Damage', '+ %d Damage', '+%d %s Damage' ],
+ 'range': ['%d - %d Damage', '%d - %d %s Damage', '+ %d - %d Damage', '+%d - %d %s Damage' ],
+ 'ammo': ['Adds %d damage per second', 'Adds %d %s damage per second', '+ %d damage per second', '+ %d %s damage per second' ],
+ },
+ 'socketBonus': 'Socket Bonus: %s', // ITEM_SOCKET_BONUS
+ 'socket': [
+ 'Meta Socket',
+ 'Red Socket',
+ 'Yellow Socket',
+ 'Blue Socket',
+ // -1 => 'Prismatic Socket' // TODO
+ ],
+ 'gemColors': [ // *_GEM
+ 'meta',
+ 'red',
+ 'yellow',
+ 'blue'
+ ],
+ 'gemConditions': { // ENCHANT_CONDITION_* in GlobalStrings.lua
+ 2: 'less than %s %s gems;',
+ 3: 'more %s gems than %s gems',
+ 5: 'at least %s %s gems',
+ },
+ 'reqRating': [ // ITEM_REQ_ARENA_RATING*
+ 'Requires personal and team arena rating of %d',
+ 'Requires personal and team arena rating of %d in 3v3 or 5v5 brackets',
+ 'Requires personal and team arena rating of %d in 5v5 bracket',
+ ],
+ 'quality': [ // ITEM_QUALITY?_DESC
+ 'Poor',
+ 'Common',
+ 'Uncommon',
+ 'Rare',
+ 'Epic',
+ 'Legendary',
+ 'Artifact',
+ 'Heirloom',
+ ],
+ 'trigger': [ // ITEM_SPELL_TRIGGER_*
+ 'Use: ',
+ 'Equip: ',
+ 'Chance on hit: ',
+ '',
+ '',
+ '',
+ '',
+ ],
+ 'bonding': [ // ITEM_BIND_*
+ 'Binds to account',
+ 'Binds when picked up',
+ 'Binds when equipped',
+ 'Binds when used',
+ 'Quest Item',
+ 'Quest Item'
+ ],
+ 'bagFamily': [ // ItemSubClass.dbc/1
+ 'Bag',
+ 'Quiver',
+ 'Ammo Pouch',
+ 'Soul Bag',
+ 'Leatherworking Bag',
+ 'Inscription Bag',
+ 'Herb Bag',
+ 'Enchanting Bag',
+ 'Engineering Bag',
+ null, /* Key */
+ 'Gem Bag',
+ 'Mining Bag',
+ ],
+ 'inventoryType': [ // INVTYPE_*
+ null, 'Head', 'Neck', 'Shoulder', 'Shirt',
+ 'Chest', 'Waist', 'Legs', 'Feet', 'Wrist',
+ 'Hands', 'Finger', 'Trinket', 'One-Hand', 'Off Hand', /*Shield*/
+ 'Ranged', 'Back', 'Two-Hand', 'Bag', 'Tabard',
+ 'Robe', 'Main Hand', 'Off Hand', 'Held In Off-Hand', 'Projectile',
+ 'Thrown', 'Ranged', 'Quiver', 'Relic'
+ ],
+ 'armorSubClass': [ // ItemSubClass.dbc/2
+ 'Miscellaneous', 'Cloth', 'Leather', 'Mail', 'Plate',
+ null, 'Shield', 'Libram', 'Idol', 'Totem',
+ 'Sigil'
+ ],
+ 'weaponSubClass': [ // ItemSubClass.dbc/4
+ 'Axe', 'Axe', 'Bow', 'Gun', 'Mace',
+ 'Mace', 'Polearm', 'Sword', 'Sword', null,
+ 'Staff', null, null, 'Fist Weapon', 'Miscellaneous',
+ 'Dagger', 'Thrown', null, 'Crossbow', 'Wand',
+ 'Fishing Pole',
+ ],
+ 'projectileSubClass': [ // ItemSubClass.dbc/6
+ null, null, 'Arrow', 'Bullet', null
+ ],
+ 'statType': [ // ITEM_MOD_*
+ 'Mana',
+ 'Health',
+ null,
+ 'Agility',
+ 'Strength',
+ 'Intellect',
+ 'Spirit',
+ 'Stamina',
+ null, null, null, null,
+ 'Increases defense rating by %d.',
+ 'Increases your dodge rating by %d.',
+ 'Increases your parry rating by %d.',
+ 'Increases your shield block rating by %d.',
+ 'Improves melee hit rating by %d.',
+ 'Improves ranged hit rating by %d.',
+ 'Improves spell hit rating by %d.',
+ 'Improves melee critical strike rating by %d.',
+ 'Improves ranged critical strike rating by %d.',
+ 'Improves spell critical strike rating by %d.',
+ 'Improves melee hit avoidance rating by %d.',
+ 'Improves ranged hit avoidance rating by %d.',
+ 'Improves spell hit avoidance rating by %d.',
+ 'Improves melee critical avoidance rating by %d.',
+ 'Improves ranged critical avoidance rating by %d.',
+ 'Improves spell critical avoidance rating by %d.',
+ 'Improves melee haste rating by %d.',
+ 'Improves ranged haste rating by %d.',
+ 'Improves spell haste rating by %d.',
+ 'Improves hit rating by %d.',
+ 'Improves critical strike rating by %d.',
+ 'Improves hit avoidance rating by %d.',
+ 'Improves critical avoidance rating by %d.',
+ 'Increases your resilience rating by %d.',
+ 'Increases your haste rating by %d.',
+ 'Increases expertise rating by %d.',
+ 'Increases attack power by %d.',
+ 'Increases ranged attack power by %d.',
+ 'Increases attack power by %d in Cat, Bear, Dire Bear, and Moonkin forms only.',
+ 'Increases damage done by magical spells and effects by up to %d.',
+ 'Increases healing done by magical spells and effects by up to %d.',
+ 'Restores %d mana per 5 sec.',
+ 'Increases your armor penetration rating by %d.',
+ 'Increases spell power by %d.',
+ 'Restores %d health per 5 sec.',
+ 'Increases spell penetration by %d.',
+ 'Increases the block value of your shield by %d.',
+ 'Unknown Bonus #%d (%d)',
+ ],
+ lockType: [ // lockType.dbc
+ null, 'Lockpicking', 'Herbalism', 'Mining', 'Disarm Trap',
+ 'Open', 'Treasure (DND)', 'Calcified Elven Gems (DND)', 'Close', 'Arm Trap',
+ 'Quick Open', 'Quick Close', 'Open Tinkering', 'Open Kneeling', 'Open Attacking',
+ 'Gahz\'ridian (DND)', 'Blasting', 'PvP Open', 'PvP Close', 'Fishing (DND)',
+ 'Inscription', 'Open From Vehicle'
+ ],
+
+};
diff --git a/src/app/features/item/item-template/item-preview.service.spec.ts b/src/app/features/item/item-template/item-preview.service.spec.ts
new file mode 100644
index 00000000000..5f82d06ff8f
--- /dev/null
+++ b/src/app/features/item/item-template/item-preview.service.spec.ts
@@ -0,0 +1,426 @@
+import { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ITEMS_QUALITY } from '@keira-shared/constants/options/item-quality';
+import { MysqlQueryService } from '@keira-shared/services/mysql-query.service';
+import { SqliteQueryService } from '@keira-shared/services/sqlite-query.service';
+import { MockedToastrService } from '@keira-shared/testing/mocks';
+import { ItemTemplate } from '@keira-shared/types/item-template.type';
+import { ToastrService } from 'ngx-toastr';
+import { of } from 'rxjs';
+import { instance } from 'ts-mockito';
+import { ItemHandlerService } from '../item-handler.service';
+import { Lock } from './item-preview';
+import { ItemPreviewService } from './item-preview.service';
+import { ItemTemplateService } from './item-template.service';
+import { ITEM_FLAG } from '@keira-shared/constants/flags/item-flags';
+import { ITEM_TYPE } from '@keira-shared/constants/options/item-class';
+
+describe('ItemPreviewService', () => {
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ ],
+ providers: [
+ { provide: ToastrService, useValue: instance(MockedToastrService) },
+ ItemPreviewService,
+ ItemTemplateService,
+ ItemHandlerService,
+ ]
+ }));
+
+ let service: ItemPreviewService;
+ let sqliteQueryService: SqliteQueryService;
+ let mysqlQueryService: MysqlQueryService;
+
+ const mockItemNameById = 'mockItemNameById';
+ const mockGetSpellNameById = 'mockGetSpellNameById';
+ const mockGetSpellDescriptionById = 'mockGetSpellDescriptionById';
+ const mockGetFactionNameById = 'mockGetFactionNameById';
+ const mockGetLockById: Lock[] = [null];
+ const mockGetMapNameById = 'mockGetMapNameById';
+ const mockGetAreaNameById = 'mockGetAreaNameById';
+ const mockGetEventNameByHolidayId = 'mockGetEventNameByHolidayId';
+ const mockGetSocketBonusById = 'mockGetSocketBonusById';
+
+ const lockData1 = {
+ id: 1, type1: 1, type2: 1, type3: 2, type4: 2, type5: 0,
+ properties1: 1, properties2: 0, properties3: 1, properties4: 0, properties5: 0,
+ reqSkill1: 0, reqSkill2: 0, reqSkill3: 1, reqSkill4: 0, reqSkill5: 0
+ };
+ const lockData2 = {
+ id: 2, type1: 2, type2: 2, type3: 2, type4: 2, type5: 2,
+ properties1: 25, properties2: 1, properties3: 1, properties4: 1, properties5: 0,
+ reqSkill1: 0, reqSkill2: 1, reqSkill3: 1, reqSkill4: 0, reqSkill5: 0
+ };
+
+ const locksData = [
+ [mockGetLockById],
+ [lockData2],
+ [lockData1],
+ [null],
+ ];
+
+ const npcVendor1 = [{ item: 600, entry: 600, eventId: 0, maxcount: 0, extendedCost: 600 }];
+ const npcVendor2 = [{ item: 601, entry: 601, eventId: 0, maxcount: 0, extendedCost: 601 }];
+
+ const mockItemEtendedCost1 = [
+ {
+ id: 600, reqHonorPoints: 20, reqArenaPoints: 4670, reqArenaSlot: 1, reqItemId1: 1, reqItemId2: 2, reqItemId3: 3, reqItemId4: 4,
+ reqItemId5: 5, itemCount1: 6, itemCount2: 7, itemCount3: 8, itemCount4: 9, itemCount5: 10, reqPersonalRating: 2200
+ }
+ ];
+ const mockItemEtendedCost2 = [
+ {
+ id: 601, reqHonorPoints: 0, reqArenaPoints: 0, reqArenaSlot: 0, reqItemId1: 0, reqItemId2: 0, reqItemId3: 0, reqItemId4: 0,
+ reqItemId5: 0, itemCount1: 6, itemCount2: 0, itemCount3: 0, itemCount4: 0, itemCount5: 0, reqPersonalRating: 0
+ }
+ ];
+
+ // items and itemset
+ const mockItems1 = [
+ { id: 123, slotBak: 1, itemset: 123 },
+ { id: 124, slotBak: 2, itemset: 123 },
+ { id: 125, slotBak: 3, itemset: 123 },
+ { id: 126, slotBak: 4, itemset: 123 },
+ { id: 127, slotBak: 5, itemset: 123 },
+ { id: 128, slotBak: 6, itemset: 123 },
+ { id: 129, slotBak: 1, itemset: 123 },
+ { id: 130, slotBak: 2, itemset: 123 },
+ { id: 131, slotBak: 3, itemset: 123 },
+ { id: 132, slotBak: 4, itemset: 123 },
+ { id: 133, slotBak: 5, itemset: 123 },
+ { id: 134, slotBak: 6, itemset: 123 },
+ ];
+
+ const mockItems2 = [
+ { id: 135, slotBak: 1, itemset: 138 },
+ { id: 136, slotBak: 2, itemset: 138 },
+ { id: 137, slotBak: 1, itemset: 138 },
+ { id: 138, slotBak: 2, itemset: 138 },
+ ];
+ const mockItems3 = [{ id: 150, slotBak: 1, itemset: 150 }];
+ const mockItems4 = [{ id: 152, slotBak: 1, itemset: 152 }];
+ const mockItems5 = [{ id: 153, slotBak: 1, itemset: 153 }, { id: 153, slotBak: 2, itemset: 153 }];
+
+ const mockItemset1 = [{ id: 123, name: 'Helias itemset', spell1: 1, bonus1: 2, bonus2: 1, skillId: 1, skillLevel: 1 }];
+ const mockItemset2 = [{ id: 123, name: 'Shin itemset', spell1: 1, spell2: 2, skillId: 1 }];
+ const mockItemset3 = [{ id: 123, name: 'Kalhac itemset' }];
+ const mockItemset4 = [{ id: 123, name: null }];
+
+ const id = 123;
+
+ beforeEach(() => {
+ mysqlQueryService = TestBed.inject(MysqlQueryService);
+ spyOn(mysqlQueryService, 'getItemNameById').and.callFake(i => of(i === 1 ? (mockItemNameById + i) : null).toPromise());
+ spyOn(mysqlQueryService, 'query').and.callFake((i) => {
+
+ if (i.indexOf('npc_vendor') > -1) {
+ if (i.indexOf('600') > -1) { return of(npcVendor1); }
+ if (i.indexOf('601') > -1) { return of(npcVendor2); }
+ }
+
+ if (i.indexOf('SELECT name FROM item_template') > -1) {
+ if (i.indexOf('153') > -1) { return of(null); }
+
+ if (i.indexOf('123') > -1) {
+ const itemsName = [{ name: '' }].concat(Array.from(new Array(11), (_, idx) => ({ name: 'Helias Item' + idx })));
+ return of(itemsName);
+ }
+
+ if (i.indexOf('138') > -1 || i.indexOf('150') > -1) { return of(Array.from(new Array(1), () => ({ name: 'Shin Item' }))); }
+ if (i.indexOf('152') > -1) { return of([{ name: null }]); }
+ }
+
+ return of(null);
+ });
+
+ sqliteQueryService = TestBed.inject(SqliteQueryService);
+ spyOn(sqliteQueryService, 'getSpellNameById').and.callFake(i => of(mockGetSpellNameById + i).toPromise());
+ spyOn(sqliteQueryService, 'getSpellDescriptionById').and.callFake(i =>
+ of(String(i).indexOf('555') > -1 ? null : mockGetSpellDescriptionById + i
+ ).toPromise());
+ spyOn(sqliteQueryService, 'getFactionNameById').and.callFake(i => of(mockGetFactionNameById + i).toPromise());
+ spyOn(sqliteQueryService, 'getMapNameById').and.callFake(i => of(
+ String(i).indexOf('123') > -1 ? '' : mockGetMapNameById + i
+ ).toPromise());
+ spyOn(sqliteQueryService, 'getAreaNameById').and.callFake(i => of(
+ String(i).indexOf('123') > -1 ? '' : mockGetAreaNameById + i
+ ).toPromise());
+ spyOn(sqliteQueryService, 'getEventNameByHolidayId').and.callFake(i => of(mockGetEventNameByHolidayId + i).toPromise());
+ spyOn(sqliteQueryService, 'getSocketBonusById').and.callFake(i => of(mockGetSocketBonusById + i).toPromise());
+ spyOn(sqliteQueryService, 'getLockById').and.callFake(i => of(locksData[i]).toPromise());
+ spyOn(sqliteQueryService, 'getSkillNameById').and.callFake(i => of(i === 1 ? 'profession' : null).toPromise());
+ spyOn(sqliteQueryService, 'query').and.callFake((i) => {
+
+ if (i.indexOf('item_extended_cost') > -1) {
+ if (i.indexOf('600') > -1) { return of(mockItemEtendedCost1); }
+ if (i.indexOf('601') > -1) { return of(mockItemEtendedCost2); }
+
+ return of([]);
+ }
+
+ if (i.indexOf('SELECT * FROM items ') > -1) {
+ if (i.indexOf('123') > -1) { return of(mockItems1); }
+ if (i.indexOf('138') > -1 || i.indexOf('140') > -1) { return of(mockItems2); }
+ if (i.indexOf('150') > -1 || i.indexOf('151') > -1) { return of(mockItems3); }
+ if (i.indexOf('152') > -1) { return of(mockItems4); }
+ if (i.indexOf('153') > -1) { return of(mockItems5); }
+ }
+
+ if (i.indexOf('SELECT * FROM itemset WHERE id = ') > -1) {
+ if (i.indexOf('id = 123') > -1) { return of(mockItemset1); }
+ if (i.indexOf('id = 138') > -1) { return of(mockItemset2); }
+ if (i.indexOf('id = 140') > -1) { return of(mockItemset3); }
+ if (i.indexOf('id = 150') > -1 || i.indexOf('id = 153') > -1) { return of(mockItemset4); }
+ }
+
+ if (i.indexOf('SELECT * FROM item_limit_category') > -1) {
+ if (i.indexOf('id = 123') > -1) { return of([{name: 'Helias', count: 2, isGem: 1 }]); }
+ if (i.indexOf('id = 124') > -1) { return of([]); }
+
+ return of([{name: 'Helias', count: 2, isGem: 0 }]);
+ }
+
+ if (i.indexOf('SELECT * FROM item_enchantment_condition') > -1) {
+ if (i.indexOf('100') > -1) { return of([
+ {
+ id: 100, color1: 4, color2: 1, color3: 3, comparator1: 4, comparator2: 2, comparator3: 3, comparator4: 4,
+ cmpColor1: 1, cmpColor2: 2, cmpColor3: 3, cmpColor4: 4, value1: 1, value2: 2, value3: 3, value4: 4,
+ }
+ ]);
+ }
+ if (i.indexOf('104') > -1) { return of([]); }
+ if (i.indexOf('105') > -1) { return of([null]); }
+ }
+
+ if (i.indexOf('SELECT * FROM item_enchantment') > -1) {
+ if (i.indexOf('100') > -1) { return of([{ id: 100, name: 'Helias', conditionId: 100 }]); }
+ if (i.indexOf('103') > -1) { return of([{ id: 103, name: '', conditionId: 0 }]); }
+ if (i.indexOf('104') > -1) { return of([{ id: 104, name: '', conditionId: 104 }]); }
+ if (i.indexOf('105') > -1) { return of([{ id: 105, name: '', conditionId: 105 }]); }
+ }
+
+ return of(null);
+ });
+ spyOn(sqliteQueryService, 'queryValue').and.callFake((i) => {
+ if (i.indexOf('SELECT gemEnchantmentId AS v') > -1) {
+ if (i.indexOf('id = 100') > -1) { return of(100); }
+ if (i.indexOf('id = 101') > -1) { return of(null); }
+ if (i.indexOf('id = 102') > -1) { return of(102); }
+ if (i.indexOf('id = 103') > -1) { return of(103); }
+ if (i.indexOf('id = 104') > -1) { return of(104); }
+ if (i.indexOf('id = 105') > -1) { return of(105); }
+ }
+
+ return of(null);
+ });
+
+ service = TestBed.inject(ItemPreviewService);
+ });
+
+ it('getItemExtendedCostFromVendor', () => {
+ service['getItemExtendedCostFromVendor'](123);
+ expect(mysqlQueryService.query).toHaveBeenCalledTimes(1);
+ });
+
+ it('getItemsetSlotBak', () => {
+ service['getItemsetSlotBak'](id);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM items WHERE itemset = ${id} ORDER BY slotBak, id`);
+ });
+
+ it('getItemNameByIDsASC', () => {
+ const IDs = [123, 1234];
+ service['getItemNameByIDsASC'](IDs);
+ expect(mysqlQueryService.query).toHaveBeenCalledTimes(1);
+ expect(mysqlQueryService.query).toHaveBeenCalledWith(`SELECT name FROM item_template WHERE entry IN (${IDs.join(',')}) ORDER BY entry ASC`);
+ });
+
+ it('getItemsetById', () => {
+ service['getItemsetById'](id);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM itemset WHERE id = ${id}`);
+ });
+
+ it('getItemLimitCategoryById', () => {
+ service['getItemLimitCategoryById'](id);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM item_limit_category WHERE id = ${id}`);
+ });
+
+ it('getGemEnchantmentIdById', () => {
+ service['getGemEnchantmentIdById'](id);
+ expect(sqliteQueryService.queryValue).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.queryValue).toHaveBeenCalledWith(`SELECT gemEnchantmentId AS v FROM items WHERE id = ${id};`);
+ });
+
+ it('getItemEnchantmentById', () => {
+ service['getItemEnchantmentById'](id);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM item_enchantment WHERE id = ${id}`);
+ });
+
+ it('getItemExtendedCost', () => {
+ const IDs = [123, 1234];
+ service['getItemExtendedCost'](IDs);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM item_extended_cost WHERE id IN (${IDs.join(',')})`);
+ });
+
+ it('getItemEnchantmentConditionById', () => {
+ service['getItemEnchantmentConditionById'](id);
+ expect(sqliteQueryService.query).toHaveBeenCalledTimes(1);
+ expect(sqliteQueryService.query).toHaveBeenCalledWith(`SELECT * FROM item_enchantment_condition WHERE id = ${id}`);
+ });
+
+ const cases = [
+ { name: 'Empty variables', template: { }, output: '' },
+ { name: 'Item Name & Quality', template: { name: 'Helias Item', Quality: 1 }, output: `Helias Item` },
+ { name: 'Quality 0', template: { name: 'Helias Item', Quality: 0 }, output: `Helias Item` },
+ { name: 'Quality null', template: { name: 'Helias Item' }, output: `Helias Item` },
+ { name: 'All Flags & Quality Epic', template: { Quality: ITEMS_QUALITY.EPIC, Flags: -1 }, output: `
Heroic
Conjured Item
Binds to account
Unique-Equipped` },
+ { name: 'Empty variables', template: { HolidayId: 140 }, output: `
Requires ${mockGetEventNameByHolidayId}140` },
+ { name: 'StartQuest', template: { startquest: 1 }, output: `
This Item Begins a Quest` },
+ { name: 'ContainerSlots', template: { ContainerSlots: 1 }, output: `
1 Slot Bag` },
+ { name: 'ContainerSlots-2', template: { ContainerSlots: 1, BagFamily: 2 }, output: `
1 Slot Ammo Pouch` },
+ { name: 'RandomProperty', template: { RandomProperty: 1 }, output: `
<Random enchantment>` },
+ { name: 'RandomSuffix', template: { RandomSuffix: 1 }, output: `
<Random enchantment>` },
+ { name: 'Durability', template: { MaxDurability: 100 }, output: `
Durability 100 / 100` },
+ { name: 'Sell Price', template: { SellPrice: 123456 }, output: `
Sell Price: 12 34 56 ` },
+ { name: 'Sell Price-1', template: { SellPrice: 9999 }, output: `
Sell Price: 99 99 ` },
+ { name: 'Sell Price-2', template: { SellPrice: 99 }, output: `
Sell Price: 99 ` },
+ { name: 'Sell Price-3', template: { SellPrice: -1 }, output: `` },
+ { name: 'Stats 1', template: { stat_type1: 1, stat_value1: 1 }, output: `
+1 Health` },
+ { name: 'Stats -1', template: { stat_type1: 1, stat_value1: -1 }, output: `
-1 Health` },
+ { name: 'Stats type null', template: { stat_type1: 8, stat_value1: 1 }, output: `` },
+ { name: 'Stats 12, ReqLvL', template: { stat_type1: 12, stat_value1: 1, RequiredLevel: 71 }, output: `
Requires Level 71
Equip: Increases defense rating by 1 (0.39 @ L71).` },
+ { name: 'Stats 13, ReqLvL', template: { stat_type1: 13, stat_value1: 1, RequiredLevel: 71 }, output: `
Requires Level 71
Equip: Increases your dodge rating by 1 (0.04% @ L71).` },
+ { name: 'Stats 13, ReqLvL', template: { stat_type1: 13, stat_value1: 1, RequiredLevel: 61 }, output: `
Requires Level 61
Equip: Increases your dodge rating by 1 (0.07% @ L61).` },
+ { name: 'Stats 13, ReqLvL', template: { stat_type1: 13, stat_value1: 1, RequiredLevel: 11 }, output: `
Requires Level 11
Equip: Increases your dodge rating by 1 (0.14% @ L34).` },
+ { name: 'Stats 16, ReqLvL', template: { stat_type1: 16, stat_value1: 1, RequiredLevel: 8 }, output: `
Requires Level 8
Equip: Improves melee hit rating by 1 (2.60% @ L8).` },
+ { name: 'Stats 38, ReqLvL', template: { stat_type1: 38, stat_value1: 1, RequiredLevel: 8 }, output: `
Requires Level 8
Equip: Increases attack power by 1.` },
+ { name: 'Stats 48, ReqLvL', template: { stat_type1: 48, stat_value1: 1, RequiredLevel: 8 }, output: `
Requires Level 8
Equip: Increases the block value of your shield by 1.` },
+ { name: 'Stats 39, ReqLvL', template: { stat_type1: 39, stat_value1: 1, RequiredLevel: 8 }, output: `
Requires Level 8
Equip: Increases ranged attack power by 1 (% @ L8).` },
+ { name: 'AllowableClass Paladin', template: { AllowableClass: 2 }, output: `
Classes: Paladin` },
+ { name: 'AllowableClass All', template: { AllowableClass: -1 }, output: `` },
+ { name: 'AllowableRace All', template: { AllowableRace: -1 }, output: `` },
+ { name: 'AllowableRace Alliance', template: { AllowableRace: 1101 }, output: `
Races: Alliance` },
+ { name: 'AllowableRace Horde', template: { AllowableRace: 690 }, output: `
Races: Horde` },
+ { name: 'AllowableRace Human', template: { AllowableRace: 1 }, output: `
Races: Human` },
+ { name: 'AllowableRace Orc', template: { AllowableRace: 2 }, output: `
Races: Orc` },
+ { name: 'Duration 1 day', template: { duration: 3600 * 24 }, output: `
Duration: 1 day` },
+ { name: 'Duration 2 days', template: { duration: 3600 * 24 * 2 }, output: `
Duration: 2 days` },
+ { name: 'Duration 1 hour', template: { duration: 3600 }, output: `
Duration: 1 hour` },
+ { name: 'Duration 2 hours', template: { duration: 3600 * 2 }, output: `
Duration: 2 hours` },
+ { name: 'Duration 1 minute', template: { duration: 60 }, output: `
Duration: 1 minute` },
+ { name: 'Duration 2 minutes', template: { duration: 60 * 2 }, output: `
Duration: 2 minutes` },
+ { name: 'Duration 1 seconds', template: { duration: 1 }, output: `
Duration: 1 second` },
+ { name: 'Duration 10 secs', template: { duration: 10 }, output: `
Duration: 10 seconds` },
+ { name: 'Duration -1 ms', template: { duration: 0.001 }, output: `
Duration: 1 millisecond` },
+ { name: 'Duration -1 ms', template: { duration: -0.001 }, output: `
Duration: -1 millisecond` },
+ { name: 'Duration -50 ms', template: { duration: -0.05 }, output: `
Duration: -50 milliseconds` },
+ { name: 'Duration 0 sec', template: { duration: -5 }, output: `
Duration: 0 seconds` },
+ { name: 'Duration 1 week', template: { duration: (3600 * 24 * 7) }, output: `
Duration: 1 week` },
+ { name: 'Duration 2 weeks', template: { duration: (3600 * 24 * 7 * 2) }, output: `
Duration: 2 weeks` },
+ { name: 'Duration 1 month', template: { duration: 3600 * 24 * 30 }, output: `
Duration: 1 month` },
+ { name: 'Duration 2 months', template: { duration: 3600 * 24 * 30 * 2 }, output: `
Duration: 2 months` },
+ { name: 'Duration 1 year', template: { duration: 3600 * 24 * 364 }, output: `
Duration: 1 year` },
+ { name: 'Duration 2 years', template: { duration: 3600 * 24 * 364 * 2 }, output: `
Duration: 2 years` },
+ { name: 'Duration (real time)', template: { duration: 1020, flagsCustom: 1 }, output: `
Duration: 17 minutes (real time)` },
+ { name: 'Feral Attack Power 1', template: {
+ dmg_min1: 10, dmg_min2: 10, dmg_max1: 10, dmg_max2: 10, delay: 1000, class: 10
+ }, output: `
10 Damage+ 10 Damage` },
+ { name: 'Feral Attack Power 2', template: {
+ dmg_min1: 10, dmg_min2: 10, dmg_max1: 10, dmg_max2: 10, delay: 1000, class: 2, subclass: 2
+ }, output: `
+ 10 Damage
(20.00 damage per second)` },
+ { name: 'Feral Attack Power 3', template: {
+ dmg_min1: 100, dmg_min2: 100, dmg_max1: 500, dmg_max2: 500, delay: 1000, class: 2, subclass: 5
+ }, output: `100 - 500 Damage | | Speed 1.00 |
---|
+ 100 - 500 Damage
(600.00 damage per second)
(7633 Feral Attack Power)` },
+ { name: 'Feral Attack Power 4', template: {
+ dmg_min1: 1, dmg_min2: 1, dmg_max1: 5, dmg_max2: 5, delay: 1000, class: 2, subclass: 5
+ }, output: `+ 1 - 5 Damage
(6.00 damage per second)` },
+ { name: 'Spell ID 1', template: { spellid_1: 1, spellid_2: 1 }, output: `
${mockGetSpellDescriptionById}1` },
+ { name: 'Spell ID 2', template: { spellid_2: 1 }, output: `
${mockGetSpellDescriptionById}1` },
+ { name: 'Spell ID 483', template: { spellid_1: 483 },
+ output: `
${mockGetSpellDescriptionById}483`
+ },
+ { name: 'Spell ID 55884', template: { spellid_1: 55884 },
+ output: `
${mockGetSpellDescriptionById}55884`
+ },
+ { name: 'Spell ID 123, 123 charges', template: { spellid_1: 123, spellcharges_1: 123, PageText: 123}, output: `
mockGetSpellDescriptionById123
<Right Click To Read>
123 Charges` },
+ { name: 'Spell ID 123, 1 charge', template: { spellid_1: 123, spellcharges_1: 1, description: 'spellDesc' }, output: `
mockGetSpellDescriptionById123
"spellDesc"
1 Charge` },
+ { name: 'Spell ID 123, no charges', template: { spellid_1: 123, }, output: `
mockGetSpellDescriptionById123` },
+ { name: 'LockId 1', template: { Flags: 1, lockid: 1 }, output: `
Locked
Requires Lockpicking (1)
Requires Lockpicking (1)
Requires Lockpicking` },
+ { name: 'LockId 2', template: { Flags: 1, lockid: 2 }, output: `
Locked
Requires mockItemNameById1
Requires Lockpicking (1)` },
+ { name: 'LockId 3', template: { Flags: 1, lockid: 3 }, output: `` },
+ { name: 'LockId 4', template: { Flags: ITEM_FLAG.OPENABLE, lockid: 3 }, output: `
<Right Click To Open>` },
+ { name: 'Req HonorRank', template: { requiredhonorrank: 1 }, output: `
Requires Private / Scout` },
+ { name: 'Req Level Range', template: { Quality: ITEMS_QUALITY.HEIRLOOM, Flags: ITEM_FLAG.ACCOUNTBOUND }, output: `
Binds to account
Requires level 1 to 80 (80)` },
+ { name: 'Damage 1', template: {
+ class: ITEM_TYPE.AMMUNITION, dmg_min1: 10, dmg_min2: 10, dmg_max1: 10, dmg_max2: 10, delay: 1000,
+ }, output: `Adds 20 damage per second` },
+ { name: 'Damage 2', template: {
+ class: ITEM_TYPE.AMMUNITION, dmg_min1: 10, dmg_min2: 10, dmg_max1: 10, dmg_max2: 10, delay: 1000, dmg_type1: 1,
+ }, output: `Adds 20 Holy damage per second` },
+ { name: 'Damage 3', template: {
+ dmg_min1: 10, dmg_min2: 10, dmg_max1: 10, dmg_max2: 10, delay: 1000, dmg_type1: 1, dmg_type2: 2,
+ }, output: `
10 Holy Damage+10 Fire Damage` },
+ { name: 'Damage 4', template: {
+ dmg_min1: 5, dmg_min2: 5, dmg_max1: 10, dmg_max2: 10, delay: 1000, dmg_type1: 1, dmg_type2: 2,
+ }, output: `
5 - 10 Holy Damage+5 - 10 Fire Damage` },
+ { name: 'Itemset full success', template: { itemset: 123, entry: 123 }, output: `
Helias itemset (0/12)
Requires profession (1)
Helias Item0
Helias Item1
Helias Item2
Helias Item3
Helias Item4
Helias Item5
Helias Item6
Helias Item7
Helias Item8
Helias Item9
Helias Item10
(2) Set: mockGetSpellDescriptionById1` },
+ { name: 'Itemset, items < 10, no skillLevel', template: { itemset: 138 }, output: `
Shin itemset (0/1)
Requires profession
Shin Item
` },
+ { name: 'Itemset - no bonus and skill', template: { itemset: 140 }, output: `
Kalhac itemset (0/1)
Shin Item
` },
+ { name: 'Itemset - no itemset name', template: { itemset: 150, }, output: `
(0/1)
Shin Item
` },
+ { name: 'Itemset - no itemset', template: { itemset: 151, }, output: `` },
+ { name: 'Itemset - itemsName with null name', template: { itemset: 152, }, output: `` },
+ { name: 'Itemset - no itemsName', template: { itemset: 153, }, output: `` },
+ { name: 'Itemset - no itemsetPieces', template: { itemset: 154, }, output: `` },
+ { name: 'Bonding, unique', template: { bonding: 1, maxcount: 1 }, output: `
Binds when picked up
Unique` },
+ { name: 'Maxcount cases', template: { maxcount: 2, BagFamily: 1 }, output: `
Unique (2)` },
+ { name: 'Item Limit Category, is Gem', template: { ItemLimitCategory: 123 }, output: `
Unique-Equipped: Helias (2)` },
+ { name: 'Item Limit Category, not Gem', template: { ItemLimitCategory: 1 }, output: `
Unique: Helias (2)` },
+ { name: 'Item Limit Category, no data', template: { ItemLimitCategory: 124 }, output: `` },
+ {
+ name: 'inventoryType, class ARMOR, armorDamager',
+ template: { class: ITEM_TYPE.ARMOR, subclass: 1, InventoryType: 1, ArmorDamageModifier: 1, armor: 1 },
+ output: `
1 Armor`
+ },
+ { name: 'inventoryType - 1', template: { class: 3, subclass: 2, InventoryType: 2 }, output: `
Neck` },
+ { name: 'inventoryType, armor', template: { class: ITEM_TYPE.WEAPON, armor: 1, ItemLevel: 10, }, output: `
1 Armor
Item Level 10` },
+ { name: 'inventoryType, block', template: { class: ITEM_TYPE.AMMUNITION, block: 1 }, output: `
1 Block` },
+ { name: 'inventoryType, subclass', template: { class: ITEM_TYPE.AMMUNITION, subclass: 2 }, output: `` },
+ { name: 'Skill, SkillRank', template: {
+ RequiredSkill: 1, RequiredReputationFaction: 1, RequiredReputationRank: 1, RequiredSkillRank: 10
+ }, output: `
Requires: profession (10)
Requires mockGetFactionNameById1 (1)` },
+ { name: 'Skill, Spell, Map, Area', template: { RequiredSkill: 1, requiredspell: 1, RequiredReputationFaction: 1, Map: 1, area: 1 },
+ output: `
mockGetMapNameById1
mockGetAreaNameById1
Requires: profession
Requires mockGetSpellNameById1
Requires mockGetFactionNameById1`
+ },
+ { name: 'Map, Area null check', template: { Map: 123, area: 123 }, output: `` },
+ { name: 'Holy & Arcane resistances', template: { holy_res: 1, arcane_res: 1 }, output: `
+1 Holy Resistance
+1 Arcane Resistance` },
+ { name: 'Gem Enchantment - success', template: { entry: 100, }, output: `
Helias
Requires less than 2 meta gems;
Requires more yellow gems than yellow gems` },
+ { name: 'Gem Enchantment null check - 1 ', template: { entry: 101, }, output: `` },
+ { name: 'Gem Enchantment null check - 2 ', template: { entry: 102, }, output: `` },
+ { name: 'Gem Enchantment null check - 3 ', template: { entry: 103, }, output: `` },
+ { name: 'Gem Enchantment null check - 4 ', template: { entry: 104, }, output: `` },
+ { name: 'Gem Enchantment null check - 5 ', template: { entry: 105, }, output: `` },
+ { name: 'Sockets ', template: { socketColor_1: 0, socketColor_2: 1, socketColor_3: 2, socketBonus: 1 }, output: `
Meta Socket
Red Socket
Socket Bonus: mockGetSocketBonusById1` },
+ { name: 'getSpellDesc ', template: {
+ spellid_1: 1, spellid_2: 2, spellcooldown_1: 2, spellcooldown_2: 1, spellcategory_1: 1, spellcategory_2: 2
+ }, output: `
mockGetSpellDescriptionById1
mockGetSpellDescriptionById2` },
+ { name: 'getSpellDesc null check - 1', template: { spellid_1: 1, spellid_2: 2 }, output: `
mockGetSpellDescriptionById1
mockGetSpellDescriptionById2` },
+ { name: 'getSpellDesc null check - 2', template: { spellid_1: 483, spellid_2: 555 }, output: `` },
+ { name: 'getLearnSpellText', template: { spellid_1: 483, spellid_2: 124 }, output: `
Use: mockGetSpellDescriptionById124` },
+ { name: 'Extended Cost - 1', template: { entry: 600, FlagsExtra: 0x04, BuyPrice: 1234 }, output: `
Requires personal and team arena rating of 2200 in 3v3 or 5v5 brackets`},
+ { name: 'Extended Cost - 2', template: { entry: 600, FlagsExtra: 0x04 }, output: `
Requires personal and team arena rating of 2200 in 3v3 or 5v5 brackets`},
+ { name: 'Extended Cost - 3', template: { entry: 600 }, output: `
Requires personal and team arena rating of 2200 in 3v3 or 5v5 brackets`},
+ { name: 'Extended Cost - 4', template: { entry: 601 }, output: ``},
+ ];
+
+ for (const { name, template, output } of cases) {
+ it(`Case ${name}`, async() => {
+ expect(await service.calculatePreview(template as ItemTemplate)).toEqual(output);
+ });
+ }
+
+});
diff --git a/src/app/features/item/item-template/item-preview.service.ts b/src/app/features/item/item-template/item-preview.service.ts
new file mode 100644
index 00000000000..bbb8e04ce75
--- /dev/null
+++ b/src/app/features/item/item-template/item-preview.service.ts
@@ -0,0 +1,1399 @@
+import { Injectable } from '@angular/core';
+import { MysqlQueryService } from '@keira-shared/services/mysql-query.service';
+import { SqliteQueryService } from '@keira-shared/services/sqlite-query.service';
+import { ITEM_TYPE, ITEM_MOD } from '@keira-shared/constants/options/item-class';
+import { ITEM_CONSTANTS } from './item-constants';
+import { MAX_LEVEL, lvlIndepRating, gtCombatRatings, CLASSES, RACE, resistanceFields } from './item-preview';
+import { ITEM_FLAG } from '@keira-shared/constants/flags/item-flags';
+import { ITEMS_QUALITY } from '@keira-shared/constants/options/item-quality';
+import { ItemTemplate } from '@keira-shared/types/item-template.type';
+
+@Injectable()
+export class ItemPreviewService {
+ private readonly ITEM_CONSTANTS = ITEM_CONSTANTS;
+
+ /* istanbul ignore next */ // because of: https://github.com/gotwarlost/istanbul/issues/690
+ constructor(
+ private readonly sqliteQueryService: SqliteQueryService,
+ private readonly mysqlQueryService: MysqlQueryService,
+ ) { }
+
+ /**
+ * query functions
+ */
+
+ private getItemsetSlotBak(itemset: number | string): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM items WHERE itemset = ${itemset} ORDER BY slotBak, id`).toPromise();
+ }
+
+ private getItemNameByIDsASC(IDs: number[]): Promise {
+ return this.mysqlQueryService.query(`SELECT name FROM item_template WHERE entry IN (${IDs.join(',')}) ORDER BY entry ASC`).toPromise();
+ }
+
+ private getItemsetById(ID: number | string): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM itemset WHERE id = ${ID}`).toPromise();
+ }
+
+ private getItemLimitCategoryById(id: number | string): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM item_limit_category WHERE id = ${id}`).toPromise();
+ }
+
+ private getGemEnchantmentIdById(id: number | string): Promise {
+ return this.sqliteQueryService.queryValue(`SELECT gemEnchantmentId AS v FROM items WHERE id = ${id};`).toPromise();
+ }
+
+ private getItemEnchantmentById(id: number | string): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM item_enchantment WHERE id = ${id}`).toPromise();
+ }
+
+ private getItemExtendedCost(IDs: number[]): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM item_extended_cost WHERE id IN (${IDs.join(',')})`).toPromise();
+ }
+
+ private getItemEnchantmentConditionById(id: number | string): Promise {
+ return this.sqliteQueryService.query(`SELECT * FROM item_enchantment_condition WHERE id = ${id}`).toPromise();
+ }
+
+ private getItemExtendedCostFromVendor(id: number | string): Promise {
+ return this.mysqlQueryService.query(`SELECT
+ nv.item,
+ nv.entry,
+ 0 AS eventId,
+ nv.maxcount,
+ nv.extendedCost
+ FROM
+ npc_vendor nv
+ WHERE
+ nv.item = ${id}
+ UNION SELECT
+ genv.item,
+ c.id AS \`entry\`,
+ ge.eventEntry AS eventId,
+ genv.maxcount,
+ genv.extendedCost
+ FROM
+ game_event_npc_vendor genv
+ LEFT JOIN
+ game_event ge ON genv.eventEntry = ge.eventEntry
+ JOIN
+ creature c ON c.guid = genv.guid
+ WHERE
+ genv.item = ${id};`).toPromise();
+ }
+
+ /**
+ * utils
+ */
+
+ private setRatingLevel(level: number, type: number, val: number): string {
+ let result = '';
+ const rating = [
+ ITEM_MOD.DEFENSE_SKILL_RATING,
+ ITEM_MOD.DODGE_RATING,
+ ITEM_MOD.PARRY_RATING,
+ ITEM_MOD.BLOCK_RATING,
+ ITEM_MOD.RESILIENCE_RATING
+ ];
+
+ if (rating.includes(type) && level < 34) {
+ level = 34;
+ }
+
+ if (gtCombatRatings[type]) {
+ let c: number;
+ if (level > 70) {
+ c = 82 / 52 * Math.pow(131 / 63, (level - 70) / 10);
+ } else if (level > 60) {
+ c = 82 / (262 - 3 * level);
+ } else if (level > 10) {
+ c = (level - 8) / 52;
+ } else {
+ c = 2 / 52;
+ }
+ // do not use localized number format here!
+ result = (val / gtCombatRatings[type] / c).toFixed(2);
+ }
+
+ if (![ITEM_MOD.DEFENSE_SKILL_RATING, ITEM_MOD.EXPERTISE_RATING].includes(type)) {
+ result += '%';
+ }
+
+ return ITEM_CONSTANTS.ratingString
+ .replace('%s', `${result}`)
+ .replace('%s', `${level}`);
+ }
+
+ private parseRating(type: number, value: number, requiredLevel: number): string {
+ // clamp level range
+ const level = requiredLevel > 1 ? requiredLevel : MAX_LEVEL;
+
+ // unknown rating
+ if ([2, 8, 9, 10, 11].includes(type) || type > ITEM_MOD.BLOCK_VALUE || type < 0 || !ITEM_CONSTANTS.statType[type]) {
+ return '';
+ }
+
+ if (lvlIndepRating.includes(type)) { // level independant Bonus
+ return ITEM_CONSTANTS.trigger[1] + ITEM_CONSTANTS.statType[type].replace('%d', `${value}`);
+ }
+
+ // rating-Bonuses
+ const js = ` (${this.setRatingLevel(level, type, value)})`;
+ return ITEM_CONSTANTS.trigger[1] + ITEM_CONSTANTS.statType[type].replace('%d', `${value}${js}`);
+ }
+
+ private getRequiredClass(classMask: number): string[] {
+ classMask &= CLASSES.MASK_ALL; // clamp to available classes..
+
+ if (classMask === CLASSES.MASK_ALL) { // available to all classes
+ return null;
+ }
+
+ const tmp = [];
+ let i = 1;
+ while (classMask) {
+ if (classMask & (1 << (i - 1))) {
+ const tmpClass = ITEM_CONSTANTS.cl[i];
+ /* istanbul ignore else */
+ if (!!tmpClass) {
+ tmp.push(`${tmpClass}`);
+ }
+
+ classMask &= ~(1 << (i - 1));
+ }
+ i++;
+ }
+
+ return tmp;
+ }
+
+ private getRaceString(raceMask: number): string[] {
+ // clamp to available races
+ raceMask &= RACE.MASK_ALL;
+ // available to all races (we don't display 'both factions')
+ if (!raceMask || raceMask === RACE.MASK_ALL) {
+ return null;
+ }
+
+ if (raceMask === RACE.MASK_HORDE) {
+ return [ITEM_CONSTANTS.ra['-2']];
+ }
+
+ if (raceMask === RACE.MASK_ALLIANCE) {
+ return [ITEM_CONSTANTS.ra['-1']];
+ }
+
+ const tmp = [];
+ let i = 1;
+ while (raceMask) {
+ if (raceMask & (1 << (i - 1))) {
+ const tmpRace = ITEM_CONSTANTS.ra[i];
+ /* istanbul ignore else */
+ if (!!tmpRace) {
+ tmp.push(tmpRace);
+ }
+ raceMask &= ~(1 << (i - 1));
+ }
+ i++;
+ }
+
+ return tmp;
+ }
+
+ private formatMoney(qty: number): string {
+ let money = '';
+
+ if (qty >= 10000) {
+ const g = Math.floor(qty / 10000);
+ money += `${g} `;
+ qty -= g * 10000;
+ }
+
+ if (qty >= 100) {
+ const s = Math.floor(qty / 100);
+ money += `${s} `;
+ qty -= s * 100;
+ }
+
+ money += `${qty} `;
+
+ return money;
+ }
+
+ private parseTime(sec: number) {
+ const time = {
+ d: 0,
+ h: 0,
+ m: 0,
+ s: 0,
+ ms: 0,
+ };
+
+ if (sec >= 3600 * 24) {
+ time['d'] = Math.floor(sec / 3600 / 24);
+ sec -= time['d'] * 3600 * 24;
+ }
+
+ if (sec >= 3600) {
+ time.h = Math.floor(sec / 3600);
+ sec -= time['h'] * 3600;
+ }
+
+ if (sec >= 60) {
+ time.m = Math.floor(sec / 60);
+ sec -= time.m * 60;
+ }
+
+ if (sec >= 1) {
+ time['s'] = sec;
+ sec -= time['s'];
+ }
+
+ if ((sec * 1000) % 1000) {
+ time.ms = sec * 1000;
+ }
+
+ return time;
+ }
+
+ private formatTime(base: number) {
+ const s = this.parseTime(base / 1000);
+ let tmp: number;
+
+ tmp = s.d + s.h / 24;
+ if (tmp > 1 && !(tmp % 364)) { // whole years
+ return Math.round((s.d + s.h / 24) / 364) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.d / 364) === 1 && !s.h ? 'sg' : 'pl'][0];
+ }
+ if (tmp > 1 && !(tmp % 30)) { // whole month
+ return Math.round((s.d + s.h / 24) / 30) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.d / 30) === 1 && !s.h ? 'sg' : 'pl'][1];
+ }
+ if (tmp > 1 && !(tmp % 7)) { // whole weeks
+ return Math.round((s.d + s.h / 24) / 7) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.d / 7) === 1 && !s.h ? 'sg' : 'pl'][2];
+ }
+ if (s.d !== 0) {
+ return Math.round(s.d + s.h / 24) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.d) === 1 && !s.h ? 'sg' : 'pl'][3];
+ }
+ if (s.h !== 0) {
+ return Math.round(s.h + s.m / 60) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.h) === 1 && !s.m ? 'sg' : 'pl'][4];
+ }
+ if (s.m !== 0) {
+ return Math.round(s.m + s.s / 60) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.m) === 1 && !s.s ? 'sg' : 'pl'][5];
+ }
+ if (s.s !== 0) {
+ return Math.round(s.s + s.ms / 1000) + ' ' + ITEM_CONSTANTS.timeUnits[Math.abs(s.s) === 1 && !s.ms ? 'sg' : 'pl'][6];
+ }
+ if (s.ms !== 0) {
+ return s.ms + ' ' + ITEM_CONSTANTS.timeUnits[ Math.abs(s.ms) === 1 ? 'sg' : 'pl'][7];
+ }
+
+ return '0 ' + ITEM_CONSTANTS.timeUnits.pl[6];
+ }
+
+ private getFeralAP(itemClass: number, subclass: number, dps: number): number {
+ // must be weapon
+ if (itemClass !== ITEM_TYPE.WEAPON) {
+ return 0;
+ }
+
+ // must be 2H weapon (2H-Mace, Polearm, Staff, ..Fishing Pole)
+ if (![5, 6, 10, 20].includes(subclass)) {
+ return 0;
+ }
+
+ // must have enough damage
+ if (dps < 54.8) {
+ return 0;
+ }
+
+ return Math.round((dps - 54.8) * 14);
+ }
+
+ private canTeachSpell(spellId1: number, spellId2: number = null): boolean {
+ // 483: learn recipe;
+ // 55884: learn mount/pet
+ if (![483, 55884].includes(spellId1)) {
+ return false;
+ }
+
+ // needs learnable spell
+ return !!spellId2;
+ }
+
+ private async getLocks(lockId: number): Promise {
+ /* istanbul ignore next */
+ if (!lockId) { return ['']; }
+
+ const lock = (await this.sqliteQueryService.getLockById(lockId))[0];
+
+ if (!lock) {
+ return [];
+ }
+
+ const locks = [];
+
+ for (let i = 1; i <= 5; i++) {
+ const prop = lock['properties' + i];
+ const rank = lock['reqSkill' + i];
+ let name = '';
+
+ const lockType = Number(lock['type' + i]);
+
+ if (lockType === 1) { // opened by item
+ name = await this.mysqlQueryService.getItemNameById(prop);
+
+ if (!name) {
+ continue;
+ }
+ } else if (lockType === 2) { // opened by skill
+
+ // exclude unusual stuff
+ if (!ITEM_CONSTANTS.lockType[prop] || ![1, 2, 3, 4, 9, 16, 20].includes(Number(prop))) {
+ continue;
+ }
+
+ name = ITEM_CONSTANTS.lockType[prop];
+
+ if (rank > 0) {
+ name += ` (${rank})`;
+ }
+ } else {
+ continue;
+ }
+
+ locks.push(`
Requires ${name}`);
+ }
+
+ return locks;
+ }
+
+ // todo (med): information will get lost if one vendor sells one item multiple times with different costs (e.g. for item 54637)
+ // wowhead seems to have had the same issues
+ private async getExtendedCost(entry: number, flagsExtra: number, buyPrice: number): Promise {
+
+ if (!entry) { return []; }
+
+ const itemz = {};
+ let xCostData = [];
+ const xCostDataArr = {};
+ const rawEntries = await this.getItemExtendedCostFromVendor(entry);
+
+ if (!rawEntries) {
+ return [];
+ }
+
+ for (const costEntry of rawEntries) {
+ /* istanbul ignore else */
+ if (costEntry.extendedCost) {
+ xCostData.push(costEntry.extendedCost);
+ }
+
+ /* istanbul ignore next */
+ if (itemz[costEntry.item] && itemz[costEntry.item][costEntry.entry]) {
+ itemz[costEntry.item][costEntry.entry] = [costEntry];
+ } else {
+ itemz[costEntry.item] = {};
+ itemz[costEntry.item][costEntry.entry] = [];
+ itemz[costEntry.item][costEntry.entry].push(costEntry);
+ }
+ }
+
+ if (
+ /* istanbul ignore next */
+ !!xCostData
+ && xCostData.length > 0) {
+ xCostData = Array.from(new Set(xCostData)); // filter duplicates
+ xCostData = await this.getItemExtendedCost(xCostData);
+
+ /* istanbul ignore else */
+ if (!!xCostData && xCostData.length > 0) {
+
+ // converting xCostData to ARRAY_KEY structure
+ for (const xCost of xCostData) {
+ xCostDataArr[xCost.id] = xCost;
+ }
+ } else {
+ /* istanbul ignore next */
+ return [];
+ }
+ }
+
+ const cItems = [];
+
+ for (const [k, vendors] of Object.entries(itemz)) {
+ for (const [l, vendor] of Object.entries(vendors)) {
+ for (const [m, vInfo] of Object.entries(vendor)) {
+
+ let costs = [];
+ /* istanbul ignore else */
+ if (xCostDataArr[vInfo['extendedCost']] && Object.keys(xCostDataArr[vInfo['extendedCost']]).length > 0) {
+ costs = xCostDataArr[vInfo['extendedCost']];
+ }
+
+ const data = {
+ stock: vInfo['maxcount'] ??
+ /* istanbul ignore next */
+ -1,
+ event: vInfo['eventId'],
+ reqRating: costs
+ ? costs['reqPersonalRating']
+ /* istanbul ignore next */
+ : 0,
+ /* istanbul ignore next */
+ reqBracket: costs
+ ? costs['reqArenaSlot']
+ /* istanbul ignore next */
+ : 0
+ };
+
+ // hardcode arena(103) & honor(104)
+ if (costs['reqArenaPoints'] > 0) {
+ data[-103] = costs['reqArenaPoints'];
+ }
+
+ if (costs['reqHonorPoints'] > 0) {
+ data[-104] = costs['reqHonorPoints'];
+ }
+
+ for (let i = 1; i < 6; i++) {
+ if (costs['reqItemId' + i] /* && costs['reqItemId' + i].length > 0 */
+ && costs['itemCount' + i] && costs['itemCount' + i] > 0) {
+ data[costs['reqItemId' + i]] = costs['itemCount' + i];
+ cItems.push(costs['reqItemId' + i]);
+ }
+ }
+
+ // no extended cost or additional gold required
+ if (!costs || flagsExtra & 0x04) {
+ if (!!buyPrice) {
+ data[0] = buyPrice;
+ }
+ }
+
+ vendor[m] = data;
+ }
+ vendors[l] = vendor;
+ }
+
+ itemz[k] = vendors;
+ }
+
+ // convert items to currency if possible
+ /* istanbul ignore else */
+ if (!!cItems) {
+
+ for (const [itemId, vendors] of Object.entries(itemz)) {
+ for (const [npcId, costData] of Object.entries(vendors)) {
+ for (const [itr, cost] of Object.entries(costData)) {
+ for (const [k, v] of Object.entries(cost)) {
+ if (cItems.includes(Number(k))) {
+ let found = false;
+ for (const item of cItems) {
+ if (item === Number(k)) {
+ delete cost[Number(k)];
+ cost[-item.id] = v;
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+ costData[itr] = cost;
+ }
+ vendors[npcId] = costData;
+ }
+ itemz[itemId] = vendors;
+ }
+ }
+
+ const result = itemz;
+
+ let reqRating = [];
+ for (const [itemId, data] of Object.entries(result)) {
+ reqRating = [];
+ for (const [npcId, entries] of Object.entries(data)) {
+ for (const costs of entries) {
+ // reqRating isn't really a cost .. so pass it by ref instead of return
+ // use highest total value
+ if (data[npcId] &&
+ costs['reqRating'] &&
+ /* istanbul ignore next */
+ (reqRating.length === 0 || (reqRating && reqRating[0] < costs['reqRating']))
+ ) {
+ reqRating = [costs['reqRating'], costs['reqBracket']];
+ }
+ }
+ }
+
+ /* istanbul ignore next */
+ if (!(data)) {
+ delete result[itemId];
+ }
+ }
+
+ return [result, reqRating];
+ }
+
+ /**
+ * get item preview text
+ */
+
+ private getDamageText(itemTemplate: ItemTemplate): string {
+ // Weapon/Ammunition Stats (not limited to weapons (see item:1700))
+ const itemClass: number = Number(itemTemplate.class);
+ const subclass: number = Number(itemTemplate.subclass);
+ const dmgmin1: number = Number(itemTemplate.dmg_min1);
+ const dmgmin2: number = Number(itemTemplate.dmg_min2);
+ const dmgmax1: number = Number(itemTemplate.dmg_max1);
+ const dmgmax2: number = Number(itemTemplate.dmg_max2);
+ const speed = itemTemplate.delay / 1000;
+ const sc1 = itemTemplate.dmg_type1;
+ const sc2 = itemTemplate.dmg_type2;
+ const dmgmin = dmgmin1 + dmgmin2;
+ const dmgmax = dmgmax1 + dmgmax2;
+ const dps = speed ? (dmgmin + dmgmax) / (2 * speed) : 0;
+
+ let damageText = '';
+ let dmg = '';
+
+ if (itemClass === ITEM_TYPE.AMMUNITION && dmgmin && dmgmax) {
+ if (sc1) {
+ damageText += ITEM_CONSTANTS.damage.ammo[1].replace('%d', ((dmgmin + dmgmax) / 2).toString()).replace('%s', ITEM_CONSTANTS.sc[sc1]);
+ } else {
+ damageText += ITEM_CONSTANTS.damage.ammo[0].replace('%d', ((dmgmin + dmgmax) / 2).toString());
+ }
+ } else if (dps) {
+ if (dmgmin1 === dmgmax1) {
+ dmg = ITEM_CONSTANTS.damage.single[sc1 ? 1 : 0]
+ .replace('%d', String(dmgmin1))
+ .replace('%s', (sc1 ? ITEM_CONSTANTS.sc[sc1] : ''));
+ } else {
+ dmg = ITEM_CONSTANTS.damage.range[sc1 ? 1 : 0]
+ .replace('%d', String(dmgmin1))
+ .replace('%d', String(dmgmax1))
+ .replace('%s', sc1 ? ITEM_CONSTANTS.sc[sc1] : '');
+ }
+
+ if (itemClass === ITEM_TYPE.WEAPON) {
+ damageText += `${dmg} | | Speed ${speed.toFixed(2)} |
---|
`;
+ } else {
+ damageText += `
${dmg}`;
+ }
+
+ // secondary damage is set
+ /* istanbul ignore next */
+ if ((dmgmin2 || dmgmax2) && dmgmin2 !== dmgmax2) {
+ damageText += ITEM_CONSTANTS.damage.range[sc2 ? 3 : 2]
+ .replace('%d', String(dmgmin2))
+ .replace('%d', String(dmgmax2))
+ .replace('%s', sc2 ? ITEM_CONSTANTS.sc[sc2] : '');
+ } else if (dmgmin2) {
+ damageText += ITEM_CONSTANTS.damage.single[sc2 ? 3 : 2]
+ .replace('%d', String(dmgmin2))
+ .replace('%s', sc2 ? ITEM_CONSTANTS.sc[sc2] : '');
+ }
+
+ if (itemClass === ITEM_TYPE.WEAPON) {
+ damageText += `
${ITEM_CONSTANTS.dps.replace('%.1f', dps.toFixed(2))}`;
+ }
+
+ // display FeralAttackPower if set
+ const fap = this.getFeralAP(itemClass, subclass, dps);
+ if (fap) {
+ damageText += `
(${fap} ${ITEM_CONSTANTS.fap})`;
+ }
+ }
+
+ return damageText;
+ }
+
+ private getStats(itemTemplate: ItemTemplate, greenText: string[]): string {
+ let stats = '';
+
+ const requiredLevel = Number(itemTemplate.RequiredLevel);
+
+ for (let i = 1; i <= 10; i++) {
+ const type = Number(itemTemplate['stat_type' + i]);
+ const qty = Number(itemTemplate['stat_value' + i]);
+
+ if (!qty || !type || type <= 0) {
+ continue;
+ }
+ // base stat
+ switch (type) {
+ case ITEM_MOD.MANA:
+ case ITEM_MOD.HEALTH:
+ // type += 1; // offsets may be required somewhere
+ case ITEM_MOD.AGILITY:
+ case ITEM_MOD.STRENGTH:
+ case ITEM_MOD.INTELLECT:
+ case ITEM_MOD.SPIRIT:
+ case ITEM_MOD.STAMINA:
+ stats += `
${(qty > 0 ? '+' : '-') + Math.abs(qty)} ${ITEM_CONSTANTS.statType[type]}`;
+ break;
+ default: // rating with % for reqLevel
+ greenText.push(this.parseRating(type, qty, requiredLevel));
+ }
+ }
+
+ return stats;
+ }
+
+ private async getItemSet(entry: number, itemset: number): Promise {
+ if (!itemset) {
+ return '';
+ }
+
+ let itemsetText = '';
+
+ const itemsetPieces = await this.getItemsetSlotBak(itemset);
+
+ if (!itemsetPieces || itemsetPieces.length === 0) { return ''; }
+
+ // check if there are multiple itemset with the same itemset ID
+ let multipleItemset = false;
+
+ if (itemsetPieces && itemsetPieces.length > 10) {
+ multipleItemset = true;
+ } else {
+ const slotBak = [];
+ for (const p of itemsetPieces) {
+ if (slotBak.includes(p.slotBak)) {
+ multipleItemset = true;
+ break;
+ } else {
+ slotBak.push(p.slotBak);
+ }
+ }
+ }
+
+ // get pieces IDs
+ const piecesIDs = [];
+
+ if (multipleItemset) {
+ // detect position of current item
+ let position = 0;
+ let currentSlotBak = itemsetPieces[0].slotBak;
+ for (let i = 0; i < itemsetPieces.length; i++) {
+ if (currentSlotBak !== itemsetPieces[i].slotBak) {
+ currentSlotBak = itemsetPieces[i].slotBak;
+ position = 0;
+ }
+
+ position++;
+ if (itemsetPieces[i].id === entry) {
+ break;
+ }
+ }
+
+ // get itemsetPieces IDs
+ let pos = 0;
+ for (const p of itemsetPieces) {
+ if (p.slotBak !== currentSlotBak) {
+ currentSlotBak = p.slotBak;
+ pos = 0;
+ }
+
+ pos++;
+
+ /* istanbul ignore else */
+ if (pos === position) {
+ piecesIDs.push(p.id);
+ }
+ }
+ } else {
+ for (const p of itemsetPieces) {
+ piecesIDs.push(p.id);
+ }
+ }
+
+ piecesIDs.sort();
+
+ // get items name
+ const itemsName: any[] = await this.getItemNameByIDsASC(piecesIDs);
+
+ if (!itemsName || (itemsName && itemsName.length === 0)) {
+ return '';
+ }
+
+ for (let i = 0; i < itemsName.length; i++) {
+ itemsName[i] = itemsName[i].name ?? '';
+ }
+
+ let itemsetAttr = await this.getItemsetById(itemset);
+
+ if (!itemsetAttr || itemsName.join('') === '' || !itemsetAttr[0]) {
+ return '';
+ } else {
+ itemsetAttr = itemsetAttr[0];
+ }
+
+ itemsetText += '
' + ITEM_CONSTANTS.setName
+ .replace('%s', `${itemsetAttr['name'] ?? ''}`)
+ .replace('%d', '0')
+ .replace('%d', itemsName.length.toString())
+ + '';
+
+ // if require skill
+ if (!!itemsetAttr['skillId']) {
+ itemsetText += `
Requires ${await this.sqliteQueryService.getSkillNameById(itemsetAttr['skillId'])}`;
+
+ if (!!itemsetAttr['skillLevel']) {
+ itemsetText += ` (${itemsetAttr['skillLevel']})`;
+ }
+ }
+
+ // list pieces
+ itemsetText += `
${itemsName.join('
')}
`;
+
+ // get bonuses
+ const setSpellsAndIdx = [];
+
+ for (let j = 1; j <= 8; j++) {
+ const spell = itemsetAttr['spell' + j];
+
+ if (!!spell) {
+ setSpellsAndIdx[spell] = j;
+ }
+ }
+
+ const setSpells = [];
+ if (setSpellsAndIdx && setSpellsAndIdx.length > 0) {
+ const spellsIDs = Object.keys(setSpellsAndIdx);
+ for (const s of spellsIDs) {
+ setSpells.push({
+ tooltip: await this.sqliteQueryService.getSpellDescriptionById(s),
+ entry: itemsetAttr['spell' + setSpellsAndIdx[s]],
+ bonus: itemsetAttr['bonus' + setSpellsAndIdx[s]],
+ });
+ }
+ }
+
+ // sort and list bonuses
+ let tmpBonus = '';
+ for (let i = 0; i < setSpells.length; i++) {
+ for (let j = i; j < setSpells.length; j++) {
+ if (setSpells[j]['bonus'] >= setSpells[i]['bonus']) {
+ continue;
+ }
+
+ const tmp = setSpells[i];
+ setSpells[i] = setSpells[j];
+ setSpells[j] = tmp;
+ }
+
+ const bonusText = setSpells[i]['bonus'] && setSpells[i]['tooltip']
+ ? ITEM_CONSTANTS.setBonus
+ .replace('%d', setSpells[i]['bonus'])
+ .replace('%s', setSpells[i]['tooltip'])
+ : '';
+
+ if (!!bonusText) {
+ tmpBonus += `
${bonusText}`;
+ }
+ }
+
+ if (tmpBonus !== '') {
+ itemsetText += `${tmpBonus}`;
+ }
+
+ return itemsetText;
+ }
+
+ private async getBonding(itemTemplate: ItemTemplate): Promise {
+ let bondingText = '';
+
+ const flags = itemTemplate.Flags;
+ const bonding: number = Number(itemTemplate.bonding);
+ const maxcount: number = Number(itemTemplate.maxcount);
+ const bagFamily: number = Number(itemTemplate.BagFamily);
+ const itemLimitCategory = itemTemplate.ItemLimitCategory;
+
+ // bonding
+ if (flags & ITEM_FLAG.ACCOUNTBOUND) {
+ bondingText += '
' + this.ITEM_CONSTANTS.bonding[0];
+ } else if (bonding) {
+ bondingText += '
' + this.ITEM_CONSTANTS.bonding[bonding];
+ }
+
+ // unique || unique-equipped || unique-limited
+ if (maxcount === 1) {
+ bondingText += '
' + this.ITEM_CONSTANTS['unique'][0];
+ } else if (!!maxcount && bagFamily !== 8192) { // not for currency tokens
+ bondingText += '
' + this.ITEM_CONSTANTS['unique'][1].replace('%d', maxcount.toString());
+ } else if (flags & ITEM_FLAG.UNIQUEEQUIPPED) {
+ bondingText += '
' + this.ITEM_CONSTANTS['uniqueEquipped'][0];
+ } else if (!!itemLimitCategory) {
+ let limit: any = await this.getItemLimitCategoryById(itemLimitCategory);
+
+ if (limit && limit.length > 0) {
+ limit = limit[0];
+
+ const index = limit && limit.isGem ? 'uniqueEquipped' : 'unique';
+ bondingText += `
${
+ ITEM_CONSTANTS[index][2].replace('%s', limit.name).replace('%d', limit.count)
+ }`;
+ }
+ }
+
+ return bondingText;
+ }
+
+ private getClassText(inventoryType: number, itemClass: number, subclass: number): string {
+ let classText = '';
+
+ let textRight = '';
+ if ([ITEM_TYPE.ARMOR, ITEM_TYPE.WEAPON, ITEM_TYPE.AMMUNITION].includes(itemClass)) {
+ let classTmpText = '';
+
+ // Class
+ if (inventoryType) {
+ classTmpText += `${ITEM_CONSTANTS.inventoryType[inventoryType]} | `;
+ textRight = ' style="text-align: right;"';
+ }
+
+ // Subclass
+ /* istanbul ignore else */
+ if (itemClass === ITEM_TYPE.ARMOR && subclass > 0) {
+ classTmpText += `${ITEM_CONSTANTS.armorSubClass[subclass]} | `;
+ } else if (itemClass === ITEM_TYPE.WEAPON) {
+ classTmpText += ITEM_CONSTANTS.weaponSubClass[subclass] ? `${ITEM_CONSTANTS.weaponSubClass[subclass]} | ` : '';
+ } else if (itemClass === ITEM_TYPE.AMMUNITION) {
+ classTmpText += ITEM_CONSTANTS.projectileSubClass[subclass]
+ ? `${ITEM_CONSTANTS.projectileSubClass[subclass]} | `
+ : '';
+ }
+
+ classTmpText += '
';
+
+ if (classTmpText !== '') {
+ classText += classTmpText;
+ }
+
+ // inventoryType/slot can occur on random items and is then also displayed <_< .. excluding Bags >_>
+ } else if (inventoryType && itemClass !== ITEM_TYPE.CONTAINER && !!ITEM_CONSTANTS.inventoryType[subclass]) {
+ classText += `
${ITEM_CONSTANTS.inventoryType[subclass]}`;
+ }
+
+ return classText;
+ }
+
+ private getArmorText(itemTemplate: ItemTemplate): string {
+ let armorText = '';
+
+ // Armor
+ const armorDamageModifier = itemTemplate.ArmorDamageModifier;
+ const armor = itemTemplate.armor;
+ const itemClass: number = Number(itemTemplate.class);
+ if (itemClass === ITEM_TYPE.ARMOR && armorDamageModifier > 0 && !!armor) {
+ armorText += `
${ITEM_CONSTANTS.armor.replace('%s', String(armor))}`;
+ } else if (armor) {
+ armorText += `
${ITEM_CONSTANTS.armor.replace('%s', String(armor))}`;
+ }
+
+ // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently)
+ const block = itemTemplate.block;
+ if (block) {
+ armorText += `
${ITEM_CONSTANTS.block.replace('%s', String(block))}`;
+ }
+
+ return armorText;
+ }
+
+ private async getRequiredText(itemTemplate: ItemTemplate): Promise {
+ let requiredText = '';
+
+ // required classes
+ const classes = this.getRequiredClass(itemTemplate.AllowableClass);
+ if (classes != null && classes.length > 0) {
+ requiredText += `
Classes: ${classes.join(', ')}`;
+ }
+
+ // required races
+ const races = this.getRaceString(itemTemplate.AllowableRace);
+ if (races) {
+ requiredText += `
Races: ${races.join(', ')}`;
+ }
+
+ // required honorRank (not used anymore)
+ if (!!itemTemplate.requiredhonorrank) {
+ requiredText += `
Requires ${ITEM_CONSTANTS.pvpRank[itemTemplate.requiredhonorrank]}`;
+ }
+
+ // required CityRank -> the value is always 0
+
+ // required level
+ if ((itemTemplate.Flags & ITEM_FLAG.ACCOUNTBOUND) && itemTemplate.Quality === ITEMS_QUALITY.HEIRLOOM) {
+
+ requiredText += '
' + ITEM_CONSTANTS.reqLevelRange
+ .replace('%d', '1')
+ .replace('%d', MAX_LEVEL.toString())
+ .replace('%s', MAX_LEVEL.toString());
+
+ } else if (itemTemplate.RequiredLevel > 1) {
+ requiredText += '
' + ITEM_CONSTANTS.reqMinLevel.replace('%d', String(itemTemplate.RequiredLevel));
+ }
+
+ // required arena team rating / personal rating / todo (low): sort out what kind of rating
+ const [res, reqRating] = await this.getExtendedCost(itemTemplate.entry, itemTemplate.FlagsExtra, itemTemplate.BuyPrice);
+
+ if (!!res && !!reqRating && res[itemTemplate.entry] && Object.keys(res[itemTemplate.entry]).length > 0 && reqRating.length > 0) {
+ requiredText += '
' + ITEM_CONSTANTS.reqRating[reqRating[1]].replace('%d', reqRating[0]);
+ }
+
+ // item level
+ const itemClass = Number(itemTemplate.class);
+ const itemLevel = itemTemplate.ItemLevel;
+ if (itemLevel > 0 && [ITEM_TYPE.ARMOR, ITEM_TYPE.WEAPON].includes(itemClass)) {
+ requiredText += `
${ITEM_CONSTANTS.itemLevel.replace('%d', String(itemLevel))}`;
+ }
+
+ // required skill
+ const requiredSkill = itemTemplate.RequiredSkill;
+ const requiredSkillRank = itemTemplate.RequiredSkillRank;
+ if (!!requiredSkill && requiredSkill > 0) {
+ let reqSkill = await this.sqliteQueryService.getSkillNameById(requiredSkill);
+ if (requiredSkillRank > 0) {
+ reqSkill += ` (${requiredSkillRank})`;
+ }
+
+ requiredText += `
Requires: ${reqSkill}`;
+ }
+
+ // required spell
+ const requiredSpell = itemTemplate.requiredspell;
+ if (!!requiredSpell && requiredSpell > 0) {
+ requiredText += `
Requires ${await this.sqliteQueryService.getSpellNameById(requiredSpell)}`;
+ }
+
+ // required reputation w/ faction
+ const requiredFaction = itemTemplate.RequiredReputationFaction;
+ const requiredFactionRank = itemTemplate.RequiredReputationRank;
+ if (!!requiredFaction && requiredFaction > 0) {
+ let reqFaction = await this.sqliteQueryService.getFactionNameById(requiredFaction);
+ if (requiredFactionRank > 0) {
+ reqFaction += ` (${requiredFactionRank})`;
+ }
+ requiredText += `
Requires ${reqFaction}`;
+ }
+
+ return requiredText;
+ }
+
+ private async getRequiredZone(map: number, area: number): Promise {
+ let requiredZone = '';
+
+ // require map
+ if (!!map) {
+ const mapName = await this.sqliteQueryService.getMapNameById(map);
+ requiredZone += mapName && mapName !== '' ? `
${mapName}` : '';
+ }
+
+ // require area
+ if (!!area) {
+ const areaName = await this.sqliteQueryService.getAreaNameById(area);
+ requiredZone += areaName && areaName !== '' ? `
${areaName}` : '';
+ }
+
+ return requiredZone;
+ }
+
+ private getDuration(duration: number, flagsCustom: number): string {
+ let durationText = '';
+
+ // max duration
+ if (duration) {
+ let rt = '';
+ if (flagsCustom & 0x1) { // if CU_DURATION_REAL_TIME
+ rt = ' (real time)';
+ }
+ durationText += `
Duration: ${this.formatTime(duration * 1000)}${rt}`;
+ }
+
+ return durationText;
+ }
+
+ private getMagicResistances(itemTemplate: ItemTemplate): string {
+ let magicRsistances = '';
+
+ // magic resistances
+ resistanceFields.forEach((rowName, idx) => {
+ const resField = itemTemplate[rowName + '_res'];
+ if (rowName != null && resField != null && resField !== 0) {
+ magicRsistances += `
+${resField} ${ITEM_CONSTANTS.resistances[idx]}`;
+ }
+ });
+
+ return magicRsistances;
+ }
+
+ private getMisc(itemTemplate: ItemTemplate): string {
+ const xMisc = [];
+
+ const spellId1 = itemTemplate.spellid_1;
+ const spellId2 = itemTemplate.spellid_2;
+ const description = itemTemplate.description;
+
+ // yellow text at the bottom, omit if we have a recipe
+ if (!!description && !this.canTeachSpell(spellId1, spellId2)) {
+ xMisc.push(`
"${description}"`);
+ }
+
+ // readable
+ const PageText = itemTemplate.PageText;
+ if (PageText > 0) {
+ xMisc.push(`
${ITEM_CONSTANTS.readClick}`);
+ }
+
+ // charges (I guess, checking first spell is enough)
+ const spellCharges1 = itemTemplate.spellcharges_1;
+ if (!!spellCharges1) {
+
+ let charges = ITEM_CONSTANTS.charges.replace('%d', Math.abs(spellCharges1).toString());
+ if (Math.abs(spellCharges1) === 1) {
+ charges = charges.replace('Charges', 'Charge');
+ }
+
+ /* istanbul ignore else */
+ if (!!charges && charges !== '') {
+ xMisc.push(`
${charges}`);
+ }
+ }
+
+ return xMisc.length > 0 ? xMisc.join('') : '';
+ }
+
+ private async getGemEnchantment(entry: number): Promise {
+ if (!entry) { return ''; }
+
+ let gemEnchantmentText = '';
+ const gemEnchantmentId = await this.getGemEnchantmentIdById(entry);
+
+ if (!!gemEnchantmentId) {
+ let gemEnch = await this.getItemEnchantmentById(gemEnchantmentId);
+ if (!gemEnch || (gemEnch && gemEnch.length === 0)) { return ''; }
+
+ gemEnch = gemEnch[0];
+
+ if (!!gemEnch['name'] && gemEnch['name'] !== '') {
+ gemEnchantmentText += `
${gemEnch['name']}`;
+ }
+
+ // activation conditions for meta gems
+ if (!!gemEnch['conditionId']) {
+
+ let gemCnd = await this.getItemEnchantmentConditionById(gemEnch['conditionId']);
+ if (!gemCnd || (gemCnd && gemCnd.length === 0)) { return ''; }
+
+ gemCnd = gemCnd[0];
+
+ if (!!gemCnd) {
+
+ for (let i = 1; i < 6; i++) {
+ const gemCndColor = Number(gemCnd[`color${i}`]);
+
+ if (!gemCndColor) {
+ continue;
+ }
+
+ const gemCndCmpColor = Number(gemCnd[`cmpColor${i}`]);
+ const gemCndComparator = Number(gemCnd[`comparator${i}`]);
+ const gemCndValue = Number(gemCnd[`value${i}`]);
+
+ let vspfArgs: any = ['', ''];
+
+ switch (gemCndComparator) {
+ case 2: // requires less than ( || ) gems
+ case 5: // requires at least than ( || ) gems
+ vspfArgs = [
+ gemCndValue,
+ ITEM_CONSTANTS['gemColors'][gemCndColor - 1],
+ ];
+ break;
+ case 3: // requires more than ( || ) gems
+ vspfArgs = [
+ ITEM_CONSTANTS['gemColors'][gemCndColor - 1],
+ ITEM_CONSTANTS['gemColors'][gemCndCmpColor - 1],
+ ];
+ break;
+ default:
+ break;
+ }
+
+ if (vspfArgs[0] === '' && vspfArgs[1] === '') {
+ continue;
+ }
+
+ let gemEnchText = ITEM_CONSTANTS['gemConditions'][gemCndComparator];
+
+ /* istanbul ignore next */
+ if (!!vspfArgs[0] && !!vspfArgs[1]) {
+ gemEnchText = gemEnchText.replace('%s', vspfArgs[0]).replace('%s', vspfArgs[1]);
+ }
+
+ gemEnchantmentText += `
Requires ${gemEnchText}`;
+ }
+ }
+ }
+ }
+
+ return gemEnchantmentText;
+ }
+
+ private async getSocketEnchantment(itemTemplate: ItemTemplate): Promise {
+
+ let socketText = '';
+
+ // fill native sockets
+ for (let j = 1; j <= 3; j++) {
+ const socketColor = Number(itemTemplate['socketColor_' + j]);
+
+ if (!socketColor) {
+ continue;
+ }
+
+ let colorId = 0;
+ for (let i = 0; i < 4; i++) {
+ if (socketColor & (1 << i)) {
+ colorId = i;
+ }
+ }
+
+ socketText += `
${ITEM_CONSTANTS.socket[colorId]}`;
+ }
+
+ const socketBonus = itemTemplate.socketBonus;
+ if (!!socketBonus) {
+ const sbonus = await this.sqliteQueryService.getSocketBonusById(socketBonus);
+ const socketBonusText = `${ITEM_CONSTANTS.socketBonus.replace('%s', sbonus)}`;
+ socketText += `
${socketBonusText}`;
+ }
+
+ return socketText;
+ }
+
+ private async getSpellDesc(itemTemplate: ItemTemplate, green: string[]) {
+ const spellId1 = itemTemplate.spellid_1;
+ const spellId2 = itemTemplate.spellid_2;
+
+ if (!this.canTeachSpell(spellId1, spellId2)) {
+ const itemSpellsAndTrigger = [];
+ for (let j = 1; j <= 5; j++) {
+ const spellid = itemTemplate['spellid_' + j];
+
+ if (spellid > 0) {
+ let cooldown = itemTemplate['spellcooldown_' + j];
+ const cooldownCategory = itemTemplate['spellcategory_' + j];
+
+ if (cooldown < cooldownCategory) {
+ cooldown = cooldownCategory;
+ }
+
+ cooldown = cooldown < 5000 ? '' : ` ( ${this.formatTime(Number(cooldown))} cooldown)`;
+
+ itemSpellsAndTrigger[spellid] = [itemTemplate['spelltrigger_' + j], cooldown];
+ }
+ }
+
+ if (itemSpellsAndTrigger && itemSpellsAndTrigger.length > 0) {
+ const spellIDs = Object.keys(itemSpellsAndTrigger);
+ for (const spellID of spellIDs) {
+ const spellTrigger = itemSpellsAndTrigger[spellID];
+ const parsed = await this.sqliteQueryService.getSpellDescriptionById(spellID); // TODO: parseText correctly
+
+ /* istanbul ignore next */
+ if (spellTrigger[0] || parsed || spellTrigger[1]) {
+ /* istanbul ignore next */
+ green.push(ITEM_CONSTANTS.trigger[spellTrigger[0]] ?? '' + parsed ?? '' + ' ' + ITEM_CONSTANTS.trigger[spellTrigger[1]] ?? '');
+ }
+ }
+
+ }
+ }
+ }
+
+ // TODO: recipes, vanity pets, mounts
+ private async getLearnSpellText(itemTemplate: ItemTemplate): Promise {
+ /* TODO - WIP */
+
+ let spellDesc = '';
+
+ // const bagFamily: number = Number(itemTemplate.BagFamily);
+ // const itemClass: number = Number(itemTemplate.class);
+ const spellId1 = itemTemplate.spellid_1;
+ const spellId2 = itemTemplate.spellid_2;
+
+ if (!spellId1 || !spellId2) { return ''; }
+
+ if (this.canTeachSpell(spellId1, spellId2)) {
+ const craftSpell = spellId2;
+
+ if (
+ /* istanbul ignore next */
+ !!craftSpell
+ ) {
+ // let xCraft = '';
+
+ const desc = await this.sqliteQueryService.getSpellDescriptionById(spellId2);
+
+ if (!!desc) {
+ spellDesc += `
${ITEM_CONSTANTS.trigger[0]} ${desc}`;
+ }
+
+ // TODO: spell description for recipe
+ // // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp
+ // if (itemClass === ITEM_CLASS_RECIPE || bagFamily === 16) {
+ // let craftItem = craftSpell->curTpl['effect1CreateItemId'];
+
+ // if (!!craftItem) {
+
+ // const reagentItems = {};
+
+ // for (let i = 1; i <= 8; i++) {
+ // if (rId = craftSpell->getField('reagent' + i)) {
+ // reagentItems[rId] = craftSpell->getField('reagentCount' + i);
+ // }
+ // }
+
+ // if (!!xCraft && !!reagentItems) {
+ // let reagents = Object.keys(reagentItems);
+ // let reqReag = [];
+
+ // for (const _ of reagents) {
+ // reqReag.push(`${reagents->getField('name', true)} (${reagentItems[reagents->id]})`);
+ // }
+
+ // xCraft += '
Requires: ' + reqReag.join(', ') + '
';
+ // }
+ // }
+ // }
+ }
+ }
+
+ return spellDesc;
+ }
+
+ // locked or openable
+ private async getLockText(flags: number, lockid: number): Promise {
+ let lockText = '';
+
+ if (!!lockid) {
+ const lockData = await this.getLocks(lockid);
+
+ if (!!lockData && lockData.length > 0) {
+ lockText += `
Locked${lockData.join('')}`;
+ } else if (flags & ITEM_FLAG.OPENABLE) {
+ lockText += `
${ITEM_CONSTANTS.openClick}`;
+ }
+ }
+
+ return lockText;
+ }
+
+ public async calculatePreview(itemTemplate: ItemTemplate): Promise {
+ let tmpItemPreview = '';
+ const green: string[] = [];
+
+ const flags = itemTemplate.Flags;
+ const bagFamily: number = Number(itemTemplate.BagFamily);
+ const quality: number = Number(itemTemplate.Quality);
+
+ // ITEM NAME
+ const itemName = itemTemplate.name;
+ if (itemName) {
+ tmpItemPreview += `${itemName}`;
+ }
+
+ // heroic tag
+ if (flags & ITEM_FLAG.HEROIC && quality === ITEMS_QUALITY.EPIC) {
+ tmpItemPreview += '
Heroic';
+ }
+
+ tmpItemPreview += await this.getRequiredZone(Number(itemTemplate.Map), Number(itemTemplate.area));
+
+ // conjured
+ if (flags & ITEM_FLAG.CONJURED) {
+ tmpItemPreview += '
Conjured Item';
+ }
+
+ tmpItemPreview += await this.getBonding(itemTemplate);
+ tmpItemPreview += this.getDuration(itemTemplate.duration, itemTemplate.flagsCustom);
+
+ // required holiday
+ const holiday = itemTemplate.HolidayId;
+ if (!!holiday) {
+ const eventName = await this.sqliteQueryService.getEventNameByHolidayId(holiday);
+ tmpItemPreview += `
Requires ${eventName}`;
+ }
+
+ // item begins a quest
+ const startquest: number = Number(itemTemplate.startquest);
+ if (!!startquest && startquest > 0) {
+ tmpItemPreview += `
This Item Begins a Quest`;
+ }
+
+ // containerType (slotCount)
+ const containerSlots: number = Number(itemTemplate.ContainerSlots);
+ if (containerSlots > 0) {
+ const fam = bagFamily ? Math.log2(bagFamily) + 1 : 0;
+ tmpItemPreview += `
${containerSlots} Slot ${ITEM_CONSTANTS.bagFamily[fam]}`;
+ }
+
+ tmpItemPreview += this.getClassText(itemTemplate.InventoryType, itemTemplate.class, itemTemplate.subclass);
+ tmpItemPreview += this.getDamageText(itemTemplate);
+ tmpItemPreview += this.getArmorText(itemTemplate);
+
+ // Item is a gem (don't mix with sockets)
+ tmpItemPreview += await this.getGemEnchantment(itemTemplate.entry);
+
+ // Random Enchantment - if random enchantment is set, prepend stats from it
+ const RandomProperty: number = itemTemplate.RandomProperty;
+ const RandomSuffix: number = itemTemplate.RandomSuffix;
+ if (!!RandomProperty || !!RandomSuffix) {
+ tmpItemPreview += `
${ITEM_CONSTANTS.randEnchant}`;
+ }
+
+ // itemMods (display stats and save ratings for later use)
+ tmpItemPreview += this.getStats(itemTemplate, green);
+
+ tmpItemPreview += this.getMagicResistances(itemTemplate);
+
+ // Socket & Enchantment (TODO)
+ tmpItemPreview += await this.getSocketEnchantment(itemTemplate);
+
+ // durability
+ const durability = itemTemplate.MaxDurability;
+ if (durability) {
+ tmpItemPreview += `
${ITEM_CONSTANTS.durability.replace(/%d/g, durability.toString())}`;
+ }
+
+ tmpItemPreview += await this.getRequiredText(itemTemplate);
+
+ const lockid = itemTemplate.lockid;
+ tmpItemPreview += await this.getLockText(flags, lockid);
+
+ // spells on item
+ await this.getSpellDesc(itemTemplate, green);
+
+ if (!!green && green.length > 0) {
+ for (const bonus of green) {
+ if (!!bonus) {
+ tmpItemPreview += `
${bonus}`;
+ }
+ }
+ }
+
+ tmpItemPreview += await this.getItemSet(itemTemplate.entry, itemTemplate.itemset);
+
+ // recipes, vanity pets, mounts
+ tmpItemPreview += await this.getLearnSpellText(itemTemplate);
+
+ tmpItemPreview += this.getMisc(itemTemplate);
+
+ const sellPrice = itemTemplate.SellPrice;
+ if (!!sellPrice && sellPrice > 0) {
+ tmpItemPreview += '
Sell Price: ' + this.formatMoney(sellPrice);
+ }
+
+ return tmpItemPreview;
+ }
+
+}
diff --git a/src/app/features/item/item-template/item-preview.ts b/src/app/features/item/item-template/item-preview.ts
new file mode 100644
index 00000000000..bdf0d1a73c0
--- /dev/null
+++ b/src/app/features/item/item-template/item-preview.ts
@@ -0,0 +1,104 @@
+import { ITEM_MOD } from '@keira-shared/constants/options/item-class';
+import { TableRow } from '@keira-shared/types/general';
+
+export class Lock extends TableRow {
+ id: number;
+ type1: number;
+ type2: number;
+ type3: number;
+ type4: number;
+ type5: number;
+ properties1: number;
+ properties2: number;
+ properties3: number;
+ properties4: number;
+ properties5: number;
+ reqSkill1: number;
+ reqSkill2: number;
+ reqSkill3: number;
+ reqSkill4: number;
+ reqSkill5: number;
+}
+
+export const MAX_LEVEL = 80;
+
+export const lvlIndepRating = [ // rating doesn't scale with level
+ ITEM_MOD.MANA,
+ ITEM_MOD.HEALTH,
+ ITEM_MOD.ATTACK_POWER,
+ ITEM_MOD.MANA_REGENERATION,
+ ITEM_MOD.SPELL_POWER,
+ ITEM_MOD.HEALTH_REGEN,
+ ITEM_MOD.SPELL_PENETRATION,
+ ITEM_MOD.BLOCK_VALUE,
+];
+
+export const gtCombatRatings = {
+ 12: 1.5,
+ 13: 13.8,
+ 14: 13.8,
+ 15: 5,
+ 16: 10,
+ 17: 10,
+ 18: 8,
+ 19: 14,
+ 20: 14,
+ 21: 14,
+ 22: 10,
+ 23: 10,
+ 24: 8,
+ 25: 0,
+ 26: 0,
+ 27: 0,
+ 28: 10,
+ 29: 10,
+ 30: 10,
+ 31: 10,
+ 32: 14,
+ 33: 0,
+ 34: 0,
+ 35: 28.75,
+ 36: 10,
+ 37: 2.5,
+ 44: 4.26,
+};
+
+export const resistanceFields = [
+ // null,
+ 'holy',
+ 'fire',
+ 'nature',
+ 'frost',
+ 'shadow',
+ 'arcane',
+];
+
+export enum CLASSES {
+ WARRIOR = 0x001,
+ PALADIN = 0x002,
+ HUNTER = 0x004,
+ ROGUE = 0x008,
+ PRIEST = 0x010,
+ DEATHKNIGHT = 0x020,
+ SHAMAN = 0x040,
+ MAGE = 0x080,
+ WARLOCK = 0x100,
+ DRUID = 0x400,
+ MASK_ALL = 0x5FF,
+}
+
+export enum RACE {
+ HUMAN = 0x001,
+ ORC = 0x002,
+ DWARF = 0x004,
+ NIGHTELF = 0x008,
+ UNDEAD = 0x010,
+ TAUREN = 0x020,
+ GNOME = 0x040,
+ TROLL = 0x080,
+ BLOODELF = 0x200,
+ DRAENEI = 0x400,
+ MASK_ALLIANCE = 0x44D,
+ MASK_HORDE = 0x2B2,
+ MASK_ALL = 0x6FF,
+}
diff --git a/src/app/features/item/item-template/item-template.component.html b/src/app/features/item/item-template/item-template.component.html
index 90a38916eec..de810065793 100644
--- a/src/app/features/item/item-template/item-template.component.html
+++ b/src/app/features/item/item-template/item-template.component.html
@@ -1,11 +1,12 @@
-
+
Loading...
+
@@ -75,9 +76,9 @@
= 0
- && editorService.form.controls.class.value < ITEM_SUBCLASS.length"
+ && editorService.form.controls.class.value !== null
+ && editorService.form.controls.class.value >= 0
+ && editorService.form.controls.class.value < ITEM_SUBCLASS.length"
[control]="editorService.form.controls.subclass"
[config]="{ options: ITEM_SUBCLASS[editorService.form.controls.class.value], name: 'subclass' }"
[modalClass]="'modal-md'"
@@ -89,7 +90,7 @@
-
+
-
+