From e7ffbf98b9a7c071e095592a52b404c71aa75e45 Mon Sep 17 00:00:00 2001 From: Ben Carter Date: Mon, 5 Feb 2024 23:48:05 -0500 Subject: [PATCH] Added first changes of bot equipment manager with working inventory show. --- modules/UI/botmgr/bot.ts | 46 +++ modules/UI/botmgr/botmgr.client.ts | 508 +++++++++++++++++++++++++++++ modules/UI/botmgr/botmgr.server.ts | 193 +++++++++++ modules/classes/logger.ts | 31 ++ modules/constants/idmaps.ts | 168 ++++++++++ 5 files changed, 946 insertions(+) create mode 100644 modules/UI/botmgr/bot.ts create mode 100644 modules/UI/botmgr/botmgr.client.ts create mode 100644 modules/UI/botmgr/botmgr.server.ts create mode 100644 modules/classes/logger.ts create mode 100644 modules/constants/idmaps.ts diff --git a/modules/UI/botmgr/bot.ts b/modules/UI/botmgr/bot.ts new file mode 100644 index 0000000..8811270 --- /dev/null +++ b/modules/UI/botmgr/bot.ts @@ -0,0 +1,46 @@ +import { BotData } from './botmgr.server'; + +export class BotStorage { + + private storage: Map = new Map(); + private active: number = null; + + public GetBotData(entry: number): BotData | undefined { + return this.storage.get(entry); + } + + GetBotItem(botId: number, slot: BotEquipmentSlotNum): number | undefined { + const bot = this.GetBotData(botId); + if(bot) { + return bot.equipment[slot]; + } + } + + SetBotData(entry: number, data: BotData): void { + + } + + SetBotItem(botId: number, slot: BotEquipmentSlotNum, item: number): void { + const bot = this.GetBotData(botId); + if(bot) { + bot.equipment[slot] = item; + } + } + + 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; + } + +} \ No newline at end of file diff --git a/modules/UI/botmgr/botmgr.client.ts b/modules/UI/botmgr/botmgr.client.ts new file mode 100644 index 0000000..9c98685 --- /dev/null +++ b/modules/UI/botmgr/botmgr.client.ts @@ -0,0 +1,508 @@ + +/** @noSelfInFile **/ +/** @ts-expect-error */ +let aio: AIO = {}; + + +/** + * @todo Add all equipment slots for the bot onload - done + * @todo Create frames for bot status + * @todo Complete resist information frame. + * @todo Update server message to send class name, bot race - done + * @todo Add hover events to handle equipment change on bot. + * @todo Add compare tooltip for bot equipment - sort of done (shows equipment next to each other) + * + * v2: + * @todo Add slot management for bot equipment + * @todo Add bot spec under bot info + * @todo Add talent spec to bot profile + */ + + +import { UIInvSlot, BotEquipSlot, BotSlotName } from "../../constants/idmaps"; +import { BotData } from "./botmgr.server"; +import { BotStorage } from "./bot"; + + + +const id = (name: string, entry: number = null) => `BotMgr${name}` + (entry ? entry : ''); +const compId = (botId: number, name: string) => `${botId}:BotMgr${name}`; + +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; +} + + +// 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 botStorage: BotStorage = new BotStorage(); + + + let BotItemTooltip: WoWAPI.GameTooltip; + + /** + * All Resists for the bot + * Is a SubFrame as well as children. + * @param parent + * @param resists + */ + function AddResistFrame(parent: WoWAPI.Frame, resists: any = {}) { + 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", 6); + magicRes1.SetPoint("TOP", resistFrame, "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("MagicResFont1"), "BACKGROUND", "GameFontHighlightSmall"); + magResFont1.SetPoint("BOTTOM", magicRes1, null, 0, 3); + magResFont1.SetText("X"); + } + + /** + * This is for the Characters left picture and class,race, name details. + * Only created once. + */ + 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}`); + } + + 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, 160); + frameChar.SetUnit("target"); + frameChar.SetFacing(0.3); + 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 equippedItemId = 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(equippedItemId && equippedItemId > 0) { + itemIcon = GetItemIcon(equippedItemId); + idsuffix = slotOrder[i]; + } + + // If there is not a piece of equipment add the background texture + if(!equippedItemId && 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); + } + } + + + 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); + ComponentsPool.set(compId(botData.entry, "RightEquipment"), equipFrame); + UpdateEquipFrame('right', equipFrame, botData); + + } + + if(!frames.weapons) { + equipFrame = CreateFrame("Frame", id("WeaponEquipment"), parent, null, 3); + equipFrame.SetPoint("CENTER", -10, -147); + equipFrame.SetSize(129, 40); + ComponentsPool.set(compId(botData.entry, "WeaponsEquipment"), equipFrame); + UpdateEquipFrame('weapons', equipFrame, botData); + // const placeholder = equipFrame.CreateTexture(id("RightEquipmentPlaceholder"), "OVERLAY"); + // placeholder.SetAllPoints(equipFrame); + // placeholder.SetTexture(0,0,0,0.8); + } + + + } + + 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"); + }); + } + + function ItemSlotOnEnter(frame: WoWAPI.Button) { + const botId = botStorage.GetActive(); + const itemId = botStorage.GetBotItem(botId, frame.GetID()); + GameTooltip.SetOwner(frame, "ANCHOR_RIGHT"); + if(itemId) { + GameTooltip.SetHyperlink(`item:${itemId}:0:0:0:0:0:0:0`); + } 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 itemId = botStorage.GetBotItem(botId, frame.GetID()); + + const [compItem, compItemId, compItemLink] = GetCursorInfo(); + print(`CursorHasItem: ${compItemLink}`); + + if(itemId && !compItem) { + if(button == "LeftButton") { + PickupItem(itemId); + return; + } + //botStorage.EquipBotItem(botId, frame.GetID(), itemId); + } + + if(compItem) { + PlaySound("INTERFACESOUND_CURSORDROPOBJECT"); + ClearCursor(); + } + + + // else { + // if(CursorHasItem()) { + // const [compItem, compItemId, compItemLink] = GetCursorInfo(); + // botStorage.EquipBotItem(botId, frame.GetID(), compItemId); + // } + // } + } + + /** + * 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); + + if(mainFrame) { print('main frame already created'); } + // 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); + }); + + // mainFrame.Hide(); + + 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); + AddEquipmentFrames(mainFrame, botData); + AddSoundEffects(mainFrame); + + InfoFramePool.set(botData.entry, mainFrame); + ComponentsPool.set(compId(botData.entry, "tooltip"), BotItemTooltip); + + // const button2 = CreateFrame("Button", id("CharacterNeckSlot"), mainFrame, "ItemButtonTemplate"); + // button2.SetPoint("TOP", button, "BOTTOM", 0, -3); + // button2.SetSize(40,40); + + // const [itemId, texture] = GetInventorySlotInfo(UIInvSlot.NECKSLOT); + + // const itemTexture2 = button2.CreateTexture(id("ItemTextureNeck"), "OVERLAY"); + // itemTexture2.SetTexture(texture); + // itemTexture2.SetPoint("CENTER", 0, 0); + // itemTexture2.SetSize(38,38); + + + + // const leftEquipment = CreateFrame("Frame", id("LeftEquipment"), mainFrame); + // leftEquipment.SetPoint("TOPLEFT", 20, -73); + // leftEquipment.SetSize(40, 330); + + // const background = leftEquipment.CreateTexture(id("Background"), "OVERLAY"); + // background.SetTexture(0,0,0,0.7); + // background.SetAllPoints(leftEquipment); + + // const button = CreateFrame("Button", id("CharacterHeadSlot"), leftEquipment, "ItemButtonTemplate"); + // button.SetPoint("TOPLEFT", 0, 0); + // button.SetSize(40,40); + + // const itemTexture = button.CreateTexture(id("ItemTextureHead"), "OVERLAY"); + // itemTexture.SetTexture(GetItemIcon(botData.equipment[BotEquipSlot.HEAD])); + // itemTexture.SetPoint("CENTER", 0, 0); + // itemTexture.SetSize(36,36); + + } + + mainFrame.Show(); + + + // aio.Handle("BotMgr", "ParseBotEntry", UnitGUID("target")); + // aio.Handle("BotMgr", "ShowComplexArray", { "one": 1, "two": 2, "three": 3 }); + + + // const headTex = GetItemIcon(botdetails.equipment[BotEquipSlot.HEAD]); + // print(headTex); + + + + + // const testTexture = itemFrame.CreateTexture(id("TestTexture"), "OVERLAY"); + // testTexture.SetTexture(GetItemIcon(2194)); + // // testTexture.SetSize(64,64); + // testTexture.SetAllPoints(button); + // testTexture.SetPoint("TOPLEFT", 0, 0); + + + // SetItemButtonTexture(button, texture); + + // button.SetScript("OnLoad", (frame) => { + // const head = frame.CreateTexture("CharacterHeadSlotTexture", "ARTWORK"); + // head.SetAllPoints(button); + // head.SetTexture(1,0,0,1); + // }); + + + + + //button.SetName("CharacterHeadSlot"); + + + + + + + + // const frameCore = CreateFrame("Frame", "BotMgrCoreFrame", mainFrame); + // frameCore.SetSize(600, 440); + // frameCore.SetPoint("TOPLEFT", 0, 0); + + // const mainTexture = frameCore.CreateTexture("BotMgrMainFrameTexture", "BACKGROUND"); + // mainTexture.SetAllPoints(frameCore); + + // const titleText = frameCore.CreateFontString("BotMgrTitle", "ARTWORK"); + // titleText.SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE"); + // titleText.SetPoint("TOP", 0, -5); + // titleText.SetText(GetUnitName("target", false)); + + // const unitTexture = frameChar.CreateTexture("BotMgrCharTexture", "BACKGROUND"); + // unitTexture.SetTexture(0, 0, 0); + // unitTexture.SetAllPoints(frameChar); + + } + + botMgrHandlers.ShowFrame = (botData: BotData) => { + + // Update the botData manager for this Bot on panel show. + botStorage.UpdateBotData(botData.entry, botData); + ShowBotFrame(botData); + } +} + diff --git a/modules/UI/botmgr/botmgr.server.ts b/modules/UI/botmgr/botmgr.server.ts new file mode 100644 index 0000000..510b074 --- /dev/null +++ b/modules/UI/botmgr/botmgr.server.ts @@ -0,0 +1,193 @@ + /** @ts-expect-error */ +let aio: AIO = {}; + +const SCRIPT_NAME = 'BotMgr'; +import { Logger } from "../../classes/logger"; +const log = new Logger(SCRIPT_NAME); + +import { + BotStat, + BotEquipSlot, + BotSlotName, + BotEquipLast, + BotStatLast, + ClassesMapping, + CharacterClass, + RacesMapping, + CharacterRace + } from "../../constants/idmaps"; + + + + /** + * Everything we ever wanted to know about the bot info on load + */ + export type BotData = { + owner: string, + name: string, + entry: number, + class: CharacterClass, + classId: number, + race: CharacterRace, + raceId: number, + equipment?: Record, // SlotName - ItemId See BotEquipSlot + stats?: Record, // StatId - Value +}; + +/** + * @todo Move to a data mgr class eventually + */ +const NpcDetailStorage = {} as Record; + +// // cheap way to load everything on the server side and force so client as access. (Don't love this at all will need to work back into plugin); +// const loader = BotStat || BotEquipSlot || BotEquipLast || BotStatLast || ClassesMapping || RacesMapping; + +/** + * 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; +} + +/** + * @noSelf + */ +function GetBotDetails(bot: Creature): BotData { + + const owner = bot.GetBotOwner(); + + // We can use bot entrys since they are 1:1 with GUIDs for shorter storage keys + NpcDetailStorage[bot.GetEntry()] = { + owner: owner.GetName(), + name: bot.GetName(), + entry: bot.GetEntry(), + class: ClassesMapping[bot.GetBotClass()], + classId: bot.GetBotClass(), + race: RacesMapping[bot.GetRace()], + raceId: bot.GetRace(), + equipment: {}, + // stats: {}, + }; + + for(let slot=0; slot <= BotEquipLast; slot++) { + const equipment = bot.GetBotEquipment(slot); + + if(equipment) { + NpcDetailStorage[bot.GetEntry()].equipment[slot] = equipment.GetEntry(); + print(`Slot: ${BotSlotName[slot]} Item: ${equipment.GetEntry()}`); + } else { + NpcDetailStorage[bot.GetEntry()].equipment[slot] = undefined; + } + } + + return NpcDetailStorage[bot.GetEntry()]; + +} + +const ShowBotMgr: player_event_on_command = (event: number,player: Player, command: string): boolean => { + if(command == 'botmgr') { + + if(TargetIsEligible(player)) { + const botdetails = GetBotDetails(GetBotNpc(player)); + + // loop through bot details and print the key value pair + for (const [key, value] of Object.entries(botdetails.equipment)) { + print(`${key}: ${value}`); + } + + 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}`); + } +} + +function ShowComplexArray(items: Record) { + + try { + for (const [key, value] of Object.entries(items)) { + print(`${key}: ${value}`); + } + } catch (e) { + PrintError(`BotMgr: Error showing complex array: ${e}`); + } + +} + +const botMgrHandlers = aio.AddHandlers('BotMgr', { + TargetIsEligible, + GetBotPanelInfo +}); + +RegisterPlayerEvent( + PlayerEvents.PLAYER_EVENT_ON_COMMAND, + (...args) => ShowBotMgr(...args) +); diff --git a/modules/classes/logger.ts b/modules/classes/logger.ts new file mode 100644 index 0000000..67e93b7 --- /dev/null +++ b/modules/classes/logger.ts @@ -0,0 +1,31 @@ +// 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}`); + } + + 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}`); + } + +} \ No newline at end of file diff --git a/modules/constants/idmaps.ts b/modules/constants/idmaps.ts new file mode 100644 index 0000000..e10b3f6 --- /dev/null +++ b/modules/constants/idmaps.ts @@ -0,0 +1,168 @@ +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 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]; \ No newline at end of file