import { BotData } from './botmgr.server'; import { Equipment } from './botmgr.server'; type ItemInHand = { entry: number | undefined, link: string | undefined, bot: number | undefined, slot: number | undefined } /** * This is the UI bot data manager class to make it easier * to managed bot data in the UI. */ export class BotStorage { private storage: Map = new Map(); private active: number = null; private pickedUp: boolean = false; private itemInHand: ItemInHand = { entry: undefined, link: undefined, bot: undefined, slot: undefined }; private bankItem: { entry: number, link: string, slot: number } = { entry: 0, link: '', slot: 0 }; public GetBotData(entry: number): BotData | undefined { return this.storage.get(entry); } GetBotItem(botId: number, slot: BotEquipmentSlotNum): Equipment | undefined { const bot = this.GetBotData(botId); if(bot) { return bot.equipment[slot]; } } SetBotData(entry: number, data: BotData): void { this.storage.set(entry, data); } SetBotItem(botId: number, slot: BotEquipmentSlotNum, item: Equipment): void { const botData = this.GetBotData(botId); if(botData) { botData.equipment[slot] = item; } this.SetBotData(botId, botData); } UpdateBotData(entry: number, data: BotData): void { this.storage.set(entry, data); } SetActive(botId: number): void { this.active = botId; } GetActive(): number { return this.active; } ClearActive(): void { this.active = null; } IsPickedUp(): boolean { return this.pickedUp; } BotItemPickedUp(botId: number, entry: number, link: string): void { const bot = this.GetBotData(botId); if(bot) { this.itemInHand.entry = entry; this.itemInHand.link = link; this.itemInHand.bot = botId; this.itemInHand.slot = this.GetSlotByItemId(entry); } this.pickedUp = true; } GetItemInHand(): ItemInHand { return this.itemInHand; } GetFromBank(): { entry: number, link: string, slot: number } | undefined { if(!this.bankItem) { return undefined; } return this.bankItem; } SetFromBank(item: { entry: number, link: string, slot: number }): void { this.bankItem = item; } ClearFromBank(): void { this.bankItem = undefined; } BotItemCursorClear(): void { this.itemInHand = undefined; this.pickedUp = false; } GetSlotByItemId(entry: number): number | undefined { const botData = this.GetBotData(this.GetActive()); const allItems = Object.entries(botData.equipment) for(const [slot, item] of allItems) { if(item.entry === entry) { return parseInt(slot); } } return; } }import * as Common from '../../constants/idmaps'; import { BotData, Equipment, EquipmentList } from './botmgr.server'; type CharInfo = { name: string, level: number, className: Common.CharacterClass, classId: keyof typeof Common.ClassesMapping, raceName: Common.CharacterRace, raceId: keyof typeof Common.RacesMapping } type CharStats = Partial>; function humanizeTalentName(input: string): string { if (input.length === 0) { return input; // Return unchanged if the input is an empty string } try { const parts = input.split("_"); parts[0] = parts[0].toLowerCase(); parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1); parts[1] = parts[1].toLowerCase(); parts[1] = parts[1].charAt(0).toUpperCase() + parts[1].slice(1); return `${parts[1]} ${parts[0]}`; } catch (e) { print(`failed to humanize talent name: ${input}` + e); } } export class BotUnit { protected myself: Creature; protected myOwner: Player; protected charinfo: CharInfo; protected equipment: EquipmentList; protected statsLeft: Record[]; protected statsRight: Record[]; protected talentSpecId: number; protected roles: number; protected allStats: Record = {}; constructor(creature: Creature) { if(!creature.IsNPCBot()) { return; } this.myself = creature; this.myOwner = creature.GetBotOwner(); this.charinfo = { name: creature.GetName(), level: creature.GetLevel(), className: Common.ClassesMapping[creature.GetClass()], classId: creature.GetClass(), raceName: Common.RacesMapping[creature.GetRace()], raceId: creature.GetRace() }; this.equipment = this._lookupEquipment(); this.talentSpecId = creature.GetTalentSpec(); this.parseStats(creature.GetBotDump()); try { this.statsLeft = this._lookupStats('left'); this.statsRight = this._lookupStats('right'); } catch (e) { print("failed to get stats for bot:" + e); } this.roles = creature.GetBotRoles(); } public toBotData(): BotData { return { name: this.charinfo.name, entry: this.myself.GetEntry(), owner: this.myOwner.GetName(), level: this.charinfo.level, class: this.charinfo.className, classId: this.charinfo.classId, race: this.charinfo.raceName, raceId: this.charinfo.raceId, talentSpec: this.talentSpecId, talentSpecName: humanizeTalentName(this.talentSpecName()), roles: this.roles, equipment: this.equipment, leftStats: this.statsLeft, rightStats: this.statsRight, allStats: this.allStats } } public isHealer(): boolean { if(this.talentSpecId == Common.TalentSpecs.SHAMAN_RESTORATION || this.talentSpecId == Common.TalentSpecs.PRIEST_DISCIPLINE || this.talentSpecId == Common.TalentSpecs.PRIEST_HOLY || this.talentSpecId == Common.TalentSpecs.PALADIN_HOLY || this.talentSpecId == Common.TalentSpecs.DRUID_RESTORATION) { return true; } return false; } public isDualWield(): boolean { if(this.talentSpecId == Common.TalentSpecs.ROGUE_COMBAT || this.talentSpecId == Common.TalentSpecs.ROGUE_SUBTLETY || this.talentSpecId == Common.TalentSpecs.ROGUE_ASSASSINATION || this.talentSpecId == Common.TalentSpecs.SHAMAN_ENHANCEMENT || this.talentSpecId == Common.TalentSpecs.WARRIOR_FURY) { if(this.equipment[Common.BotEquipSlot.MAINHAND] && this.equipment[Common.BotEquipSlot.OFFHAND]) { return true; } } } public GetMeleeStats (): Record{ const botStatValues = Object.values(Common.BotStatLabel); type BotStatValues = typeof botStatValues[number]; return { left: [ "Strength", "Agility", "Damage", "Power", "Hit Rating", "Crit %", "Expertise", "Armor Pen" ], right: [ "Haste Rating", "Armor", "Stamina", "Defense", "Dodge", "Parry", "Block", "Physical Res." ] } } public GetRangedStats (): Record{ return { left: [ "Strength", "Agility", "Damage Rng", "Speed", "Power", "Hit Rating", "Crit %", "Armor Pen" ], right: [ "Expertise", "Haste Rating", "Armor", "Stamina", "Defense", "Dodge", "Parry", "Block", ] } } public GetCasterStats (): Record { return { left: [ "Intellect", "Spirit", "Stamina", "Bonus Dmg", "Crit %", "Hit Rating", "Spell Pen" ], right: [ "Haste Rating", "MP5", "Spell Res.", "Dodge", "Armor", "Parry", ] } } public GetStatMappings() { switch(this.talentSpecId) { case Common.TalentSpecs.WARRIOR_ARMS: case Common.TalentSpecs.WARRIOR_FURY: case Common.TalentSpecs.WARRIOR_PROTECTION: case Common.TalentSpecs.PALADIN_PROTECTION: case Common.TalentSpecs.PALADIN_RETRIBUTION: case Common.TalentSpecs.DK_BLOOD: case Common.TalentSpecs.DK_FROST: case Common.TalentSpecs.DK_UNHOLY: case Common.TalentSpecs.ROGUE_ASSASSINATION: case Common.TalentSpecs.ROGUE_COMBAT: case Common.TalentSpecs.ROGUE_SUBTLETY: case Common.TalentSpecs.SHAMAN_ENHANCEMENT: case Common.TalentSpecs.DRUID_FERAL: return this.GetMeleeStats(); case Common.TalentSpecs.HUNTER_SURVIVAL: case Common.TalentSpecs.HUNTER_MARKSMANSHIP: case Common.TalentSpecs.HUNTER_BEASTMASTERY: return this.GetRangedStats(); case Common.TalentSpecs.MAGE_ARCANE: case Common.TalentSpecs.MAGE_FIRE: case Common.TalentSpecs.MAGE_FROST: case Common.TalentSpecs.WARLOCK_AFFLICTION: case Common.TalentSpecs.WARLOCK_DEMONOLOGY: case Common.TalentSpecs.WARLOCK_DESTRUCTION: case Common.TalentSpecs.PRIEST_DISCIPLINE: case Common.TalentSpecs.PRIEST_HOLY: case Common.TalentSpecs.PRIEST_SHADOW: case Common.TalentSpecs.SHAMAN_ELEMENTAL: case Common.TalentSpecs.SHAMAN_RESTORATION: case Common.TalentSpecs.DRUID_BALANCE: case Common.TalentSpecs.DRUID_RESTORATION: return this.GetCasterStats(); default: print(`Unknown Talent Spec: ${this.talentSpecId}`); } } public talentSpecName() { // print(`Talent Spec: ${this.talentSpecId}`); const keys = Object.keys(Common.TalentSpecs); for(let i=0; i < keys.length; i++) { if(Common.TalentSpecs[keys[i]] === this.talentSpecId) { return keys[i]; } } } private _lookupEquipment(): EquipmentList { const myEquipment = {} as EquipmentList; for(let slot=0; slot <= Common.BotEquipLast; slot++) { const equipment = this.myself.GetBotEquipment(slot); if(equipment) { myEquipment[slot] = { entry: equipment.GetEntry(), link: equipment.GetItemLink(), quality: equipment.GetQuality(), itemLevel: equipment.GetItemLevel(), enchantmentId: equipment.GetEnchantmentId(0), // Only the permenant enchantments } } else { myEquipment[slot] = undefined; } } return myEquipment; } private _lookupStats(panel: 'left' | 'right'): Record[] { const statMappings = this.GetStatMappings(); const classStats: Record[] = [] for(let stat = 0; stat < statMappings[panel].length; stat++) { const statName = statMappings[panel][stat]; let statValue = this.allStats[statName]; const statRecord= {}; // skip offhand stats will be handled with main hand if(statName === 'Dmg Off') { continue; } // handle some special cases for stats if(statName === 'Damage') { statRecord[statName] = statValue; classStats.push(statRecord); // Go ahead and add dual wield damage also if(this.isDualWield()) { //statRecord['Dmg Off'] = statValue; //classStats.push(statRecord); } continue; } if(this.isHealer() && statName === 'Bonus Dmg') { statRecord['Bonus Heals'] = statValue; classStats.push(statRecord); // print(`Stat: Bonus Heals = ${statValue}`); continue; } if(statName && statValue) { statRecord[statName] = statValue; classStats.push(statRecord); // print(`Stat: ${statName} = ${statValue}`); } else { // print("failed to get stat: " + statName); } } return classStats; } private parseStats(botdump: string) { const stats = botdump.split('\n'); for(let i=0; i `BotMgr${name}` + (entry ? entry : ''); const compId = (botId: number, name: string) => `${botId}:BotMgr${name}`; // includes of global polyfills in main file for submodules let incObjectEntries = { 1: 'inlude'}; Object.entries(incObjectEntries); let incParseInt = parseInt('1'); function ucase(input: string): string { if (input.length === 0) { return input; // Return unchanged if the input is an empty string } const firstLetter = input.charAt(0).toUpperCase(); let restOfTheString = input.slice(1).toLowerCase(); if(input.slice(-1) == "1" || input.slice(-1) == "2") { restOfTheString = restOfTheString.slice(0, -1); } return firstLetter + restOfTheString; } function humanizeName(input: string): string { if (input.length === 0) { return input; // Return unchanged if the input is an empty string } const parts = input.split("_"); parts[0] = parts[0].toLowerCase(); parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1); parts[1] = parts[1].toLowerCase(); parts[1] = parts[1].charAt(0).toUpperCase() + parts[1].slice(1); return `${parts[1]} ${parts[0]}`; } // If we are a client file. aio.AddAddon() will return false and this file will be serialized and sent to client. if(!aio.AddAddon()) { const botMgrHandlers = aio.AddHandlers('BotMgr', {}); const InfoFramePool: Map = new Map(); const ComponentsPool: Map = new Map(); // key botId + ":" + componentid const ItemClickFuncs: Map = new Map(); // containerid (1-13):itemslotId (1-36) => click function const botStorage: BotStorage = new BotStorage(); let BotItemTooltip: WoWAPI.GameTooltip; function AddResistFrame(parent: WoWAPI.Frame, botData: BotData) { const resistFrame = CreateFrame("Frame", id("ResistsFrame"), parent); resistFrame.SetSize(32, 160); resistFrame.SetPoint("TOPRIGHT", parent, "TOPLEFT", 297, -77); const magicRes1 = CreateFrame("Frame", id("MagicResFrame1"), resistFrame, "MagicResistanceFrameTemplate", 1); magicRes1.SetPoint("TOP", 0, 0); magicRes1.SetSize(32, 32); const magResBack1 = magicRes1.CreateTexture(id("MagicResTexture1"), "BACKGROUND"); magResBack1.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ResistanceIcons"); magResBack1.SetTexCoord(0, 1, 0.2265, 0.3398); magResBack1.SetAllPoints(magicRes1); const magResFont1 = magicRes1.CreateFontString(id("Resist2"), "BACKGROUND", "GameFontHighlightSmall"); magResFont1.SetPoint("BOTTOM", magResBack1, null, 0, 3); magResFont1.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats["Resistance: arcane"]}`); ComponentsPool.set(compId(botData.entry, "Resist1"), magResFont1); // End Arcance Resistance const magicRes2 = CreateFrame("Frame", id("MagicResFrame2"), resistFrame, "MagicResistanceFrameTemplate", 2); magicRes2.SetPoint("TOP", magicRes1, "BOTTOM", 0, 0); magicRes2.SetSize(32, 32); const magResBack2 = magicRes2.CreateTexture(id("MagicResTexture2"), "BACKGROUND"); magResBack2.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ResistanceIcons"); magResBack2.SetTexCoord(0, 1, 0, 0.11328125); magResBack2.SetAllPoints(magicRes2); const magResFont2 = magicRes1.CreateFontString(id("Resist2"), "BACKGROUND", "GameFontHighlightSmall"); magResFont2.SetPoint("BOTTOM", magicRes2, null, 0, 3); magResFont2.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats["Resistance: fire"]}`); ComponentsPool.set(compId(botData.entry, "Resist2"), magResFont2); // end fire resistance const magicRes3 = CreateFrame("Frame", id("MagicResFrame3"), resistFrame, "MagicResistanceFrameTemplate", 3); magicRes3.SetPoint("TOP", magicRes2, "BOTTOM", 0, 0); magicRes3.SetSize(32, 32); const magResBack3 = magicRes3.CreateTexture(id("MagicResTexture3"), "BACKGROUND"); magResBack3.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ResistanceIcons"); magResBack3.SetTexCoord(0, 1, 0.11328125, 0.2265625); magResBack3.SetAllPoints(magicRes3); const magResFont3 = magicRes3.CreateFontString(id("Resist3"), "BACKGROUND", "GameFontHighlightSmall"); magResFont3.SetPoint("BOTTOM", magicRes3, null, 0, 3); magResFont3.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats["Resistance: nature"]}`); ComponentsPool.set(compId(botData.entry, "Resist3"), magResFont3); // end nature resistance const magicRes4 = CreateFrame("Frame", id("MagicResFrame4"), resistFrame, "MagicResistanceFrameTemplate", 4); magicRes4.SetPoint("TOP", magicRes3, "BOTTOM", 0, 0); magicRes4.SetSize(32, 32); const magResBack4 = magicRes4.CreateTexture(id("MagicResTexture4"), "BACKGROUND"); magResBack4.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ResistanceIcons"); magResBack4.SetTexCoord(0, 1, 0.33984375, 0.453125); magResBack4.SetAllPoints(magicRes4); const magResFont4 = magicRes4.CreateFontString(id("Resist4"), "BACKGROUND", "GameFontHighlightSmall"); magResFont4.SetPoint("BOTTOM", magicRes4, null, 0, 3); magResFont4.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats["Resistance: frost"]}`); ComponentsPool.set(compId(botData.entry, "Resist4"), magResFont4); // end frost resistance const magicRes5 = CreateFrame("Frame", id("MagicResFrame5"), resistFrame, "MagicResistanceFrameTemplate", 5); magicRes5.SetPoint("TOP", magicRes4, "BOTTOM", 0, 0); magicRes5.SetSize(32, 32); const magResBack5 = magicRes5.CreateTexture(id("MagicResTexture5"), "BACKGROUND"); magResBack5.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ResistanceIcons"); magResBack5.SetTexCoord(0, 1, 0.453125, 0.56640625); magResBack5.SetAllPoints(magicRes5); const magResFont5 = magicRes5.CreateFontString(id("Resist5"), "BACKGROUND", "GameFontHighlightSmall"); magResFont5.SetPoint("BOTTOM", magicRes5, null, 0, 3); magResFont5.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats["Resistance: shadow"]}`); ComponentsPool.set(compId(botData.entry, "Resist5"), magResFont5); // end shadow resistance } function AddPortrait(parent: WoWAPI.Frame, botData: BotData) { const portrait = parent.CreateTexture(id("Portrait", botData.entry), "ARTWORK"); portrait.SetPoint("TOPLEFT", 10, -7); portrait.SetSize(58, 58); SetPortraitTexture(portrait, "target"); const characterName = CreateFrame("Frame", id("CharacterName", botData.entry), parent); characterName.SetSize(109,12); characterName.SetPoint("CENTER",6,232); characterName.SetScript("OnLoad", (frame) => { RaiseFrameLevel(frame); }); const charFont = characterName.CreateFontString(id("CharacterNameFont", botData.entry), "BACKGROUND", "GameFontNormal"); charFont.SetText(GetUnitName("target", false)); charFont.SetSize(300,12); charFont.SetPoint("CENTER",0,0); charFont.SetTextColor(1,1,1,1); const infoTextFrame = CreateFrame("Frame", id("InfoTextFrame", botData.entry), parent); infoTextFrame.SetSize(200,12); infoTextFrame.SetPoint("CENTER", characterName, "BOTTOM", 0, -15); infoTextFrame.SetScript("OnLoad", (frame) => { RaiseFrameLevel(frame); }); const infoTextFont = infoTextFrame.CreateFontString(id("InfoTextFont", botData.entry), "BACKGROUND", "GameFontHighlightSmall"); if(botData.classId > 10) { infoTextFont.SetText(`${YELLOW_FONT_COLOR_CODE} Level ${UnitLevel("target")} ${botData.class}`); } else { infoTextFont.SetText(`${YELLOW_FONT_COLOR_CODE} Level ${UnitLevel("target")} ${botData.race} ${botData.class}`); } const spec = infoTextFrame.CreateFontString(id("SpecFont", botData.entry), "BACKGROUND", "GameFontHighlightSmall"); spec.SetText(`${botData.talentSpecName}`); spec.SetPoint("TOP", infoTextFont, "BOTTOM", 0, -2); ComponentsPool.set(compId(botData.entry, "SpecFont"), spec); infoTextFont.SetSize(300,12); infoTextFont.SetPoint("CENTER",0,0); } function AddCharacterModel(parent: WoWAPI.Frame, botData: BotData) { const frameChar = CreateFrame("PlayerModel", id("ModelFrame", botData.entry), parent, null, botData.entry); frameChar.SetPoint("TOP", -5, -82); frameChar.SetSize(240, 175); frameChar.SetUnit("target"); frameChar.SetFacing(0.3); frameChar.SetAlpha(0.65); frameChar.SetGlow(0.9); frameChar.SetFrameStrata("MEDIUM"); } function UpdateEquipFrame(group: 'left' | 'right' | 'weapons', parent: WoWAPI.Frame, botData: BotData) { let slotOrder = []; switch(group) { case 'left': slotOrder = [BotEquipSlot.HEAD,BotEquipSlot.NECK,BotEquipSlot.SHOULDERS,BotEquipSlot.BACK,BotEquipSlot.CHEST,-1,-2,BotEquipSlot.WRIST]; break; case 'right': slotOrder = [BotEquipSlot.HANDS,BotEquipSlot.WAIST,BotEquipSlot.LEGS,BotEquipSlot.FEET,BotEquipSlot.FINGER1,BotEquipSlot.FINGER2,BotEquipSlot.TRINKET1,BotEquipSlot.TRINKET2]; break; case 'weapons': slotOrder = [BotEquipSlot.MAINHAND,BotEquipSlot.OFFHAND,BotEquipSlot.RANGED]; break; } // loop through the left equipment and create the buttons and texture. for(let i = 0; i < slotOrder.length; i++) { const itemSlotId = slotOrder[i] >= 0 ? slotOrder[i] : 92+slotOrder[i]; const equipSlot = CreateFrame("Button", id(`${group}-EquipmentSlot-${slotOrder[i]}`), parent, "ItemButtonTemplate", itemSlotId); if(group === 'weapons') equipSlot.SetPoint("TOPLEFT", i*40+1, 0); else equipSlot.SetPoint("TOPLEFT", 0, -i*40-1); equipSlot.SetSize(40, 40); equipSlot.SetScript("OnEnter", ItemSlotOnEnter); equipSlot.SetScript("OnLeave", ItemSlotOnLeave); equipSlot.SetScript("OnClick", ItemSlotOnClick); const equippedItem: Equipment = botData.equipment[slotOrder[i]]; let itemIcon: WoWAPI.TexturePath; let itemId: number; let idsuffix: string | number; // If it is a shirt or tabard which are not supported just show the background texture. if(slotOrder[i] < 0) { const shirtOrTabard = (slotOrder[i] === -1) ? "SHIRT" : "TABARD"; [itemId, itemIcon] = GetInventorySlotInfo(UIInvSlot[`${shirtOrTabard}SLOT`]); idsuffix = shirtOrTabard } // If we have a piece of equipment add the icon template if(equippedItem && equippedItem.entry > 0) { itemIcon = GetItemIcon(equippedItem.entry); idsuffix = slotOrder[i]; } // If there is not a piece of equipment add the background texture if(!equippedItem && slotOrder[i] > 0) { let slotName = BotSlotName[slotOrder[i]]; if(slotOrder[i] === BotEquipSlot.FINGER1) slotName = "FINGER0"; if(slotOrder[i] === BotEquipSlot.FINGER2) slotName = "FINGER1"; if(slotOrder[i] === BotEquipSlot.TRINKET1) slotName = "TRINKET0"; if(slotOrder[i] === BotEquipSlot.TRINKET2) slotName = "TRINKET1"; if(slotOrder[i] === BotEquipSlot.OFFHAND) slotName = "SECONDARYHAND"; [itemId, itemIcon] = GetInventorySlotInfo(UIInvSlot[`${slotName}SLOT`]); idsuffix = slotOrder[i]; } const itemTexture = equipSlot.CreateTexture(id(`ItemTexture-${idsuffix}`), "OVERLAY"); itemTexture.SetTexture(itemIcon); itemTexture.SetPoint("CENTER", 0, 0); itemTexture.SetSize(36,36); ComponentsPool.set(compId(botData.entry, `ItemSlotTexture-${itemSlotId}`), itemTexture); } } function AddEquipmentFrames(parent: WoWAPI.Frame, botData: BotData) { // Get all our frames const frames = { left: ComponentsPool.get(compId(botData.entry, "LeftEquipment")), right: ComponentsPool.get(compId(botData.entry, "RightEquipment")), weapons: ComponentsPool.get(compId(botData.entry, "WeaponsEquipment")) }; let equipFrame: WoWAPI.Frame; if(!frames.left) { equipFrame = CreateFrame("Frame", id("LeftEquipment"), parent, null, 1); equipFrame.SetPoint("TOPLEFT", 20, -73); equipFrame.SetSize(40, 330); UpdateEquipFrame('left', equipFrame, botData); ComponentsPool.set(compId(botData.entry, "LeftEquipment"), equipFrame); } if(!frames.right) { equipFrame = CreateFrame("Frame", id("RightEquipment"), parent, null, 2); equipFrame.SetPoint("TOPRIGHT", -40, -73); equipFrame.SetSize(40, 330); UpdateEquipFrame('right', equipFrame, botData); ComponentsPool.set(compId(botData.entry, "RightEquipment"), equipFrame); } if(!frames.weapons) { equipFrame = CreateFrame("Frame", id("WeaponEquipment"), parent, null, 3); equipFrame.SetPoint("CENTER", -10, -147); equipFrame.SetSize(129, 40); UpdateEquipFrame('weapons', equipFrame, botData); ComponentsPool.set(compId(botData.entry, "WeaponsEquipment"), equipFrame); } } function AddStats(parent: WoWAPI.Frame | undefined, botData: BotData) { const leftStats = botData.leftStats; const rightStats = botData.rightStats; for(let i =0; i < leftStats.length; i++) { const statName = Object.keys(leftStats[i])[0]; let statLabel = ComponentsPool.get(compId(botData.entry, `StatName-${statName}`)); if(!statLabel) { statLabel = parent.CreateFontString(id(`StatName-${statName}`), "ARTWORK", "GameFontNormalSmall"); statLabel.SetPoint("TOPLEFT", parent, "TOPLEFT", 5, -7 - (i * 14)); statLabel.SetJustifyH("LEFT"); statLabel.SetText(`${statName}:`); ComponentsPool.set(compId(botData.entry, `StatName-${statName}`), statLabel); } let statValue = ComponentsPool.get(compId(botData.entry, `StatValue-${statName}`)); // if there is not an existing component create a new one if(!statValue) { statValue = parent.CreateFontString(id(`StatValue-${statName}`), "ARTWORK", "GameFontNormalSmall"); statValue.SetPoint("TOPRIGHT", parent, "TOP", -4, -5 - (i * 14)); if(statName === "Damage") { statValue.SetSize(90, 14); } else { statValue.SetSize(50, 14); } statValue.SetJustifyH("RIGHT"); ComponentsPool.set(compId(botData.entry, `StatValue-${statName}`), statValue); } statValue.SetText(`${colors('white')}${leftStats[i][statName]}`); } for(let i =0; i < rightStats.length; i++) { const statName = Object.keys(rightStats[i])[0]; let statLabel = ComponentsPool.get(compId(botData.entry, `StatName-${statName}`)); if(!statLabel) { statLabel = parent.CreateFontString(id(`StatName-${statName}`), "ARTWORK", "GameFontNormalSmall"); statLabel.SetPoint("TOPLEFT", parent, "TOPLEFT", 118, -7 - (i * 14)); statLabel.SetText(`${statName}:`); statLabel.SetJustifyH("LEFT"); ComponentsPool.set(compId(botData.entry, `StatName-${statName}`), statLabel); } let statValue = ComponentsPool.get(compId(botData.entry, `StatValue-${statName}`)); if(!statValue) { statValue = parent.CreateFontString(id(`StatValue-${statName}`), "ARTWORK", "GameFontNormalSmall"); statValue.SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, -5 - (i * 14)); statValue.SetSize(50, 14); statValue.SetJustifyH("RIGHT"); ComponentsPool.set(compId(botData.entry, `StatValue-${statName}`), statValue); } statValue.SetText(`${colors('white')}${rightStats[i][statName]}`); } } function CreateStats(parent: WoWAPI.Frame, botData: BotData) { const statsFrame = CreateFrame("Frame", id("CharacterAttr"), parent, null, 1); statsFrame.SetSize(230,78); statsFrame.SetPoint("TOPLEFT", 67, -251); statsFrame.SetFrameLevel(parent.GetFrameLevel() + 1); // statsFrame.SetFrameStrata("LOW"); statsFrame.SetAlpha(1.0); statsFrame.SetBackdropColor(0,0,0,1.0); const leftTop = statsFrame.CreateTexture(id("StatLeftTop"), "BACKGROUND"); leftTop.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); leftTop.SetSize(115,16); leftTop.SetPoint("TOPLEFT", 0, 0); leftTop.SetTexCoord(0, 0.8984375, 0, 0.125); const leftmiddle = statsFrame.CreateTexture(id("StatLeftMiddle"), "BACKGROUND"); leftmiddle.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); leftmiddle.SetSize(115,95); leftmiddle.SetPoint("TOPLEFT", leftTop, "BOTTOMLEFT", 0, 0); leftmiddle.SetTexCoord(0, 0.8984375, 0.125, 0.1953125); const leftBottom = statsFrame.CreateTexture(id("StatLeftBottom"), "BACKGROUND"); leftBottom.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); leftBottom.SetSize(115,16); leftBottom.SetPoint("TOPLEFT", leftmiddle, "BOTTOMLEFT", 0, 0); leftBottom.SetTexCoord(0, 0.8984375, 0.484375, 0.609375); const rightTop = statsFrame.CreateTexture(id("StatRightTop"), "BACKGROUND"); rightTop.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); rightTop.SetSize(115,16); rightTop.SetPoint("TOPLEFT", leftTop, "TOPRIGHT",0, 0); rightTop.SetTexCoord(0, 0.8984375, 0, 0.125); const rightMiddle = statsFrame.CreateTexture(id("StatRightMiddle"), "BACKGROUND"); rightMiddle.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); rightMiddle.SetSize(115,95); rightMiddle.SetPoint("TOPLEFT", leftmiddle, "TOPRIGHT", 0, 0); rightMiddle.SetTexCoord(0, 0.8984375, 0.125, 0.1953125); const rightBottom = statsFrame.CreateTexture(id("StatRightBottom"), "BACKGROUND"); rightBottom.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"); rightBottom.SetSize(115,16); rightBottom.SetPoint("TOPLEFT", leftBottom, "TOPRIGHT", 0, 0); rightBottom.SetTexCoord(0, 0.8984375, 0.484375, 0.609375); AddStats(statsFrame, botData); } function SetBackground(parent: WoWAPI.Frame) { // Left corner const leftUpper = parent.CreateTexture(id("BgUpperLeft"), "BACKGROUND"); leftUpper.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-CharacterTab-L1"); leftUpper.SetSize(256,256); leftUpper.SetPoint("TOPLEFT"); // Right corner const rightUpper = parent.CreateTexture(id("BgUpperRight"), "BACKGROUND"); rightUpper.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-CharacterTab-R1"); rightUpper.SetSize(128,256); rightUpper.SetPoint("TOPRIGHT"); // left bottom const leftBottom = parent.CreateTexture(id("BgBottomLeft"), "BACKGROUND"); leftBottom.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-CharacterTab-L2"); leftBottom.SetSize(256,256); leftBottom.SetPoint("BOTTOMLEFT"); // right bottom const rightBottom = parent.CreateTexture(id("BgBottomRight"), "BACKGROUND"); rightBottom.SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-CharacterTab-R2"); rightBottom.SetSize(128,256); rightBottom.SetPoint("BOTTOMRIGHT"); // Close Button const closeButton = CreateFrame("Button", id("CloseButton"), parent, "UIPanelCloseButton"); closeButton.SetPoint("CENTER", parent, "TOPRIGHT", -44, -25); closeButton.SetScript("OnClick", () => { parent.Hide(); }); } function AddSoundEffects(frame: WoWAPI.Frame) { frame.SetScript("OnShow", (frame) => { PlaySound("igCharacterInfoOpen"); }); frame.SetScript("OnHide", (frame) => { PlaySound("igCharacterInfoClose"); }); } /** * START OF EVENT HANDLERS */ function ItemSlotOnEnter(frame: WoWAPI.Button) { const botId = botStorage.GetActive(); const theItem = botStorage.GetBotItem(botId, frame.GetID()); GameTooltip.SetOwner(frame, "ANCHOR_RIGHT"); if(theItem) { GameTooltip.SetHyperlink(theItem.link); } else { if(frame.GetID() == 90) { GameTooltip.SetText("Tabard"); } else if(frame.GetID() == 91) { GameTooltip.SetText("Shirt"); } else { GameTooltip.SetText( ucase(BotSlotName[frame.GetID()]) ); } } if(CursorHasItem()) { const [compItem, compItemId, compItemLink] = GetCursorInfo(); const BotTooltip = ComponentsPool.get(compId(botId, "tooltip")); BotTooltip.SetOwner(frame, "ANCHOR_LEFT"); BotTooltip.SetHyperlink(compItemLink); BotTooltip.Show(); } GameTooltip.Show(); } function ItemSlotOnLeave(frame: WoWAPI.Button) { const botId = botStorage.GetActive(); const BotTooltip = ComponentsPool.get(compId(botId, "tooltip")); BotTooltip.Hide(); GameTooltip.Hide(); } function ItemSlotOnClick(frame: WoWAPI.Button, button: string) { const botId = botStorage.GetActive(); const theItem = botStorage.GetBotItem(botId, frame.GetID()); const [compItem, compItemId, compItemLink] = GetCursorInfo(); // IF we have a bank item since it is not our inventory it will crash the server so store it then send equip const bankItem = botStorage.GetFromBank(); if(bankItem) { for(let i=0; i <= 4; i++) { if(GetContainerNumFreeSlots(i)) { if(i === 0) { PutItemInBackpack(); } else { PutItemInBag(i); } } } } // Special case to handle unquipping items via modified click if(IsModifiedClick("AUTOLOOTTOGGLE")) { if(theItem && !compItem) { aio.Handle("BotMgr", "UnequipTheItem", GetUnitName("player", false), frame.GetID(), botId); return; } } if(theItem && !compItem) { if(button == "LeftButton") { PickupItem(theItem.link); // print('Set Bot Pickup Item', botId, theItem.entry, theItem.link); botStorage.BotItemPickedUp(botId, theItem.entry, theItem.link); return; } } if(compItem) { const slot = frame.GetID(); // if we have a bot virtual item in hand if(botStorage.IsPickedUp()) { const botItemInHand = botStorage.GetItemInHand(); // first unequip item on target bot aio.Handle("BotMgr", "UnequipTheItem", GetUnitName("player", false), slot, botItemInHand.bot); aio.Handle("BotMgr", "EquipTheItem", GetUnitName("player", false), botId, slot, compItemId, compItemLink); } else { aio.Handle("BotMgr", "EquipTheItem", GetUnitName("player", false), botId, slot, compItemId, compItemLink); } // Attempt to equip the item. PlaySound("INTERFACESOUND_CURSORDROPOBJECT"); ClearCursor(); } } botMgrHandlers.OnEquipSuccess = (botId: number, slot: BotEquipmentSlotNum, item: Equipment) => { const itemTexture = ComponentsPool.get(compId(botId, `ItemSlotTexture-${slot}`)); itemTexture.SetTexture(GetItemIcon(item.entry)); // Hide Tooltips otherwise it will show old item. const BotTooltip = ComponentsPool.get(compId(botId, "tooltip")); botStorage.SetBotItem(botId, slot, item); BotTooltip.Hide(); GameTooltip.Hide(); } botMgrHandlers.OnUnEquipSuccess = (botId: number, slot: BotEquipmentSlotNum) => { const itemTexture = ComponentsPool.get(compId(botId, `ItemSlotTexture-${slot}`)); /** TO DO move to generic function for getting textures right now is copy/paste */ let slotName: string = BotSlotName[slot]; if(slot === BotEquipSlot.FINGER1) slotName = "FINGER0"; if(slot === BotEquipSlot.FINGER2) slotName = "FINGER1"; if(slot === BotEquipSlot.TRINKET1) slotName = "TRINKET0"; if(slot === BotEquipSlot.TRINKET2) slotName = "TRINKET1"; if(slot === BotEquipSlot.OFFHAND) slotName = "SECONDARYHAND"; const [, itemIcon] = GetInventorySlotInfo(UIInvSlot[`${slotName}SLOT`]); itemTexture.SetTexture(itemIcon); // Hide Tooltips otherwise it will show old item. const BotTooltip = ComponentsPool.get(compId(botId, "tooltip")); BotTooltip.Hide(); GameTooltip.Hide(); } botMgrHandlers.OnEquipFail = (botId: number, slot: BotEquipmentSlotNum, itemId: number, itemLink: string) => { PlaySound("ITEMGENERICSOUND"); botStorage.BotItemCursorClear(); ClearCursor(); } botMgrHandlers.OnUnEquipFail = (botId: number, slot: BotEquipmentSlotNum) => { PlaySound("ITEMGENERICSOUND"); botStorage.BotItemCursorClear(); ClearCursor(); } botMgrHandlers.UpdateBotData = (data: BotData) => { botStorage.SetBotData(data.entry, data); UpdateBotFrame(data); } function HandleUnequipItem(itemButton: WoWAPI.Button, isBankSlot: boolean = false): void { const slotNum = itemButton.GetID(); const bagId = itemButton.GetParent().GetID(); if(!GetContainerItemLink((isBankSlot) ? -1 : bagId, slotNum)) { if(botStorage.IsPickedUp()) { const item = botStorage.GetItemInHand(); aio.Handle("BotMgr", "UnequipTheItem", GetUnitName("player", false), item.slot, item.bot); } } } /** * This handles listening on Bot Items being dragged to the bag. Attaches * to the default handler before run and handles bot items specifically. */ function StoreItemSlotHandlers(): void { // Intercept Bank Item Slots Click Event for(let bankSlot = 1; bankSlot <= _G[`NUM_BANKGENERIC_SLOTS`]; bankSlot++) { ItemClickFuncs.set(`bank:${bankSlot}`, _G[`BankFrameItem${bankSlot}`].GetScript("OnClick")); _G[`BankFrameItem${bankSlot}`].SetScript("OnClick", (frame: WoWAPI.Button, ...args) => { HandleUnequipItem(frame, true); const callback = ItemClickFuncs.get(`bank:${frame.GetID()}`); (callback) ? callback(frame, ...args) : null; //print(`No callback for bank:${bankSlot}`) if(CursorHasItem()) { const [compItem, compItemId, compItemLink] = GetCursorInfo(); botStorage.SetFromBank({ slot: frame.GetID(), link: compItemLink, entry: compItemId }); } }); } } function UpdateBotFrame(botData: BotData) { // Set the new Talent Spec const talentSpec = ComponentsPool.get(compId(botData.entry, "SpecFont")); talentSpec.SetText(botData.talentSpecName); // Update Resist Frames let resist = ComponentsPool.get(compId(botData.entry, "Resist1")); resist.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats['Resistance: arcane']}`); resist = ComponentsPool.get(compId(botData.entry, "Resist2")); resist.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats['Resistance: fire']}`); resist = ComponentsPool.get(compId(botData.entry, "Resist3")); resist.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats['Resistance: nature']}`); resist = ComponentsPool.get(compId(botData.entry, "Resist4")); resist.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats['Resistance: frost']}`); resist = ComponentsPool.get(compId(botData.entry, "Resist5")); resist.SetText(`${GREEN_FONT_COLOR_CODE}${botData.allStats['Resistance: shadow']}`); // Update the stats frame AddStats(undefined, botData); } /** * Shows or Creates a new Bot Equipment Management Frame * Every NPC Bot that is requested to be managed will get their own unique frame. This * reduces what textures and subframes need to be reloaded. For instance 3d models, portraits. * * Each Frame will be keyed on a Frame Manager by EntryID. This should not cause performance issues as * each player is limited to the number of NPC bots they can manage. * * @param player * @param botdetails * @returns * @noSelf */ function ShowBotFrame(botData: BotData) { let mainFrame: WoWAPI.Frame = null; mainFrame = InfoFramePool.get(botData.entry); // Build the complete frame if we do not already have one in the pool. if(!mainFrame) { mainFrame = CreateFrame("Frame", id("MainFrame"+botData.entry), UIParent, null, botData.entry); mainFrame.SetPoint("TOPLEFT", 300, -204); mainFrame.SetSize(384, 512); mainFrame.SetFrameLevel(5); mainFrame.SetMovable(true); mainFrame.EnableMouse(true); mainFrame.RegisterForDrag("LeftButton"); mainFrame.SetScript("OnDragStart", mainFrame.StartMoving); mainFrame.SetScript("OnHide", mainFrame.StopMovingOrSizing); mainFrame.SetScript("OnDragStop", mainFrame.StopMovingOrSizing); mainFrame.SetScript("OnEnter", (frame) => { botStorage.SetActive(frame.GetID()); frame.SetFrameLevel(20); }); mainFrame.SetScript("OnLeave", (frame) => { frame.SetFrameLevel(5); }); BotItemTooltip = CreateFrame("GameTooltip", id("ItemToolTip"+botData.entry), mainFrame, "GameTooltipTemplate", botData.entry); BotItemTooltip.SetOwner(mainFrame, "ANCHOR_NONE"); BotItemTooltip.Hide(); // Build all elements of the frame on creation. SetBackground(mainFrame); AddPortrait(mainFrame, botData); AddCharacterModel(mainFrame, botData); AddResistFrame(mainFrame, botData); AddEquipmentFrames(mainFrame, botData); CreateStats(mainFrame, botData); AddSoundEffects(mainFrame); InfoFramePool.set(botData.entry, mainFrame); ComponentsPool.set(compId(botData.entry, "tooltip"), BotItemTooltip); mainFrame.Show(); // mainFrame.RegisterEvent("CURSOR_UPDATE"); // mainFrame.RegisterEvent("ITEM_LOCK_CHANGED"); mainFrame.RegisterEvent("ITEM_UNLOCKED"); mainFrame.SetScript("OnEvent", (frame: WoWAPI.Frame, eventName: WoWAPI.Event, ...args) => { if(eventName === "ITEM_UNLOCKED") { botStorage.ClearFromBank(); } }); } else { mainFrame.Show(); UpdateBotFrame(botData); } } botMgrHandlers.ShowFrame = (botData: BotData) => { botStorage.UpdateBotData(botData.entry, botData); ShowBotFrame(botData); } // Global calls to set things up StoreItemSlotHandlers(); } /** @ts-expect-error */ let aio: AIO = {}; const SCRIPT_NAME = 'BotMgr'; import { Logger } from "../../classes/logger"; import { BotUnit } from "./botUnit"; const log = new Logger(SCRIPT_NAME); import { BotStat, BotEquipLast, ClassesMapping, CharacterClass, RacesMapping, CharacterRace, QualityType } from "../../constants/idmaps"; export type Equipment = { entry: number, link: string, quality?: QualityType, itemLevel?: number, enchantmentId?: number, } export type EquipmentList = Record; /** * Everything we ever wanted to know about the bot info on load */ export type BotData = { owner: string, name: string, level: number, talentSpec: number, talentSpecName: string, roles: number, entry: number, class: CharacterClass, classId: number, race: CharacterRace, raceId: number, equipment?: EquipmentList, // SlotName - ItemId See BotEquipSlot leftStats?: Record[], rightStats?: Record[], allStats?: Record // StatId - Value }; /** * @todo Move to a data mgr class eventually */ const NpcDetailStorage = {} as Record; /** * Get the current targetted npc bot or returns undefined if not a bot. * @param player * @returns Creature | undefined * @noSelf */ function GetBotNpc(player: Player): Creature | undefined { try { const target = player.GetSelection(); const creature = target.ToCreature(); if(!creature.IsNPCBot()) { return; } return creature; } catch (e) { log.error(`Could not lookup bot npc: ${e}`); } } /** * This target is eligible for the player to manage otherwise ship them a friendly error message * @param player * @returns boolean * @noSelf */ function TargetIsEligible(player: Player) { const creature = GetBotNpc(player); if(creature) { const botOwner = creature.GetBotOwner(); if(botOwner.GetGUID() == player.GetGUID()) { log.info(`Target is a NPCBot that can be managed by the player`); return true; } } return false; } /** * Used to retrieve the bot for the player * @param player * @returns Creature */ function GetBotForPlayer(player: string, botEntry: number) { try { const owner = GetPlayerByName(player); const creatures = owner.GetCreaturesInRange(300, botEntry) as Creature[]; const bot = creatures[0]; return bot; } catch (e) { log.error(`Could not get bot for player ${player}: ${e}`); } } /** * @noSelf */ function GetBotDetails(bot: Creature): BotData { try { const botUnit = new BotUnit(bot); NpcDetailStorage[bot.GetEntry()] = botUnit.toBotData(); } catch (e) { log.error(`Could not get bot details: ${e}`); } return NpcDetailStorage[bot.GetEntry()]; } /** * Sends a client message with update bot details typically fired * after an equipment event. * @param player * @param botEntry */ function RefreshBotData(bot: Creature): void { try { bot.RegisterEvent((delay:number, repeats:number, bot: Creature) => { const data = GetBotDetails(bot); log.info(`Sending bot details to player: ${bot.GetBotOwner().GetName()}`); aio.Handle(bot.GetBotOwner(), 'BotMgr', 'UpdateBotData', data); }, 650, 1); } catch (e) { log.error(`Could not send bot details: ${e}`); } } /** * Equip an item for the bot and update bot details * @param event * @param player * @param command * @returns */ function EquipTheItem(player: string, botEntry: number, slot: BotEquipmentSlotNum, item: number, link: string ): void { if(botEntry && typeof botEntry !== 'number') { return; } try { const bot = GetBotForPlayer(player, botEntry); let data; const isEligible = bot.BotCanEquipItem(item, slot); if(!isEligible) { log.error(`Bot cannot equip item: ${item} in slot: ${slot}`); return; } // already equipped if(bot.BotEquipItem(item, slot)) { data = GetBotDetails(bot); // log.log(`Bot successfully equipped item: ${item} in slot: ${slot}`); aio.Handle(bot.GetBotOwner(), 'BotMgr', 'OnEquipSuccess',bot.GetEntry(), slot, data.equipment[slot]); RefreshBotData(bot); } else { // log.error(`Bot failed to equip item: ${item} in slot: ${slot}`); aio.Handle(bot.GetBotOwner(), 'BotMgr', 'OnEquipFail', bot.GetEntry(), slot, item, link); } } catch (error) { log.error(`Error equipping item: ${error}`); } } function UnequipTheItem(player: string, slot: number, botEntry: number): void { try { const bot = GetBotForPlayer(player, botEntry); if(bot.BotUnequipBotItem(slot)) { let data = GetBotDetails(bot); log.log(`Bot successfully unequipped item at slot: ${slot}`); aio.Handle(bot.GetBotOwner(), 'BotMgr', 'OnUnEquipSuccess',bot.GetEntry(), slot); RefreshBotData(bot); } else { log.error(`Bot failed to equip item in slot: ${slot}`); aio.Handle(bot.GetBotOwner(), 'BotMgr', 'OnUnEquipFail', bot.GetEntry(), slot); } } catch (error) { log.error(`Error unequipping item: ${error}`); } } const ShowBotMgr: player_event_on_command = (event: number,player: Player, command: string): boolean => { if(command == 'botmgr') { if(TargetIsEligible(player)) { const botdetails = GetBotDetails(GetBotNpc(player)); aio.Handle(player, 'BotMgr', 'ShowFrame', botdetails); return false; } else { player.PlayDirectSound(8959, player); // Play error sound (no money sound player.SendNotification("That is not a NPCBot that you can manage!"); return false; } } return true; } /*** * @noSelf */ function GetBotPanelInfo(player: Player): void { const target = player.GetSelection(); const creature = target.ToCreature(); if(!creature.IsNPCBot()) { return; } try { const target = player.GetSelection(); PrintInfo(`Server ${target.GetGUID()}`); const entry = GetGUIDEntry(target.GetGUID()); print(`BotMgr: Parsing Bot Entry: ${entry}`); } catch (e) { print(`BotMgr: Error parsing bot entry: ${e}`); } } const botMgrHandlers = aio.AddHandlers('BotMgr', { TargetIsEligible, GetBotPanelInfo, "EquipTheItem": EquipTheItem, "UnequipTheItem": UnequipTheItem }); RegisterPlayerEvent( PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => ShowBotMgr(...args) ); /** @ts-expect-error */ let aio: AIO = {}; if(!aio.AddAddon()) { const gamblerHandlers = aio.AddHandlers('GamblerMain', {}); const classImages = [ "Interface/Gambler/druid", "Interface/Gambler/deathknight", "Interface/Gambler/hunter", "Interface/Gambler/mage", "Interface/Gambler/paladin", "Interface/Gambler/priest", "Interface/Gambler/rogue", "Interface/Gambler/shaman", "Interface/Gambler/warlock", "Interface/Gambler/warrior", ]; let slotSpin = []; let multiplier = 1; // this function will randomly select a class image from the array above function getRandomClassImage() { const spinIndex = Math.floor(Math.random() * classImages.length); slotSpin.push(spinIndex); return classImages[spinIndex]; } // reset the spin function resetSpin () { slotSpin = []; } function determineWin(): number { let win = 0; let gold = 0; let tokens = 0; // Jackpot is all 3 slots as deathknight arthas if(slotSpin[0] == 1 && slotSpin[1] == 1 && slotSpin[2] == 1) { if(multiplier == 3) { tokens = 100; } gold = multiplier * 2000; win = 2; } if(slotSpin[0] == slotSpin[1] && slotSpin[1] == slotSpin[2]) { if(multiplier == 3) { tokens = 50; } gold = multiplier * 500; win = 1; } // Deathknights are considered wild cards if( (slotSpin[0] == slotSpin[1] && slotSpin[2] === 1) || (slotSpin[0] == slotSpin[2] && slotSpin[1] === 1) || (slotSpin[1] == slotSpin[2] && slotSpin[0] === 1) || (slotSpin[0] == 1 && slotSpin[1] === 1) || (slotSpin[0] == 1 && slotSpin[2] === 1) || (slotSpin[1] == 1 && slotSpin[2] === 1) ) { if(multiplier == 3) { tokens = 3; } gold = multiplier * 300; win = 1; } // handle two of the same class in a row if((slotSpin[0] == slotSpin[1]) && win == 0) { gold = multiplier * 150; win = 1; if(slotSpin[1] == 1) { if(multiplier == 3) { tokens = 3; } gold = multiplier * 150; win = 1; } } // Return money on any lich king wild if((slotSpin[0] == 1 || slotSpin[1] == 1 || slotSpin[2] == 1) && win == 0) { if(multiplier == 3) { tokens = 0; gold = 100; } else { tokens = 0; gold = 20; } win = 1; } if(win > 0) { PlaySoundFile("Sound\\Interface\\LootCoinLarge.wav", "Master"); aio.Handle("GamblerMain", "AwardSlotWin", gold, tokens); } return win; } function SpinSlots(SlotFrame: WoWAPI.Frame, Slot: WoWAPI.Texture[]) { let timer = 1; let counter = 1; PlaySoundFile("Sound\\Doodad\\GnomeMachine02StandLoop.wav", "Master"); SlotFrame.SetScript("OnUpdate", (frame, elapsed) => { timer = timer + elapsed; if(timer > 0.20) { counter = counter + 1; resetSpin(); timer = 0; Slot[0].SetTexture(getRandomClassImage()); Slot[1].SetTexture(getRandomClassImage()); Slot[2].SetTexture(getRandomClassImage()); if(counter > 22) { frame.SetScript("OnUpdate", null); determineWin(); } } }); } function ShowSlots(player: Player) { const GamblerMainFrame = CreateFrame("Frame", "GamblerMainFrame", UIParent, "UIPanelDialogTemplate"); GamblerMainFrame.SetSize(512,324); GamblerMainFrame.SetMovable(false); GamblerMainFrame.SetPoint("CENTER"); GamblerMainFrame.EnableMouse(true); GamblerMainFrame.EnableKeyboard(true); GamblerMainFrame.Hide(); const Title = GamblerMainFrame.CreateFontString("TitleFrame", "OVERLAY", "GameFontHighlight"); Title.SetPoint("TOPLEFT", 15, -10); Title.SetText("Heros Slots"); Title.SetFont("Fonts\\FRIZQT__.TTF", 10); // Slots Display Window const Slots = CreateFrame("Frame", "SlotsFrame", GamblerMainFrame); Slots.SetSize(420,160); Slots.SetPoint("CENTER", 0, 25); Slots.SetFrameLevel(1); Slots.SetBackdrop({ bgFile: "Interface/DialogFrame/UI-DialogBox-Background", edgeFile: "Interface/DialogFrame/UI-DialogBox-Border", tile: true, tileSize: 32, edgeSize: 32, insets: { left: 11, right: 12, top: 12, bottom: 11 } }); // Slot Columns 1 - 3 const Slot1 = Slots.CreateTexture("Slot1Texture", null, Slots); Slot1.SetSize(128,128); Slot1.SetAlpha(0.85); Slot1.SetPoint("TOPLEFT", 13, -16); Slot1.SetTexture(getRandomClassImage()); let [ Slot1Point, Slot1Region, Slot1RelPoint, x1offset, y1offset ] = Slot1.GetPoint(); const Slot2 = Slots.CreateTexture("Slot2Texture", null, Slots); Slot2.SetSize(128,128); Slot2.SetAlpha(0.85); Slot2.SetPoint("TOPLEFT", Slot1Region, Slot1RelPoint, x1offset + 128 + 5, y1offset); Slot2.SetTexture(getRandomClassImage()); let [ Slot2Point, Slot2Region, Slot2RelPoint, x2offset, y2offset ] = Slot2.GetPoint(); const Slot3 = Slots.CreateTexture("Slot3Texture", null, Slots); Slot3.SetSize(128,128); Slot3.SetAlpha(0.85); Slot3.SetPoint("TOPLEFT", Slot2Region, Slot2RelPoint, x2offset + 128 + 5, y2offset); Slot3.SetTexture(getRandomClassImage()); // Low bet button. const SpinButton = CreateFrame("Button", "SpinButtonLow", GamblerMainFrame, "UIPanelButtonTemplate"); SpinButton.SetSize(128,32); SpinButton.SetPoint("CENTER", -80, -80); SpinButton.SetText("Bet 20g Spin"); SpinButton.SetFrameLevel(2); SpinButton.SetScript("OnClick", (frame, mouse, button) => { resetSpin(); multiplier = 1; aio.Handle("GamblerMain", "PayForSpin", 20*10000); }); const SpinButtonHigh = CreateFrame("Button", "SpinButtonHigh", GamblerMainFrame, "UIPanelButtonTemplate"); SpinButtonHigh.SetSize(128,32); SpinButtonHigh.SetPoint("CENTER", 80, -80); SpinButtonHigh.SetText("Bet 100g Spin"); SpinButtonHigh.SetFrameLevel(2); SpinButtonHigh.SetScript("OnClick", (frame, mouse, button) => { resetSpin(); multiplier = 3; aio.Handle("GamblerMain", "PayForSpin", 100*10000); }); gamblerHandlers.StartSpin = (player: Player) => { SpinSlots(Slots, [Slot1, Slot2, Slot3]); SendChatMessage("Started a spin", "CHANNEL", null, "7"); } GamblerMainFrame.Show(); return GamblerMainFrame; } gamblerHandlers.ShowFrame = (player: Player) => { ShowSlots(player); } } /** @ts-expect-error */ let aio: AIO = {}; /** * Gambler - Slot Machine * This is the server side code used to add gambling games to the server. */ /** * Game OBject that will start the slot machine up */ const SLOT_GAME_OBJECT = 750001; const ShowGambler: player_event_on_command = (event: number,player: Player, command: string): boolean => { if(command == 'gamble') { aio.Handle(player, 'GamblerMain', 'ShowFrame'); return false; } return true; }; /** * @noSelf */ function PayForSpin(this:void, player: Player, cost: number): void { const money = player.GetCoinage(); if(money >= cost) { player.ModifyMoney(cost * -1); aio.Handle(player, 'GamblerMain', 'StartSpin'); } else { player.SendNotification("You don't have enough money to spin the slots!"); player.PlayDirectSound(8959, player); } } function AwardSlotWin(this:void, player: Player, gold: number, tokens: number): void { player.ModifyMoney(gold*10000); if(tokens > 0) { player.AddItem(910001, tokens); } if(tokens > 75) { player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I HIT THE JACKPOT! I won ${gold} gold and ${tokens} tokens!`, player); } else { if(tokens > 0) { player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I won ${gold} gold and ${tokens} tokens!`, player); } else { player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I won ${gold} gold`, player); } } } const SendSlotStart: gameobject_event_on_use = (event: number, gameobject: GameObject, player: Player): boolean => { aio.Handle(player, 'GamblerMain', 'ShowFrame'); return true; } const gamblerHandlers = aio.AddHandlers('GamblerMain', { PayForSpin, AwardSlotWin }); RegisterPlayerEvent( PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => ShowGambler(...args) ); RegisterGameObjectEvent(SLOT_GAME_OBJECT, GameObjectEvents.GAMEOBJECT_EVENT_ON_USE, (...args) => SendSlotStart(...args)); /** * Class for find out information about an account */ export type BasicCharacter = { guid: number, name: string } export class AccountInfo { private accountId: number; constructor(accountId: number) { this.accountId = accountId; } GetAccountMoney(): number { const result = CharDBQuery(`SELECT SUM(Money) as AccountMoney from acore_characters.characters WHERE account = ${this.accountId}`); const row = result.GetRow() as Record; return row.AccountMoney; } GetCharacters(): BasicCharacter[] { const result = CharDBQuery(`SELECT guid, name from characters WHERE account = ${this.accountId}`); const characters: BasicCharacter[] = []; for(let i=0; i < result.GetRowCount(); i++) { const row = result.GetRow(); characters.push({ guid: row.guid as number, name: row.name as string }); result.NextRow(); } return characters; } } export function GetGroupSize(player: Player): number { const group = player.GetGroup(); let groupCount = 0; if(group != undefined) { const members = group.GetMembers(); for(let member of members) { member.GetName(); groupCount += 1; } } return groupCount; }const CLASS_WEAPON = 2; const CLASS_ARMOR = 4; const WEAPON_TYPES = { 0: "Axe", 1: "2H Axe", 2: "Bow", 3: "Gun", 4: "Mace", 5: "2H Mace", 6: "Polearm", 7: "Sword", 8: "2H Sword", 10: "Staff", 13: "Fist Weapon", 15: "Dagger", 16: "Thrown", 17: "Spear", 18: "Crossbow", 19: "Wand", }; const ARMOR_TYPES = { 0: "Misc", 1: "Cloth", 2: "Leather", 3: "Mail", 4: "Plate", 6: "Shield", }; type ArmorType = "Range" | "Melee" | "Caster" | "Tank" ; export class ItemDetails { item: Item; stats: Record; constructor(item: Item) { this.item = item; this.stats = {}; } IsWeapon(): boolean { return this.item.GetClass() === CLASS_WEAPON; } IsArmor(): boolean { return this.item.GetClass() === CLASS_ARMOR; } GetWeaponType(): string | false { if (!this.IsWeapon()) { return false; } if(this.HasRangedStats()) { return "Ranged"; } if(this.HasCasterStats()) { return "Caster"; } if(this.HasMeleeStats()) { return "Melee"; } if(this.HasDefensiveStats()) { return "Tank"; } const stats = this.GetStats(); if (stats.Description) { const desc: string = stats.Description; if (desc.includes("attack power") || desc.includes("critical strike rating") || desc.includes("hit rating") || desc.includes("melee haste rating") || desc.includes("armor penetration rating")) { return "Melee"; } if (desc.includes("defense rating") || desc.includes("block") || desc.includes("parry") ) { return "Tank"; } if (desc.includes("spell power") || desc.includes("spell critical strike rating") || desc.includes("spell hit rating") || desc.includes("spell haste rating") || desc.includes("spell penetration")) { return "Caster"; } if (desc.includes("ranged attack power") || desc.includes("ranged critical strike rating") || desc.includes("ranged hit rating") || desc.includes("ranged haste rating") || desc.includes("ranged weapon penetration")) { return "Range"; } } // still nothing? Then just do the best guess based on what the hell it is. if(this.IsMeleeWeapon()) { return "Melee"; } if(this.IsRangedWeapon()) { return "Range"; } if(this.IsCasterWeapon()) { return "Caster"; } if(this.IsArmor() && ARMOR_TYPES[this.item.GetSubClass()] === "Plate") { return "Tank"; } if(this.IsArmor() && ARMOR_TYPES[this.item.GetSubClass()] === "Mail") { return "Ranged"; } if(this.IsArmor() && ARMOR_TYPES[this.item.GetSubClass()] === "Leather") { return "Melee"; } if(this.IsArmor() && ARMOR_TYPES[this.item.GetSubClass()] === "Cloth") { return "Caster"; } if(this.item.GetSubClass() === 6) { return "Tank"; } return false; } GetArmorType(): ArmorType | false { if(!this.IsArmor()) { return false; } if(this.IsRangedArmor()) { return "Range"; } if(this.IsCasterArmor()) { return "Caster"; } if(this.IsTankArmor()) { return "Tank"; } if(this.IsMeleeArmor()) { return "Melee"; } return false } IsMeleeWeapon(): boolean { if (!this.IsWeapon()) { return false; } const subClass = this.item.GetSubClass(); switch (WEAPON_TYPES[subClass]) { case "Axe": case "2H Axe": case "Mace": case "2H Mace": case "Polearm": case "Sword": case "2H Sword": case "Fist Weapon": case "Dagger": case "Spear": if (this.HasCasterStats()) { return false; } return true; default: return false; } } IsCasterWeapon() { if (!this.IsWeapon()) { return false; } switch (WEAPON_TYPES[this.item.GetSubClass()]) { case "Mace": case "2H Mace": case "Sword": case "Dagger": case "Wand": case "Staff": if (this.HasCasterStats()) { return true; } return false; default: return false; } } IsRangedWeapon() { if (!this.IsWeapon()) { return false; } switch (WEAPON_TYPES[this.item.GetSubClass()]) { case "Bow": case "Gun": case "Thrown": case "Crossbow": return true; default: return false; } } Is2HWeapon() { if (!this.IsWeapon()) { return false; } switch (WEAPON_TYPES[this.item.GetSubClass()]) { case "2H Axe": case "2H Mace": case "Polearm": case "2H Sword": case "Staff": return true; default: return false; } } IsCasterArmor(): boolean { if (!this.IsArmor()) { return false; } const type = ARMOR_TYPES[this.item.GetSubClass()]; if (type === "Cloth") { return true; } if (this.HasCasterStats()) { return true; } return false; } IsMeleeArmor(): boolean { if (!this.IsArmor()) { return false; } const type = ARMOR_TYPES[this.item.GetSubClass()]; if (type === "Cloth") { return false; } if(this.HasMeleeStats()) { return true; } return false; } IsRangedArmor(): boolean { if (!this.IsArmor()) { return false; } const type = ARMOR_TYPES[this.item.GetSubClass()]; if (type === "Cloth" || type === "Plate") { return false; } if(this.HasRangedStats()) { return true; } return false; } IsTankArmor(): boolean { if (!this.IsArmor()) { return false; } const type = ARMOR_TYPES[this.item.GetSubClass()]; if (type === "Cloth" || type === "Mail") { return false; } if(this.HasDefensiveStats()) { return true; } return false; } HasCasterStats(): boolean { const stats = this.GetStats(); if (Object.keys(stats).length === 0) { return false; } const casterStats = [5, 6, 18, 21, 27, 30, 41, 42, 43, 45, 47]; for (let i = 1; i <= 8; i++) { let statType = stats[`stat_type${i}`] ? stats[`stat_type${i}`] : 0; if (casterStats.includes(statType)) { return true; } } return false; } HasDefensiveStats(): boolean { const stats = this.GetStats(); if (Object.keys(stats).length === 0) { return false; } const defStats = [1,12, 13, 14, 15, 22, 23, 24, 25, 26, 27, 48]; for (let i = 1; i <= 8; i++) { let statType = stats[`stat_type${i}`] ? stats[`stat_type${i}`] : 0; if (defStats.includes(statType)) { return true; } } return false; } HasMeleeStats(): boolean { const stats = this.GetStats(); if (Object.keys(stats).length === 0) { return false; } const meleeStats = [3, 16,19,28,31,32,36,37,38,44] for (let i = 1; i <= 8; i++) { let statType = stats[`stat_type${i}`] ? stats[`stat_type${i}`] : 0; if (meleeStats.includes(statType)) { return true; } } return false; } HasRangedStats(): boolean { const stats = this.GetStats(); if (Object.keys(stats).length === 0) { return false; } const rangedStats = [17,20,29,39]; for (let i = 1; i <= 8; i++) { let statType = stats[`stat_type${i}`] ? stats[`stat_type${i}`] : 0; if (rangedStats.includes(statType)) { return true; } } } GetStats(): Record { // return stats if we have already parsed them. if (Object.keys(this.stats).length != 0) { return this.stats; } const entry = this.item.GetEntry(); const sql = `SELECT stat_type1, stat_value1, stat_type2, stat_value2, stat_type3, stat_value3, stat_type4, stat_value4, stat_type5, stat_value5, stat_type6, stat_value6, stat_type7, stat_value7, stat_type8, stat_value8, spell_dbc.Description_Lang_enUS as Description FROM item_template LEFT JOIN spell_dbc ON item_template.spellid_1 = spell_dbc.ID WHERE entry = ${entry}`; const query = WorldDBQuery(sql); // print(`GetStats: ${sql}`); if(query) { return query.GetRow(); } else { PrintError("ItemDetails/GetStats: Failed to get ITem Stats: ", sql); return {}; } } } // Purpose: Logger class to log messages to the console. export class Logger { public logname: string; constructor(name: string) { this.logname = name; } log(message: string) { const info = debug.getinfo(2, "Sl"); print(`[${this.logname}][Log]: ${message} was printed from ${info.short_src}:${info.currentline}`); } debug(message: string) { const info = debug.getinfo(2, "Sl"); PrintDebug(`[${this.logname}][Debug]: ${message} was printed from ${info.short_src}:${info.currentline}`); } info(message: string) { const info = debug.getinfo(2, "Sl"); PrintInfo(`[${this.logname}][Info]: ${message} was printed from ${info.short_src}:${info.currentline}`); } warn(message: string) { const info = debug.getinfo(2, "Sl"); print(`\\27[33m[${this.logname}][Warn]: ${message} was printed from ${info.short_src}:${info.currentline}\\27[0m`); } error(message: string) { const info = debug.getinfo(2, "Sl"); PrintError(`[${this.logname}][Error]: ${message} was printed from ${info.short_src}:${info.currentline}`); } }import { AccountInfo } from "./account"; export const GOLD_TO_COPPER = 10000; /** * Converts a copper cost to gold * @param cost Cost of item in copper * @returns number */ export function ToGold(cost: number) : number { return Math.floor(cost / GOLD_TO_COPPER); } /** * Converts a gold cost to copper * @param gold Cost of item in gold * @returns number */ export function ToCopper(gold: number) : number { return gold*GOLD_TO_COPPER; } /** * Gets a scaling tax for players to help with balancing the economy for guild features. * @param player Player * @param tax amount of tax against player to levy number (0-100) * @returns number result in copper */ export function GetPlayerTax(player: Player, tax: number) : number { const account = new AccountInfo(player.GetAccountId()); return (tax/100) * account.GetAccountMoney(); } // A function that will take a min and a max and return a random number between them export function rollDice(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); }declare function GetGameTime(): number; const PLAYER_TYPE = 'player'; export const StatEvents = { TOKEN_CREATED: 'token_created', TICKETS_AWARDED: 'darkmoon_tickets_awarded', }; export class Stats { stats = new Map(); entity: StatEntity; constructor(entity: StatEntity) { this.entity = entity; this.load(); } static GetStatsByType(type: string, name: string) : Map { const result = CharDBQuery(`SELECT id, name, value, updated FROM ${type}_stats WHERE name = '${name}'`); const stats = new Map(); if(!result) { return stats; } for(let i=0; i < result.GetRowCount(); i++) { const row = result.GetRow(); stats.set(row.id as number, row.value as number); result.NextRow(); } return stats; }; load() : boolean { const result = CharDBQuery(`SELECT id, name, value, updated FROM ${this.entity.type}_stats WHERE id = ${this.entity.id}`); if(!result) { return false; } for(let i=0; i < result.GetRowCount(); i++) { const row = result.GetRow(); const stat: Stat = { name: row.name as string, type: this.entity.type, value: row.value as number, updated: row.updated as number, loaded: true } this.stats.set(stat.name, stat); result.NextRow(); } return true; } save() : void { for(const stat of this.stats.values()) { if(!stat.loaded) { CharDBExecute(`INSERT INTO ${this.entity.type}_stats (id, name, value, updated) VALUES (${this.entity.id}, '${stat.name}', ${stat.value}, ${stat.updated})`); PrintDebug(`Inserted ${stat.name} for ${this.entity.type} ${this.entity.id} with value ${stat.value}`); } else { CharDBExecute(`UPDATE ${this.entity.type}_stats SET value = ${stat.value}, updated = ${stat.updated} WHERE id = ${this.entity.id} AND name = '${stat.name}'`); PrintDebug(`Updated ${stat.name} for ${this.entity.type} ${this.entity.id} to ${stat.value}`); } } } getStat(name: string) : Stat | undefined { return this.stats.get(name); } setStat(name: string, value: number) : void { const stat = this.stats.get(name); if(stat) { stat.value = value; stat.updated = GetGameTime(); } else { this.stats.set(name, { name: name, type: PLAYER_TYPE, value: value, updated: GetGameTime(), loaded: false }); } } increment(name: string, amount: number = 1) : void { const stat = this.stats.get(name); if(stat) { stat.value += amount; stat.updated = GetGameTime(); } else { this.stats.set(name, { name: name, type: PLAYER_TYPE, value: 0, updated: GetGameTime(), loaded: false }); } } } /** * Custom player stats that will be */ export class PlayerStats extends Stats { player: Player; playerStats: Stat[] = []; constructor(player: Player) { super({ id: player.GetGUID(), type: PLAYER_TYPE }); this.player = player; } } interface StatEntity { type: string, id: number } interface Stat { type: string, name: string, value: number, updated: number, loaded: boolean } /* @noSelfInFile */ type TriggerInput = { triggerName: string, characterGuid: number, isSet: boolean } /** * Sets a player trigger boolean that can be retieved later as needed * @param charTrigger TriggerInput */ export function SetTrigger(charTrigger: TriggerInput) { let sql = `INSERT INTO player_trigger (triggerName, characterGuid, isSet) `+ `VALUES ("${charTrigger.triggerName}", ${charTrigger.characterGuid}, ${charTrigger.isSet})`+ `ON DUPLICATE KEY UPDATE isSet=${charTrigger.isSet}`; print(sql); CharDBExecute(sql); } /** * Will return the value of the trigger if it exists, otherwise it will return false * @param charGuid number * @param triggerName string * @returns boolean */ export function GetTrigger(charGuid: number, triggerName: string) { let sql = `SELECT isSet from player_trigger WHERE triggerName="${triggerName}" and characterGuid=${charGuid}`; const result = CharDBQuery(sql); if(result && result.GetRowCount() > 0) { return result.GetBool(0) } else { return false; } } export function colors(name: string) { const colors = { GREY: "|cff999999", RED: "|cffff0000", WHITE: "|cffFFFFFF", GREEN: "|cff1eff00", PURPLE: "|cff9F3FFF", BLUE: "|cff0070dd", ORANGE: "|cffFF8400", YELLOW: "|cffFFFF00", }; const keyName = name.toUpperCase(); if(colors[keyName]) { return colors[keyName]; } else { return colors.WHITE; } }export const BotEquipSlot = { MAINHAND: 0, OFFHAND: 1, RANGED: 2, HEAD: 3, SHOULDERS: 4, CHEST: 5, WAIST: 6, LEGS: 7, FEET: 8, WRIST: 9, HANDS: 10, BACK: 11, BODY: 12, FINGER1: 13, FINGER2: 14, TRINKET1: 15, TRINKET2: 16, NECK: 17, } as const; export const BotSlotName = { 0: "MAINHAND", 1: "OFFHAND", 2: "RANGED", 3: "HEAD", 4: "SHOULDER", 5: "CHEST", 6: "WAIST", 7: "LEGS", 8: "FEET", 9: "WRIST", 10: "HANDS", 11: "BACK", 12: "BODY", 13: "FINGER1", 14: "FINGER2", 15: "TRINKET1", 16: "TRINKET2", 17: "NECK", } as const; export const BotEquipLast = 17; export const BotStatLabel = { "total str": "Strength", "total agi": "Agility", "total sta": "Stamina", "total int": "Intellect", "total spi": "Spirit", "Melee AP": "Power", "Ranged AP": "Power", "armor": "Armor", "crit": "Crit %", "defense": "Defense", "miss": "Miss", "dodge": "Dodge", "parry": "Parry", "block": "Block", "block value": "Block Value", "Damage taken melee": "Physical Res.", "Damage taken spell": "Spell Res.", "Damage range mainhand": "Damage", "Damage range offhand": "Dmg Off", "Attack time offhand": "Speed Off", "Damage mult mainhand": "Damage Multiplier (Mainhand)", "Attack time mainhand": "Speed Main", "Damage range ranged": "Damage Rng", "Damage mult ranged": "Damage Multiplier (Ranged)", "Attack time ranged": "Speed", "base hp": "Base Health", "total hp": "Total Health", "base mana": "Base Mana", "total mana": "Total Mana", "spell power": "Bonus Dmg", "health regen_5 bonus": "Health Regen (5s Bonus)", "haste": "Haste Rating", "hit": "Hit Rating", "expertise": "Expertise", "mana regen_5 casting": "MP5", "armor penetration": "Armor Pen", "spell penetration": "Spell Pen", "Resistance: holy": "Resist Holy", "Resistance: fire": "Resist Fire", "Resistance: nature": "Resist Nature", "Resistance: frost": "Resist Frost", "Resistance: shadow": "Resist Shadow", "Resistance: arcane": "Resist Arcane", } as const; export type BotStatName = Partial; export const BotStat = { MANA: 0, HEALTH: 1, AGILITY: 3, STRENGTH: 4, INTELLECT: 5, SPIRIT: 6, STAMINA: 7, DEFENSE_SKILL_RATING: 12, DODGE_RATING: 13, PARRY_RATING: 14, BLOCK_RATING: 15, HIT_MELEE_RATING: 16, HIT_RANGED_RATING: 17, HIT_SPELL_RATING: 18, CRIT_MELEE_RATING: 19, CRIT_RANGED_RATING: 20, CRIT_SPELL_RATING: 21, HIT_TAKEN_MELEE_RATING: 22, HIT_TAKEN_RANGED_RATING: 23, HIT_TAKEN_SPELL_RATING: 24, CRIT_TAKEN_MELEE_RATING: 25, CRIT_TAKEN_RANGED_RATING: 26, CRIT_TAKEN_SPELL_RATING: 27, HASTE_MELEE_RATING: 28, HASTE_RANGED_RATING: 29, HASTE_SPELL_RATING: 30, HIT_RATING: 31, CRIT_RATING: 32, HIT_TAKEN_RATING: 33, CRIT_TAKEN_RATING: 34, RESILIENCE_RATING: 35, HASTE_RATING: 36, EXPERTISE_RATING: 37, ATTACK_POWER: 38, RANGED_ATTACK_POWER: 39, FERAL_ATTACK_POWER: 40, SPELL_HEALING_DONE: 41, SPELL_DAMAGE_DONE: 42, MANA_REGENERATION: 43, ARMOR_PENETRATION_RATING: 44, SPELL_POWER: 45, HEALTH_REGEN: 46, SPELL_PENETRATION: 47, BLOCK_VALUE: 48, DAMAGE_MIN: 49, DAMAGE_MAX: 50, ARMOR: 51, RESIST_HOLY: 52, RESIST_FIRE: 53, RESIST_NATURE: 54, RESIST_FROST: 55, RESIST_SHADOW: 56, RESIST_ARCANE: 57, EXPERTISE: 58, MAX_BOT_ITEM_MOD: 59, BOT_STAT_MOD_RESISTANCE_START: 51, // Assuming BOT_STAT_MOD_ARMOR is defined somewhere } as const; export const BotStatLast = 58; export const UIInvSlot = { AMMOSLOT: "AMMOSLOT", HEADSLOT: "HEADSLOT", NECKSLOT: "NECKSLOT", SHOULDERSLOT: "SHOULDERSLOT", SHIRTSLOT: "SHIRTSLOT", CHESTSLOT: "CHESTSLOT", WAISTSLOT: "WAISTSLOT", LEGSSLOT: "LEGSSLOT", FEETSLOT: "FEETSLOT", WRISTSLOT: "WRISTSLOT", HANDSSLOT: "HANDSSLOT", FINGER0SLOT: "FINGER0SLOT", FINGER1SLOT: "FINGER1SLOT", TRINKET0SLOT: "TRINKET0SLOT", TRINKET1SLOT: "TRINKET1SLOT", BACKSLOT: "BACKSLOT", MAINHANDSLOT: "MAINHANDSLOT", SECONDARYHANDSLOT: "SECONDARYHANDSLOT", RANGEDSLOT: "RANGEDSLOT", TABARDSLOT: "TABARDSLOT", } as const; export const ClassesMapping: Record = { 1: "Warrior", 2: "Paladin", 3: "Hunter", 4: "Rogue", 5: "Priest", 6: "Death Knight", 7: "Shaman", 8: "Mage", 9: "Warlock", 10: "Druid", 11: "Blade Master", 12: "Sphynx", 13: "Archmage", 14: "Dreadlord", 15: "Spellbreaker", 16: "Dark Ranger", 17: "Necromancer", 18: "Sea Witch", 19: "Crypt Lord", } as const; export type CharacterClass = typeof ClassesMapping[keyof typeof ClassesMapping]; export const RacesMapping: Record = { 1: "Human", 2: "Orc", 3: "Dwarf", 4: "Night Elf", 5: "Undead", 6: "Tauren", 7: "Gnome", 8: "Troll", 9: "Goblin", 10: "Blood Elf", 11: "Draenei", 12: "Worgen", } as const; export type CharacterRace = typeof RacesMapping[keyof typeof RacesMapping]; export const TalentSpecs = { WARRIOR_ARMS : 1, WARRIOR_FURY : 2, WARRIOR_PROTECTION : 3, PALADIN_HOLY : 4, PALADIN_PROTECTION : 5, PALADIN_RETRIBUTION : 6, HUNTER_BEASTMASTERY : 7, HUNTER_MARKSMANSHIP : 8, HUNTER_SURVIVAL : 9, ROGUE_ASSASSINATION : 10, ROGUE_COMBAT : 11, ROGUE_SUBTLETY : 12, PRIEST_DISCIPLINE : 13, PRIEST_HOLY : 14, PRIEST_SHADOW : 15, DK_BLOOD : 16, DK_FROST : 17, DK_UNHOLY : 18, SHAMAN_ELEMENTAL : 19, SHAMAN_ENHANCEMENT : 20, SHAMAN_RESTORATION : 21, MAGE_ARCANE : 22, MAGE_FIRE : 23, MAGE_FROST : 24, WARLOCK_AFFLICTION : 25, WARLOCK_DEMONOLOGY : 26, WARLOCK_DESTRUCTION : 27, DRUID_BALANCE : 28, DRUID_FERAL : 29, DRUID_RESTORATION : 30, DEFAULT : 31, BEGIN : 1, END : 31 } as const; export const BotRoles = { NONE : 0, TANK : 1, TANK_OFF : 2, DPS : 4, HEAL : 8, RANGED : 16, PARTY : 32, // hidden GATHERING_MINING : 64, GATHERING_HERBALISM : 128, GATHERING_SKINNING : 256, GATHERING_ENGINEERING : 512, AUTOLOOT : 1024, AUTOLOOT_POOR : 2048, AUTOLOOT_COMMON : 4096, AUTOLOOT_UNCOMMON : 8192, AUTOLOOT_RARE : 16384, AUTOLOOT_EPIC : 32768, AUTOLOOT_LEGENDARY : 65536, // MASK_MAIN : (1 | 2 | 4 | 8 | 16), // MASK_GATHERING : (64 | 128 | 256 | 512), // MASK_LOOTING : (2048 | 4096 | 8192 | 16384 | 32768 | 65536), // BOT_MAX_ROLE : 131072, } as const; /**************** ITEM CONSTANTS *************************/ export const ItemQuality = { Poor: 0, Common: 1, Uncommon: 2, Rare: 3, Epic: 4, Legendary: 5, Artifact: 6, Heirlooms: 7, } as const; export type QualityType = typeof ItemQuality[keyof typeof ItemQuality]; export const ItemStat = { MANA: 0, HEALTH: 1, AGILITY: 3, STRENGTH: 4, INTELLECT: 5, SPIRIT: 6, STAMINA: 7, DEFENSE_SKILL_RATING: 12, DODGE_RATING: 13, PARRY_RATING: 14, BLOCK_RATING: 15, HIT_MELEE_RATING: 16, HIT_RANGED_RATING: 17, HIT_SPELL_RATING: 18, CRIT_MELEE_RATING: 19, CRIT_RANGED_RATING: 20, CRIT_SPELL_RATING: 21, HIT_TAKEN_MELEE_RATING: 22, HIT_TAKEN_RANGED_RATING: 23, HIT_TAKEN_SPELL_RATING: 24, CRIT_TAKEN_MELEE_RATING: 25, CRIT_TAKEN_RANGED_RATING: 26, CRIT_TAKEN_SPELL_RATING: 27, HASTE_MELEE_RATING: 28, HASTE_RANGED_RATING: 29, HASTE_SPELL_RATING: 30, HIT_RATING: 31, CRIT_RATING: 32, HIT_TAKEN_RATING: 33, CRIT_TAKEN_RATING: 34, RESILIENCE_RATING: 35, HASTE_RATING: 36, EXPERTISE_RATING: 37, ATTACK_POWER: 38, RANGED_ATTACK_POWER: 39, FERAL_ATTACK_POWER: 40, // Note: This is not used as of 3.3 SPELL_HEALING_DONE: 41, SPELL_DAMAGE_DONE: 42, MANA_REGENERATION: 43, ARMOR_PENETRATION_RATING: 44, SPELL_POWER: 45, HEALTH_REGEN: 46, SPELL_PENETRATION: 47, BLOCK_VALUE: 48, } as const; export const DamageType = { Physical: 0, Holy: 1, Fire: 2, Nature: 3, Frost: 4, Shadow: 5, Arcane: 6, } as const; export const SocketColor = { Meta: 1, Red: 2, Yellow: 4, Blue: 8, } as const; export const SocketBonus = { 3312: '+8 Strength', 3313: '+8 Agility', 3305: '+12 Stamina', 3: '+8 Intellect', 2872: '+9 Healing', 3753: '+9 Spell Power', 3877: '+16 Attack Power', } as const;