From 325bf44fc93adf1cfabdabbd870d8fc49f85cf9f Mon Sep 17 00:00:00 2001 From: Ben Carter Date: Tue, 6 May 2025 19:13:38 -0400 Subject: [PATCH] mythic changes in development coded new npc --- modules/UI/mythicplus/advancement_state.ts | 35 ++ .../mythicplus/mythic_advancement.client.ts | 591 ++++++++++++++++++ .../mythicplus/mythic_advancement.server.ts | 223 +++++++ modules/UI/mythicplus/mythic_custom_spells.ts | 356 +++++++++++ modules/UI/mythicplus/mythic_items.ts | 172 +++++ modules/UI/mythicplus/mythic_npcs.ts | 444 +++++++++++++ modules/classes/logger.ts | 1 + modules/classes/mapzones.ts | 536 ++++++++++++++++ modules/npcs/soulswapper.ts | 2 +- 9 files changed, 2359 insertions(+), 1 deletion(-) create mode 100644 modules/UI/mythicplus/advancement_state.ts create mode 100644 modules/UI/mythicplus/mythic_advancement.client.ts create mode 100644 modules/UI/mythicplus/mythic_advancement.server.ts create mode 100644 modules/UI/mythicplus/mythic_custom_spells.ts create mode 100644 modules/UI/mythicplus/mythic_items.ts create mode 100644 modules/UI/mythicplus/mythic_npcs.ts create mode 100644 modules/classes/mapzones.ts diff --git a/modules/UI/mythicplus/advancement_state.ts b/modules/UI/mythicplus/advancement_state.ts new file mode 100644 index 0000000..28145d7 --- /dev/null +++ b/modules/UI/mythicplus/advancement_state.ts @@ -0,0 +1,35 @@ +import { Logger } from "../../classes/logger"; +const log = new Logger("AdvancementState"); + +export type AdvancementType = "Magic" | "Attack" | "Defense"; +export class AdvancementState { + + private advancement: string | null = null; + private advType: AdvancementType | null = null; + + SetType(type: AdvancementType): void { + this.advType = type; + } + + GetAdvType(): AdvancementType | null { + return this.advType; + } + + SetAdvancement(icon: string): void { + this.advancement = icon; + } + + GetAdvancement(): string | null { + return this.advancement; + } + + ClearAdvancement() { + this.advancement = null; + } + + ClearState() { + this.advancement = null; + this.advType = null; + } + +} \ No newline at end of file diff --git a/modules/UI/mythicplus/mythic_advancement.client.ts b/modules/UI/mythicplus/mythic_advancement.client.ts new file mode 100644 index 0000000..7c4e286 --- /dev/null +++ b/modules/UI/mythicplus/mythic_advancement.client.ts @@ -0,0 +1,591 @@ +/** @ts-expect-error */ +let aio: AIO = {}; + +const id = (name: string) => `MythicAdvUI_${name}`; + +function GetComponent(name: string, type: string = "Frame"): T | null { + const component = _G[id(name)]; + return component ? component as T : null; +} + +import { colors } from "../../classes/ui-utils"; +import { AdvancementState } from "./advancement_state"; + +/** + * Advancement Name and Spell Reference + * + * ('80000001','spell_mp_titans_strength_aura'), + * ('80000002','spell_mp_steel_forged_aura'), + * ('80000003','spell_mp_celestial_grace_aura'), + * ('80000004','spell_mp_forbidden_knowledge_aura'), + * ('80000005','spell_mp_spectral_reflexes_aura'), + * ('80000006','spell_mp_eldritch_barrier_aura'), + * ('80000007','spell_mp_hellfire_shielding_aura'), + * ('80000008','spell_mp_primal_endurance_aura'), + * ('80000009','spell_mp_lichbane_aura'), + * ('80000010','spell_mp_glacial_fortress_aura'); + */ + +if (!aio.AddAddon()) { + const upgradeUIHandlers = aio.AddHandlers("MythicAdvUI", {}); + const UpgradeUIFrames: Map = new Map(); + + // const itemSlots: Map = new Map(); + // let selectedDice: WoWAPI.Button | null = null; + // let rolling: boolean = false; + + const mainWidth = 768; + const mainHeight = 512; + const centerOffset = mainWidth / 2; + + // Load the advancement state based on the requested type for now just do magic type + const advancementState = new AdvancementState(); + advancementState.SetType("Magic"); + + const customTextures = { + // frame background elements + "bgFrame": "Interface\\AddOns\\MythicPlusData\\Textures\\mythic-adv-frame", + "bgFrameHq": "Interface\\AddOns\\MythicPlusData\\Textures\\mythic-adv-frame-hq", + "darkBg": "Interface\\AddOns\\MythicPlusData\\Textures\\DialogBox-Background-Dark", + "diceFrame": "Interface\\AddOns\\MythicPlusData\\Textures\\advancement_frame", + + // advancement icons buttons + "str": "Interface\\AddOns\\MythicPlusData\\Textures\\str_adv.tga", + "agi": "Interface\\AddOns\\MythicPlusData\\Textures\\agi_adv.tga", + "int": "Interface\\AddOns\\MythicPlusData\\Textures\\int_adv.tga", + "spr": "Interface\\AddOns\\MythicPlusData\\Textures\\spr_adv.tga", + "sta": "Interface\\AddOns\\MythicPlusData\\Textures\\sta_adv.tga", + "icon_overlay": "Interface\\AddOns\\MythicPlusData\\Textures\\icon_overlay.tga", + "int_selected": "Interface\\AddOns\\MythicPlusData\\Textures\\int_adv_selected.tga", + "spr_selected": "Interface\\AddOns\\MythicPlusData\\Textures\\spr_adv_selected.tga", + + // Dice buttons + "single_roll": "Interface\\AddOns\\MythicPlusData\\Textures\\single_roll.tga", + "double_roll": "Interface\\AddOns\\MythicPlusData\\Textures\\double_roll.tga", + "triple_roll": "Interface\\AddOns\\MythicPlusData\\Textures\\triple_roll.tga", + "single_roll_selected": "Interface\\AddOns\\MythicPlusData\\Textures\\single_roll_selected.tga", + "double_roll_selected": "Interface\\AddOns\\MythicPlusData\\Textures\\double_roll_selected.tga", + "triple_roll_selected": "Interface\\AddOns\\MythicPlusData\\Textures\\triple_roll_selected.tga", + + // Advancement Bars + "bars": "Interface\\AddOns\\MythicPlusData\\Textures\\adv_bars.tga", + + // Roll Numbers + "numbers": "Interface\\AddOns\\MythicPlusData\\Textures\\big_numbers.tga", + } + + interface ItemSlot extends WoWAPI.Button { + hasItem?: boolean; + itemLink?: string; + } + + function GetTexture(name: string): string { + return customTextures[name] || ""; + } + + function UpdateDiceVisibility(show: boolean): void { + const buttons = ['single_roll', 'double_roll', 'triple_roll']; + buttons.forEach(name => { + const button = GetComponent(`${name}_button`, "Button"); + if (button) { + if (show) { + button.SetAlpha(0.8); + button.Enable(); + } else { + button.SetAlpha(0.0); + button.Disable(); + } + } + }); + } + + function SelectAdvancement(name: string, button: WoWAPI.Button): void { + + // if the button click is itself, reset it + if (advancementState.GetAdvancement() === name) { + button.SetNormalTexture(GetTexture(name)); + advancementState.SetAdvancement(null); + UpdateDiceVisibility(false); + } else { + // Set the new advancement + button.SetNormalTexture(GetTexture(`${name}_selected`)); + + // Handle opposing button + const opposingStats = { + 'int': 'spr', + 'spr': 'int', + 'str': 'agi', + 'agi': 'str' + }; + + const opposingStat = opposingStats[name]; + if (opposingStat) { + const opposingButton = GetComponent(`${opposingStat}_button`, "Button"); + if (opposingButton) { + opposingButton.SetNormalTexture(GetTexture(opposingStat)); + } + } + + // Update advancement state and show dice buttons + advancementState.SetAdvancement(name); + UpdateDiceVisibility(true); + } + } + + /** + * Add icons icons used for advancement upgrades. + * @param frame Main Frame + */ + function CreateAdvIcons(frame: WoWAPI.Frame): void { + + const ICON_POS_LEFT = 38; + const ICON_POS_TOP = -150; + + let topIcon: string = ""; + let bottomIcon: string = ""; + let topText: string = ""; + let bottomText: string = ""; + + if (advancementState.GetAdvType() === "Magic") { + topIcon = "spr"; + bottomIcon = "int"; + topText = "Celestial Grace"; + bottomText = "Forbidden Knowledge"; + } + else if (advancementState.GetAdvType() === "Attack") { + topIcon = "str"; + bottomIcon = "agi"; + topText = "Titan's Strength"; + bottomText = "Spectral Reflexes"; + } + else if (advancementState.GetAdvType() === "Defense") { + topIcon = "sta"; + topText = "Steel Forged" + } + + const topButton = CreateFrame("Button", id(`${topIcon}_button`), frame); + topButton.SetSize(132, 132); + topButton.SetPoint("TOPLEFT", ICON_POS_LEFT, ICON_POS_TOP); + topButton.SetNormalTexture(GetTexture(topIcon)); + topButton.SetHighlightTexture(GetTexture("icon_overlay")); + topButton.SetScript("OnClick", function(self: WoWAPI.Button) { + SelectAdvancement(topIcon, self); + }); + + topButton.Show(); + + const bottomButton = CreateFrame("Button", id(`${bottomIcon}_button`), frame); + bottomButton.SetSize(132, 132); + bottomButton.SetPoint("TOPLEFT", topButton, "BOTTOMLEFT", 0, -20); + bottomButton.SetNormalTexture(GetTexture(bottomIcon)); + bottomButton.SetHighlightTexture(GetTexture("icon_overlay")); + bottomButton.SetScript("OnClick", function(self: WoWAPI.Button) { + SelectAdvancement(bottomIcon, self); + }); + + bottomButton.Show(); + + // Add advancement bars + const topbarTexture = frame.CreateTexture(id("topbar_gold"), "ARTWORK"); + topbarTexture.SetPoint("LEFT", topButton, "RIGHT", -4, -10); + topbarTexture.SetSize(187, 37); + topbarTexture.SetTexture(GetTexture("bars")); + topbarTexture.SetTexCoord(36/256, 218/256, 0/256, 37/256); + + const bottombarTexture = frame.CreateTexture(id("bottombar_gold"), "ARTWORK"); + bottombarTexture.SetPoint("LEFT", bottomButton, "RIGHT", -4, -10); + bottombarTexture.SetSize(187, 37); + bottombarTexture.SetTexture(GetTexture("bars")); + bottombarTexture.SetTexCoord(36/256, 218/256, 0/256, 37/256); + + const topbarText = frame.CreateFontString(id("topbar_text"), "OVERLAY"); + topbarText.SetPoint("CENTER", topbarTexture, "CENTER", 0, 33); + topbarText.SetTextColor(1, 1, 1, 1) // White + topbarText.SetFont("Fonts\\FRIZQT__.TTF", 16) + topbarText.SetText(topText); + + const bottombarText = frame.CreateFontString(id("bottombar_text"), "OVERLAY"); + bottombarText.SetPoint("CENTER", bottombarTexture, "CENTER", 0, 33); + bottombarText.SetTextColor(1, 1, 1, 1) // White + bottombarText.SetFont("Fonts\\FRIZQT__.TTF", 16) + bottombarText.SetText(bottomText); + } + + /** + * + */ + + /** + * Creates the dice roll buttons that appear below the roll frame + * @param frame Main Frame + * @param rollFrame The frame containing the roll numbers + */ + function CreateDiceButtons(frame: WoWAPI.Frame, rollFrame: WoWAPI.Frame): void { + const buttonWidth = 72; + const buttonHeight = 72; + const spacing = 20; + const totalWidth = (buttonWidth * 3) + (spacing * 2); + const startX = -(totalWidth / 2) + (buttonWidth / 2); + + const buttons = [ + { name: 'single_roll', rolls: 1 }, + { name: 'double_roll', rolls: 2 }, + { name: 'triple_roll', rolls: 3 } + ]; + + let selectedButton = GetComponent("selected_dice_button", "Button"); + + buttons.forEach((buttonInfo, index) => { + const buttonId = `${buttonInfo.name}_button`; + let button = GetComponent(buttonId, "Button"); + + if (!button) { + button = CreateFrame("Button", id(buttonId), frame) as WoWAPI.Button; + button.SetSize(buttonWidth, buttonHeight); + button.SetPoint("TOP", rollFrame, "BOTTOM", startX + (index * (buttonWidth + spacing)), -75); + + const normalTexture = button.CreateTexture(null, "BACKGROUND"); + normalTexture.SetTexture(GetTexture(buttonInfo.name)); + normalTexture.SetAllPoints(button); + button.SetNormalTexture(normalTexture); + + const highlightTexture = button.CreateTexture(null, "HIGHLIGHT"); + highlightTexture.SetTexture(GetTexture(`${buttonInfo.name}_selected`)); + highlightTexture.SetAllPoints(button); + button.SetHighlightTexture(highlightTexture); + + // Start hidden by default + button.SetAlpha(0.0); + button.SetScript("OnClick", function() { + if (selectedButton) { + const prevButtonInfo = buttons.find(b => b.name === selectedButton.GetName().split('_')[0]); + if (prevButtonInfo) { + selectedButton.SetNormalTexture(GetTexture(prevButtonInfo.name)); + } + } + + if (selectedButton === button) { + selectedButton = null; + } else { + selectedButton = button; + button.SetNormalTexture(GetTexture(`${buttonInfo.name}_selected`)); + // Here you would typically trigger the roll with the number of dice + frame["rollNumbers"](buttonInfo.rolls, 25 * buttonInfo.rolls); + } + }); + } + }); + + // Ensure buttons are hidden/shown based on current advancement state + UpdateDiceVisibility(advancementState.GetAdvancement() !== null); + } + + /** + * Add title text to the frame. + * @param frame Main Frame + */ + function AddTitle(frame: WoWAPI.Frame): void { + // Add title text + const titleBar = frame.CreateFontString(id("TitleBar"), "OVERLAY"); + titleBar.SetPoint("TOP", -150, -40); + titleBar.SetTextColor(1, 0.84, 0, 1) // Golden yellow + titleBar.SetFont("Interface/Modules/MythicPlus/Fonts/NOTO.TTF", 16) + titleBar.SetText(`Mythic Upgrades`); + } + + /** + * This adds in the big numbers that will flash to simulate a dice rolls... + * even though the dice roll actually happens on the server in the module. + * @param frame + */ + function ShowRollNumbers(frame: WoWAPI.Frame): void { + + let rollFrame = GetComponent("roll_frame"); + if (!rollFrame) { + rollFrame = CreateFrame("Frame", id("roll_frame"), frame); + rollFrame.SetSize(120, 120); + rollFrame.SetPoint("CENTER", 200, -24); + rollFrame.SetBackdrop({ + bgFile: GetTexture("darkBg"), + tile: false, + tileSize: 120, + insets: { left: 1, right: 1, top: 1, bottom: 1 }, + }); + rollFrame.SetBackdropColor(0, 0, 0, 0.3); // RGB + alpha + } + rollFrame.Show(); + + // create a texture that will change the graphic every few seconds between 1-25 using big_number tga + // for numbers greater than 9 it will need to add to smaller textures. Below are coordinates of the + // big_number tga [UL, UR, LL, LR] + + const coords: {[key: number]: number[]} = { + 0: [35, 96, 8, 81], + 1: [174, 210, 8, 81], + 2: [292, 345, 8, 81], + 3: [424, 472, 8, 81], + 4: [33, 95, 98, 163], + 5: [167, 217, 98, 163], + 6: [291, 346, 98, 163], + 7: [425, 471, 98, 163], + 8: [38, 89, 182, 247], + 9: [165, 218, 182, 247], + } + + let currentTextures: WoWAPI.Texture[] = []; + + function clearTextures() { + currentTextures.forEach(texture => { + texture.Hide(); + texture.ClearAllPoints(); + }); + currentTextures = []; + } + + function createNumberTexture(digit: number, xOffset: number = 0) { + const rollTexture = rollFrame.CreateTexture(null, "ARTWORK"); + rollTexture.SetSize(60, 60); + rollTexture.SetPoint("CENTER", rollFrame, "CENTER", xOffset, 0); + rollTexture.SetTexture(GetTexture("numbers")); + + + if (coords[digit]) { + rollTexture.SetTexCoord( + coords[digit][0] / 512, + coords[digit][1] / 512, + coords[digit][2] / 256, + coords[digit][3] / 256 + ); + } + currentTextures.push(rollTexture); + return rollTexture; + } + + function showNumber(num: number) { + clearTextures(); + if (num >= 10) { + const tens = Math.floor(num / 10); + const ones = num % 10; + createNumberTexture(tens, -30).Show(); + createNumberTexture(ones, 30).Show(); + } else { + createNumberTexture(num, 0).Show(); // Explicitly set offset to 0 for single digits + } + } + + let rolling = false; + let lastUpdate = 0; + + // Make rollNumbers accessible on the frame for external calls + frame["rollNumbers"] = function(min: number, max: number, duration: number = 2000) { + if (rolling) return; + clearTextures(); + rolling = true; + let elapsed = 0; + + frame.SetScript("OnUpdate", function(_, deltaTime) { + elapsed = elapsed + deltaTime * 1000; + lastUpdate += deltaTime * 1000; + + // Update number every 100ms + if (lastUpdate >= 100) { + lastUpdate = 0; + const randomNum = Math.floor(Math.random() * (max - min + 1)) + min; + showNumber(randomNum); + } + + if (elapsed >= duration) { + frame.SetScript("OnUpdate", null); + rolling = false; + } + }); + } + } + + /** + * Create the main upgrade window frame. + * @returns Main Frame + */ + function CreateUpgradeWindow(state: AdvancementState): WoWAPI.Frame { + + // Main frame creation + const frame = CreateFrame("Frame", id("main"), UIParent); + frame.SetSize(mainWidth, mainHeight); + frame.SetPoint("CENTER"); + frame.SetMovable(true); + frame.EnableMouse(true); + frame.RegisterForDrag("LeftButton"); + frame.SetScript("OnDragStart", frame.StartMoving); + frame.SetScript("OnDragStop", frame.StopMovingOrSizing); + frame.SetFrameLevel(100); // Set high frame level to stay on top + + // Background texture + const bgTexture = frame.CreateTexture(id("background"), "BACKGROUND"); + bgTexture.SetPoint('TOPLEFT'); + bgTexture.SetWidth(mainWidth); + bgTexture.SetHeight(mainHeight); + bgTexture.SetTexture(GetTexture("bgFrameHq")); + bgTexture.SetTexCoord(0, 768/1024, 0, 1); // Show 75% of width (768/1024) and full height + + // Add components + CreateAdvIcons(frame); + AddTitle(frame); + ShowRollNumbers(frame); + + + + const rollFrame = GetComponent("roll_frame"); + if (rollFrame) { + CreateDiceButtons(frame, rollFrame); + } + + return frame; + } + + // Remove the template's close button + // const templateCloseButton = frame.GetChildren()[0] as WoWAPI.Frame; + // if (templateCloseButton) { + // templateCloseButton.Hide(); + // } + + // Add our custom close button + // const closeButton = CreateFrame("Button", id("CloseButton"), frame, "UIPanelCloseButton"); + // closeButton.SetPoint("TOPRIGHT", -5, -5); + // closeButton.SetScript("OnClick", () => { + // frame.Hide(); + // }); + + // Header Row: Skill Icons & Ranks + // for (let i = 0; i < 3; i++) { + // const skillFrame = CreateFrame("Frame", id(`Skill_${i}`), frame); + // skillFrame.SetSize(64, 64); + // skillFrame.SetPoint("TOP", -125 + i * 125, -55); + + // const skillIcon = skillFrame.CreateTexture(id(`SkillIcon_${i}`), "ARTWORK"); + // skillIcon.SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); + // skillIcon.SetAllPoints(); + + // const skillName = skillFrame.CreateFontString(id(`SkillName_${i}`), "OVERLAY", "GameFontNormalLarge"); + // skillName.SetPoint("TOP", skillFrame, "BOTTOM", 0, -5); + // skillName.SetText(`Skill ${i + 1}`); + + // const skillRank = skillFrame.CreateFontString(id(`SkillRank_${i}`), "OVERLAY", "GameFontHighlightLarge"); + // skillRank.SetPoint("TOP", skillName, "BOTTOM", 0, -2); + // skillRank.SetText("0 / 50"); + // } + + // Add a new texture in the middle of the frame that is 512x512 and the middle layer + // const centerTexture = frame.CreateTexture(id("DiceTextureBack"), "ARTWORK"); + // centerTexture.SetDrawLayer("ARTWORK", 3); + // centerTexture.SetSize(128, 128); + // centerTexture.SetPoint("CENTER", frame, "CENTER", 0, -20); + // centerTexture.SetTexture("Interface\\MythicPlus\\adv-dice-light.blp"); + + // const circleTexture = frame.CreateTexture(id("DiceTextureBack"), "BACKGROUND"); + // circleTexture.SetDrawLayer("ARTWORK", 5); + // circleTexture.SetSize(512, 512); + // circleTexture.SetAlpha(0.20); + // circleTexture.SetPoint("CENTER", frame, "CENTER", 0, -20); + // circleTexture.SetTexture("Interface\\MythicPlus\\gold-circle.blp"); + + // // Add a glowing effect behind the dice + // const glowTexture = frame.CreateTexture(id("DiceGlow"), "ARTWORK"); + // glowTexture.SetDrawLayer("ARTWORK", -2); + // glowTexture.SetSize(300, 300); + // glowTexture.SetPoint("CENTER", centerTexture, "CENTER", 0, 0); + // glowTexture.SetTexture("Interface\\GLUES\\MODELS\\UI_MainMenu_Legion\\UI_MainMenu_Legion3"); + // glowTexture.SetAlpha(0.3); + + // Roll Display with better font handling + // const rollDisplay = frame.CreateFontString(id("RollDisplay"),"ARTWORK"); + // rollDisplay.SetFont("Interface\\MythicPlus\\NOTO.TTF", 256, "THICKOUTLINE"); + // rollDisplay.SetPoint("CENTER", frame, "CENTER", 0, -80); + // rollDisplay.SetTextColor(1, 0.84, 0, 1); // More golden yellow color + // rollDisplay.SetText("-"); + + + // Dice Multipliers with Selection Logic + // const diceMultipliers = ["1x", "2x", "3x"]; + // for (let k = 0; k < diceMultipliers.length; k++) { + // const diceButton = CreateFrame("Button", id(`Dice_${k}`), frame); + // diceButton.SetSize(24, 24); + // diceButton.SetPoint("TOPLEFT", 150 + (k * 30), -450); + + // const diceText = diceButton.CreateFontString(id(`DiceText_${k}`), "OVERLAY", "GameFontHighlight"); + // diceText.SetPoint("CENTER", diceButton, "CENTER", 0, 0); + // diceText.SetText(diceMultipliers[k]); + + // diceButton.SetScript("OnClick", function() { + // if (selectedDice) { + // selectedDice.SetAlpha(1.0); // Reset previous selection + // } + // selectedDice = diceButton; + // selectedDice.SetAlpha(1.5); // Glow effect + // }); + // } + + // Roll Button with animation logic + // const rollButton = CreateFrame("Button", id("RollButton"), frame, "UIPanelButtonTemplate"); + // rollButton.SetSize(100, 30); + // rollButton.SetPoint("BOTTOM", frame, "BOTTOM", 0, 50); + // rollButton.SetText("Roll"); + + // function rollDice(min: number, max: number, duration: number = 5000) { + // if (rolling) return; + // rolling = true; + // let elapsed = 0; + // let lastUpdate = 0; + + // frame.SetScript("OnUpdate", function(_, deltaTime) { + // elapsed = elapsed + deltaTime * 5000; + // lastUpdate += deltaTime * 1000; + + // // Only update every 200ms to slow down the rolling animation + // if (lastUpdate >= 100) { + // lastUpdate = 0; + // rollDisplay.SetText(`${Math.floor(Math.random() * (max - min + 1)) + min}`); + // } + + // if (elapsed >= duration) { + // frame.SetScript("OnUpdate", null); + // rolling = false; + // // Set final roll + // rollDisplay.SetText(`${Math.floor(Math.random() * (max - min + 1)) + min}`); + // } + // }); + // } + + // rollButton.SetScript("OnClick", function() { + // rollDice(2, 23); // Default to 2-23 range for backward compatibility + // }); + + // Roll History + // const historyFrame = CreateFrame("Frame", id("HistoryFrame"), frame); + // historyFrame.SetSize(380, 80); + // historyFrame.SetPoint("BOTTOM", frame, "BOTTOM", 0, 10); + + // const historyText = historyFrame.CreateFontString(id("HistoryText"), "OVERLAY", "GameFontHighlight"); + // historyText.SetPoint("TOPLEFT", historyFrame, "TOPLEFT", 10, -10); + // historyText.SetText("Roll History:"); + + + let frame: WoWAPI.Frame | undefined = undefined; + + if(!UpgradeUIFrames.has(advancementState.GetAdvType())) { + // frame = CreateUpgradeWindow(advancementState); + } else { + // if we have the frame already show it + // frame = UpgradeUIFrames.get(advancementState.GetAdvType()); + } + + // For debugging purposes show the window + //frame.Show(); + + // Triggered from ths server to be set up later + upgradeUIHandlers.ShowUpgradeWindow = () => { + // if (!UpgradeUIFrames.has(1)) { + // CreateUpgradeWindow(); + // } + // UpgradeUIFrames.get(1).Show(); + }; +} diff --git a/modules/UI/mythicplus/mythic_advancement.server.ts b/modules/UI/mythicplus/mythic_advancement.server.ts new file mode 100644 index 0000000..e235ce5 --- /dev/null +++ b/modules/UI/mythicplus/mythic_advancement.server.ts @@ -0,0 +1,223 @@ +/** @ts-expect-error */ +let aio: AIO = {}; + +const SCRIPT_NAME = 'UpgradeUI'; +import { Logger } from "../../classes/logger"; +const log = new Logger(SCRIPT_NAME); + +// Flag to track if materials have been loaded +let materialsLoaded = false; + +type ItemType = { + entry: number; + name: string; +} + +type Materials = { + name: string; + items: Array +} + +const upgradeMaterials: Record = {}; + +/** + * Helper function to safely get items from a material + */ +const getItemsFromMaterial = (material: Materials): Array => { + if (!material) { + return []; + } + + if (!material.items) { + material.items = []; + } + + return material.items; +}; + +/** + * Handles the logic for showing the upgrade UI when a player types .advanceme + */ +const ShowUpgradeUI: player_event_on_command = (event: number, player: Player, command: string): boolean => { + if (command === "advanceme") { + log.info(`Showing Upgrade UI for player: ${player.GetName()}`); + aio.Handle(player, 'UpgradeUI', 'ShowUpgradeWindow'); + return false; + } + return true; +}; + +/** + * Get a list of materials from the database that can be used for mythic plus advancement + * @param event World startup event + */ +// const GetMaterialsList: eluna_event_on_lua_state_open = (event: number) => { + +// const query = WorldDBQuery(`select materialId, entry, name from mp_material_types`); + +// const categoryMap: Record = { +// 1: "Cloth", +// 2: "Rare Cloth", +// 3: "Plants", +// 4: "Rare Plants", +// 5: "Ore", +// 6: "Rare Ore", +// 7: "Leather", +// 8: "Rare Leather", +// 9: "Gems", +// 10: "Rare Gems", +// 11: "Enchanting", +// 12: "Rare Enchanting", +// 13: "Water Elements", +// 14: "Rare Water Elements", +// 15: "Fire Elements", +// 16: "Rare Fire Elements", +// 17: "Nature Elements", +// 18: "Rare Earth Elements", +// 19: "Shadow Elements", +// 20: "Rare Shadow Elements", +// 21: "Arcane Elements", +// 22: "Rare Arcane Elements", +// 23: "Veilstone", +// }; + +// if (query) { +// do { +// const materialId = query.GetUInt32(0); +// const entry = query.GetUInt32(1); +// const categoryName = categoryMap[materialId] || "Unknown"; + +// // Initialize the material group if it doesn't exist +// if (!upgradeMaterials[materialId]) { +// upgradeMaterials[materialId] = { +// name: categoryName, +// items: [] +// }; +// } + +// // Add the item to the material group +// upgradeMaterials[materialId].items.push({ +// entry: entry, +// name: query.GetString(2) +// }); + +// } while (query.NextRow()); +// } + +// log.info(`Loaded ${Object.keys(upgradeMaterials).length} material groups with items for mythic plus advancement`); +// materialsLoaded = true; +// }; + +// /** +// * This will show all the counts for a player by from the upgradeMaterials object +// */ +// function GetMaterials(this:void, player: Player) { + +// // Create a readable string representation of the materials and their counts +// let materialsString = 'Your Mythic+ Advancement Materials:\n'; +// log.info("Showing material counts"); + +// // Track total materials by category +// const materialCounts: Record = {}; + +// // Initialize counts for all categories +// const materialIds = Object.keys(upgradeMaterials); +// log.info(`Found ${materialIds.length} material groups`); + +// // Initialize all material counts to 0 +// for (let i = 0; i < materialIds.length; i++) { +// const materialId = materialIds[i]; +// materialCounts[materialId] = 0; +// } + +// // Process each material group +// for (let i = 0; i < materialIds.length; i++) { +// const materialId = materialIds[i]; +// log.info(`Processing material ID: ${materialId}`); + +// // Get the material object and verify it exists +// const material = upgradeMaterials[materialId]; +// if (!material) { +// log.info(`Material with ID ${materialId} is undefined`); +// continue; +// } + +// // Get the material name +// const materialName = material.name || "Unknown Material"; +// materialsString = materialsString + '\n' + materialName + ':\n'; + +// // Get items using our helper function to ensure it's not nil +// const items = getItemsFromMaterial(material); +// log.info(`Material ${materialName} has ${items.length} items`); + +// if (items.length === 0) { +// log.info(`No items found for material ${materialName}`); +// materialsString = materialsString + ' No items found in this category\n'; +// continue; +// } + +// // Process each item in the material category +// let categoryTotal = 0; +// for (let j = 0; j < items.length; j++) { +// // Get the item and verify it exists +// const item = items[j]; +// if (!item) { +// log.info(`Item at index ${j} is undefined for material ${materialName}`); +// continue; +// } + +// // Get the item entry and name +// const itemEntry = item.entry; +// const itemName = item.name || "Unknown Item"; + +// log.info(`Checking item ${itemName} (${itemEntry})`); + +// try { +// // Get the item count +// const count = player.GetItemCount(itemEntry, true); // true = include bank +// log.info(`Player has ${count} of item ${itemName}`); + +// // Add to category total +// categoryTotal = categoryTotal + count; + +// // Only show items the player has +// if (count > 0) { +// materialsString = materialsString + ' - ' + itemName + ': ' + count + '\n'; +// } +// } catch (error) { +// log.info(`Error getting count for item ${itemName}: ${error}`); +// } +// } + +// // Store the category total +// materialCounts[materialId] = categoryTotal; + +// // Add total for this category +// materialsString = materialsString + ' Total ' + materialName + ': ' + categoryTotal + '\n'; +// } + +// log.info("Initialized material counts"); + +// // Send the material counts to the player +// player.SendBroadcastMessage(materialsString); + +// // Log the material counts for debugging +// log.info('Showed material counts for player: ' + player.GetName()); + +// return true; +// } + +// const UpgradeHandlers = aio.AddHandlers("UpgradeUI", { +// ShowMaterialCount +// }); + +/** + * Register the command event to listen for ".advanceme" + */ +RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => ShowUpgradeUI(...args)); + +/** + * Get all the material types from the database + * RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_OPEN, (...args) => GetMaterialsList(...args)); + */ + diff --git a/modules/UI/mythicplus/mythic_custom_spells.ts b/modules/UI/mythicplus/mythic_custom_spells.ts new file mode 100644 index 0000000..59f49d1 --- /dev/null +++ b/modules/UI/mythicplus/mythic_custom_spells.ts @@ -0,0 +1,356 @@ +/** + * Custom Spells that have been implemented for Mythic Plus components + */ +import { GetFusedItemFromItemEntry, IsVeilstone, IsMythicFusionItem, IsRareFusionItem, GetFusedItemMaterialId } from "./mythic_items"; +import { Logger } from "../../classes/logger"; + +const log = new Logger("mythic_custom_spells"); +const MAX_STACK_SIZE = 200; + +/** + * Spells for combining old materials + * 150000 - Ore Fusion + * 150001 - Cloth Fusion + * 150002 - Leather Fusion + * 150003 - Alchemy Fusion + * 150004 - Gem Fusion + * 150005 - Essence Fusion + * 150006 - Cold Fusion + * 150007 - Flame Fusion + * 150008 - Arcane Fusion + * 150009 - Dark Fusion + * 150010 - Earth Fusion + */ + +// Spells for combining old materials +export const SPELL_ORE_FUSION = 150000; +export const SPELL_CLOTH_FUSION = 150001; +export const SPELL_LEATHER_FUSION = 150002; +export const SPELL_ALCHEMY_FUSION = 150003; +export const SPELL_GEM_FUSION = 150004; +export const SPELL_ESSENCE_FUSION = 150005; +export const SPELL_COLD_FUSION = 150006; +export const SPELL_FLAME_FUSION = 150007; +export const SPELL_ARCANE_FUSION = 150008; +export const SPELL_DARK_FUSION = 150009; +export const SPELL_EARTH_FUSION = 150010; +export const SPELL_ORE_FUSION_RANK_2 = 150011; +export const SPELL_CLOTH_FUSION_RANK_2 = 150012; +export const SPELL_LEATHER_FUSION_RANK_2 = 150013; +export const SPELL_ALCHEMY_FUSION_RANK_2 = 150014; +export const SPELL_GEM_FUSION_RANK_2 = 150015; +export const SPELL_ESSENCE_FUSION_RANK_2 = 150016; +export const SPELL_COLD_FUSION_RANK_2 = 150017; +export const SPELL_FLAME_FUSION_RANK_2 = 150018; +export const SPELL_ARCANE_FUSION_RANK_2 = 150019; +export const SPELL_DARK_FUSION_RANK_2 = 150020; +export const SPELL_EARTH_FUSION_RANK_2 = 150021; + + +export const FUSION_SPELLS_RANK_1 = [150000, 150001, 150002, 150003, 150004, 150005, 150006, 150007, 150008, 150009, 150010]; +export const FUSION_SPELLS_RANK_2 = [150011, 150012, 150013, 150014, 150015, 150016, 150017, 150018, 150019, 150020, 150021]; + +/** + * Stat Advancement Spells + * 1. 80000001 - Titans' Strength Aura + * 2. 80000002 - Steel Forged Aura + * 3. 80000003 - Celestial Grace Aura + * 4. 80000004 - Forbidden Knowledge Aura + * 5. 80000005 - Spectral Reflexes Aura + * 6. 80000006 - Eldritch Barrier Aura + * 7. 80000007 - Hellfire Shielding Aura + * 8. 80000008 - Primal Endurance Aura + * 9. 80000009 - Lichbane Aura + * 10. 80000010 - Glacial Fortress Aura + */ + +// Stat Advancement Spells +export enum StatAdvancementSpell { + TITANS_STRENGTH_AURA = 80000001, + STEEL_FORGED_AURA = 80000002, + CELESTIAL_GRACE_AURA = 80000003, + FORBIDDEN_KNOWLEDGE_AURA = 80000004, + SPECTRAL_REFLEXES_AURA = 80000005, + ELDRITCH_BARRIER_AURA = 80000006, + HELLFIRE_SHIELDING_AURA = 80000007, + PRIMAL_ENDURANCE_AURA = 80000008, + LICHBANE_AURA = 80000009, + GLACIAL_FORTRESS_AURA = 80000010, +} + +export const STAT_ADVANCEMENT_SPELLS = { + TITANS_STRENGTH_AURA: StatAdvancementSpell.TITANS_STRENGTH_AURA, + STEEL_FORGED_AURA: StatAdvancementSpell.STEEL_FORGED_AURA, + CELESTIAL_GRACE_AURA: StatAdvancementSpell.CELESTIAL_GRACE_AURA, + FORBIDDEN_KNOWLEDGE_AURA: StatAdvancementSpell.FORBIDDEN_KNOWLEDGE_AURA, + SPECTRAL_REFLEXES_AURA: StatAdvancementSpell.SPECTRAL_REFLEXES_AURA, + ELDRITCH_BARRIER_AURA: StatAdvancementSpell.ELDRITCH_BARRIER_AURA, + HELLFIRE_SHIELDING_AURA: StatAdvancementSpell.HELLFIRE_SHIELDING_AURA, + PRIMAL_ENDURANCE_AURA: StatAdvancementSpell.PRIMAL_ENDURANCE_AURA, + LICHBANE_AURA: StatAdvancementSpell.LICHBANE_AURA, + GLACIAL_FORTRESS_AURA: StatAdvancementSpell.GLACIAL_FORTRESS_AURA, +}; + +const RARE_ITEM_REQUIREMENT = 20; +const MYTHIC_ITEM_REQUIREMENT = 5; + +// Get the total number of items in the stack for the given item +function GetTotalStackCount(item: Item): number { + if (!item) return 0; + return item.GetCount ? item.GetCount() : 0; +} + +const playerAngerCount: Record = {}; + +const MythicMaterialFusion: player_event_on_spell_cast = (event: number, player: Player, spell: Spell, skipCheck: boolean) => { + if(!FUSION_SPELLS_RANK_1.includes(spell.GetEntry()) && !FUSION_SPELLS_RANK_2.includes(spell.GetEntry())) { + return false; + } + + const rank = FUSION_SPELLS_RANK_1.includes(spell.GetEntry()) ? 1 : 2; + const target = spell.GetTarget() as Item; + log.info(`Item Details ${target.GetName()}`); + const hasItem = player.HasItem(target.GetEntry()); + + if(!hasItem) { + log.info("Did not have an item target"); + return false; + } + + const fusedItemEntry = GetFusedItemFromItemEntry(target.GetEntry()); + + log.info(`Fused Item Entry ${fusedItemEntry}`); + if(fusedItemEntry == 0) { + player.SendBroadcastMessage("You can not cast fuse this material"); + player.PlayDirectSound(847); + return false; + } + + if(IsVeilstone(target.GetEntry())) { + PrintInfo(`Veilstone detected`); + const angerCount = (playerAngerCount[player.GetGUIDLow()] || 0) + 1; + playerAngerCount[player.GetGUIDLow()] = angerCount; + if(angerCount === 1) { + player.SendBroadcastMessage("You are attempting to do something that is forbidden, do no not do it again."); + player.PlayDirectSound(847); + return false; + } + + if(angerCount == 2) { + player.SendBroadcastMessage("You are about to anger the gods, stop yourself before you cause a rift in the fabric of the world!!!"); + player.PlayDirectSound(847); + return false + } + + if(angerCount >= 3) { + player.Teleport(0, -11139.2, -1742.44, -29.7367, 0); + player.SendNotification("YOU HAVE ATTEMPTED TO CORRUPT THE VEIL AND HAVE ANGERED THE GODS. YOU HAVE BEEN SENTENCED TO PURGATORY!"); + player.RemoveItem(target, 1); + return false; + } + } + + // make sure the correct spell is mapping to the correct material type + // @see CategoryMapToFused + const materialId = GetFusedItemMaterialId(target.GetEntry()); + if(materialId == 0) { + PrintInfo(`No material id found for fused item entry: ${fusedItemEntry}`); + return false; + } + if(materialId === 1 && spell.GetEntry() !== SPELL_CLOTH_FUSION && spell.GetEntry() !== SPELL_CLOTH_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + if(materialId === 2 && spell.GetEntry()!== SPELL_CLOTH_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 3 && spell.GetEntry() !== SPELL_ALCHEMY_FUSION && spell.GetEntry() !== SPELL_ALCHEMY_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 4 && spell.GetEntry() !== SPELL_ALCHEMY_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 5 && spell.GetEntry() !== SPELL_ORE_FUSION && spell.GetEntry() !== SPELL_ORE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 6 && spell.GetEntry() !== SPELL_ORE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 7 && spell.GetEntry() !== SPELL_LEATHER_FUSION && spell.GetEntry() !== SPELL_LEATHER_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 8 && spell.GetEntry() !== SPELL_LEATHER_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 9 && spell.GetEntry() !== SPELL_GEM_FUSION && spell.GetEntry() !== SPELL_GEM_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 10 && spell.GetEntry() !== SPELL_GEM_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 11 && spell.GetEntry() !== SPELL_ESSENCE_FUSION && spell.GetEntry() !== SPELL_ESSENCE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 12 && spell.GetEntry() !== SPELL_ESSENCE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 13 && spell.GetEntry() !== SPELL_COLD_FUSION && spell.GetEntry() !== SPELL_COLD_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 14 && spell.GetEntry() !== SPELL_COLD_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 15 && spell.GetEntry() !== SPELL_FLAME_FUSION && spell.GetEntry() !== SPELL_FLAME_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 16 && spell.GetEntry() !== SPELL_FLAME_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 17 && spell.GetEntry() !== SPELL_ARCANE_FUSION && spell.GetEntry() !== SPELL_ARCANE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 18 && spell.GetEntry() !== SPELL_ARCANE_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 19 && spell.GetEntry() !== SPELL_DARK_FUSION && spell.GetEntry() !== SPELL_DARK_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 20 && spell.GetEntry() !== SPELL_DARK_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 21 && spell.GetEntry() !== SPELL_EARTH_FUSION && spell.GetEntry() !== SPELL_EARTH_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + + if(materialId === 22 && spell.GetEntry() !== SPELL_EARTH_FUSION_RANK_2) { + player.PlayDirectSound(847); + return false; + } + +PrintInfo(`materialId: ${materialId} and spell: ${spell.GetEntry()}`); + + if(IsMythicFusionItem(fusedItemEntry)) { + if(target.GetCount() < MYTHIC_ITEM_REQUIREMENT) { + player.SendBroadcastMessage("You do not have enough materials to apply fusion."); + player.PlayDirectSound(847); + return false; + } + + + log.info(`Fusing ${target.GetName()}`); + + const random = Math.floor(Math.random() * 3) + 1; + player.PlayDirectSound(10720); + //player.AddItem(fusedItemEntry, random); + target.SetCount(target.GetCount() - MYTHIC_ITEM_REQUIREMENT); + return true; + } + + if(IsRareFusionItem(fusedItemEntry)) { + if(target.GetCount() < RARE_ITEM_REQUIREMENT) { + player.SendBroadcastMessage("You do not have enough materials to apply fusion"); + player.PlayDistanceSound(847); + return false; + } + + // If the target is a max stack then apply it to the entire stack for QoL reasons + let totalFused = 0; + let removeCount = 0; + if(target.GetCount() === MAX_STACK_SIZE) { + for(let i = 0; i < 10; i++) { + let random = Math.floor(Math.random() * 3) + 1; + totalFused += random; + } + removeCount = MAX_STACK_SIZE; + player.RemoveItem(target, MAX_STACK_SIZE); + } else { + totalFused = Math.floor(Math.random() * 3) + 1; + removeCount = RARE_ITEM_REQUIREMENT; + target.SetCount(target.GetCount() - RARE_ITEM_REQUIREMENT); + player.SaveToDB(); + player.RemoveItem(target, 1); + } + + player.PlayDirectSound(12334); + player.AddItem(fusedItemEntry, totalFused); + } + + return true; +}; + + + + // // Get the item + // const stackCount = GetTotalStackCount(item); + + // // Determine if this item can be fused + // // const fusedItemEntry = GetFusedItemFromItemEntry(itemEntry); + // if (!fusedItemEntry || fusedItemEntry === 0) { + // player.SendBroadcastMessage("This item cannot be fused."); + // return false; + // } + + // // Calculate number of batches (every 20 items) + // const batchSize = 20; + // const numBatches = Math.floor(stackCount / batchSize); + // if (numBatches === 0) { + // player.SendBroadcastMessage(`You need at least ${batchSize} items to fuse.`); + // return false; + // } + + // // Remove items from player's inventory + // item.Remove(batchSize * numBatches); + + // // For each batch, roll 1-3 and give that many fused items + // let totalFused = 0; + // for (let i = 0; i < numBatches; i++) { + // const amount = 1 + Math.floor(Math.random() * 3); // 1-3 + // player.AddItem(fusedItemEntry, amount); + // totalFused += amount; + // } + + // player.SendBroadcastMessage( + // `You fused ${numBatches * batchSize} items into ${totalFused} rare materials!` + // ); + +// Register +RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_SPELL_CAST, (...args) => MythicMaterialFusion(...args)); diff --git a/modules/UI/mythicplus/mythic_items.ts b/modules/UI/mythicplus/mythic_items.ts new file mode 100644 index 0000000..f4268e9 --- /dev/null +++ b/modules/UI/mythicplus/mythic_items.ts @@ -0,0 +1,172 @@ +// custom items for advancement +export const ANCIENT_DICE = 911000; +export const ONYX_SPIKE_RELIC = 911001; +export const VEILSTONE = 911002; +export const FUSED_RARE_ORE = 911003; +export const FUSED_MYTHIC_ORE = 911004; +export const FUSED_RARE_CLOTH = 911005; +export const FUSED_MYTHIC_CLOTH = 911006; +export const FUSED_RARE_LEATHER = 911007; +export const FUSED_MYTHIC_LEATHER = 911008; +export const FUSED_RARE_ALCHEMY = 911009; +export const FUSED_MYTHIC_ALCHEMY = 911010; +export const FUSED_RARE_GEM = 911011; +export const FUSED_MYTHIC_GEM = 911012; +export const FUSED_RARE_ESSENCE = 911013; +export const FUSED_MYTHIC_ESSENCE = 911014; +export const FUSED_RARE_ICE_STONE = 911015; +export const FUSED_MYTHIC_ICE_STONE = 911016; +export const FUSED_RARE_INFERNAL_STONE = 911017; +export const FUSED_MYTHIC_INFERNAL_STONE = 911018; +export const FUSED_RARE_ARCANE_CRYSTAL = 911019; +export const FUSED_MYTHIC_ARCANE_CRYSTAL = 911020; +export const FUSED_RARE_DARK_CRYSTAL = 911021; +export const FUSED_MYTHIC_DARK_CRYSTAL = 911022; +export const FUSED_RARE_EARTH_STONE = 911023; +export const FUSED_MYTHIC_EARTH_STONE = 911024; + +export const MYTHIC_MATERIALS = { + ANCIENT_DICE, + ONYX_SPIKE_RELIC, + VEILSTONE, + FUSED_RARE_ORE, + FUSED_MYTHIC_ORE, + FUSED_RARE_CLOTH, + FUSED_MYTHIC_CLOTH, + FUSED_RARE_LEATHER, + FUSED_MYTHIC_LEATHER, + FUSED_RARE_ALCHEMY, + FUSED_MYTHIC_ALCHEMY, + FUSED_RARE_GEM, + FUSED_MYTHIC_GEM, + FUSED_RARE_ESSENCE, + FUSED_MYTHIC_ESSENCE, + FUSED_RARE_ICE_STONE, + FUSED_MYTHIC_ICE_STONE, + FUSED_RARE_INFERNAL_STONE, + FUSED_MYTHIC_INFERNAL_STONE, + FUSED_RARE_ARCANE_CRYSTAL, + FUSED_MYTHIC_ARCANE_CRYSTAL, + FUSED_RARE_DARK_CRYSTAL, + FUSED_MYTHIC_DARK_CRYSTAL, + FUSED_RARE_EARTH_STONE, + FUSED_MYTHIC_EARTH_STONE +}; + +export const CategoryMapToFused: Record = { + 1: FUSED_RARE_CLOTH, + 2: FUSED_MYTHIC_CLOTH, + 3: FUSED_RARE_ALCHEMY, + 4: FUSED_MYTHIC_ALCHEMY, + 5: FUSED_RARE_ORE, + 6: FUSED_MYTHIC_ORE, + 7: FUSED_RARE_LEATHER, + 8: FUSED_MYTHIC_LEATHER, + 9: FUSED_RARE_GEM, + 10: FUSED_MYTHIC_GEM, + 11: FUSED_RARE_ESSENCE, + 12: FUSED_MYTHIC_ESSENCE, + 13: FUSED_RARE_ICE_STONE, + 14: FUSED_MYTHIC_ICE_STONE, + 15: FUSED_RARE_INFERNAL_STONE, + 16: FUSED_MYTHIC_INFERNAL_STONE, + 17: FUSED_RARE_ARCANE_CRYSTAL, + 18: FUSED_MYTHIC_ARCANE_CRYSTAL, + 19: FUSED_RARE_DARK_CRYSTAL, + 20: FUSED_MYTHIC_DARK_CRYSTAL, + 21: FUSED_RARE_EARTH_STONE, + 22: FUSED_MYTHIC_EARTH_STONE +}; + +export const RARE_FUSED_ITEMS: number[] = [ + FUSED_RARE_ORE, + FUSED_RARE_CLOTH, + FUSED_RARE_LEATHER, + FUSED_RARE_ALCHEMY, + FUSED_RARE_GEM, + FUSED_RARE_ESSENCE, + FUSED_RARE_ICE_STONE, + FUSED_RARE_INFERNAL_STONE, + FUSED_RARE_ARCANE_CRYSTAL, + FUSED_RARE_DARK_CRYSTAL, + FUSED_RARE_EARTH_STONE +]; + +export const MYTHIC_FUSED_ITEMS: number[] = [ + FUSED_MYTHIC_ORE, + FUSED_MYTHIC_CLOTH, + FUSED_MYTHIC_LEATHER, + FUSED_MYTHIC_ALCHEMY, + FUSED_MYTHIC_GEM, + FUSED_MYTHIC_ESSENCE, + FUSED_MYTHIC_ICE_STONE, + FUSED_MYTHIC_INFERNAL_STONE, + FUSED_MYTHIC_ARCANE_CRYSTAL, + FUSED_MYTHIC_DARK_CRYSTAL, + FUSED_MYTHIC_EARTH_STONE +]; + +// Populated after load with items mapped to fused counterparts. +// key is the material id, value is the item entries that can be used in fusion +export const FusedToItemMap: Record = {}; + +// create a lookup of item ids from the database to the fused items as the items in each category will +// produce that fused item when the fusion spell is cast on it. +const CreateItemToFusedMap: eluna_event_on_lua_state_open = (event: number) => { + const query = WorldDBQuery(`select materialId, entry, name from mp_material_types`); + if (query) { + do { + const materialId = query.GetUInt32(0); + const entry = query.GetUInt32(1); + + // Initialize the material group if it doesn't exist + if (!FusedToItemMap[materialId]) { + FusedToItemMap[materialId] = [entry]; + } else { + FusedToItemMap[materialId].push(entry); + } + + } while (query.NextRow()); + } + + PrintInfo(`Loaded ${Object.keys(FusedToItemMap).length} material groups and mapped to fused items`); + +} +RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_OPEN, (...args) => CreateItemToFusedMap(...args)); + +export function GetFusedItemFromItemEntry(itemEntry: number): number { + for (const [materialId, entries] of Object.entries(FusedToItemMap)) { + if (entries.includes(itemEntry)) { + return CategoryMapToFused[Number(materialId)]; + } + } + + return 0; +} + +// Get the category of the fused item +export function GetFusedItemMaterialId(itemEntry: number): number { + for (const [materialId, entries] of Object.entries(FusedToItemMap)) { + if (entries.includes(itemEntry)) { + return Number(materialId); + } + } + return 0; +} + +export function IsRareFusionItem(fusedItemEntry: number): boolean { + + PrintInfo(`looking up ${fusedItemEntry}`); + return (RARE_FUSED_ITEMS.includes(fusedItemEntry)) ? true : false; +} + +export function IsMythicFusionItem(fusedItemEntry: number): boolean { + return (MYTHIC_FUSED_ITEMS.includes(fusedItemEntry)) ? true : false; +} + +export function IsVeilstone(itemEntry: number): boolean { + if(itemEntry == VEILSTONE) { + return true; + } + return false; +} diff --git a/modules/UI/mythicplus/mythic_npcs.ts b/modules/UI/mythicplus/mythic_npcs.ts new file mode 100644 index 0000000..3b9d9e6 --- /dev/null +++ b/modules/UI/mythicplus/mythic_npcs.ts @@ -0,0 +1,444 @@ +/** @ts-expect-error */ +let aio: AIO = {}; + +import { MapNames, MapIds, BossIDs } from "../../classes/mapzones"; +import * as Spells from "./mythic_custom_spells"; +/** +* This is the file for managing new NPC interactions + * - Mick Ashwild - 9500561 teaches leather/fire fusions + * - Thorin Firehand - 9500562 teaches ore/cold fusions + * - Elowyn Threadbinder - 9500563 teaches cloth/arcane fusions + * - Shivey - 9500564 teaches alchemy/nature fusions + * - ??? - 9500565 teaches gem/essence fusions + * - ??? - Old Witch teaches shadow fusion + * + */ + +export enum NPCType { + MICK_ASHWILD = 'mick', + THORIN_FIREHAND = 'thorin', + ELOWYN_THREADBINDER = 'elowyn', + SHIVEY = 'shivey', + OLD_WITCH = 'old_witch' +} + +export const NPCIds: Record = { + [NPCType.MICK_ASHWILD]: 9500561, + [NPCType.THORIN_FIREHAND]: 9500562, + [NPCType.ELOWYN_THREADBINDER]: 9500563, + [NPCType.SHIVEY]: 9500564, + [NPCType.OLD_WITCH]: 9500565 +}; + +export enum GobjectType { + PORTAL = 'portal', + +} + +export const GobjectIds: Record = { + [GobjectType.PORTAL]: 181508 +} + +const AUDIO_BASE_PATH = "Interface\\Modules\\MythicPlus\\Audio\\"; + +export const AudioPaths: Record = { + [NPCType.MICK_ASHWILD]: AUDIO_BASE_PATH + "Mick\\mick-", + [NPCType.THORIN_FIREHAND]: AUDIO_BASE_PATH + "Thorin\\thorin-", + [NPCType.ELOWYN_THREADBINDER]: AUDIO_BASE_PATH + "Elowyn\\elowyn-", + [NPCType.SHIVEY]: AUDIO_BASE_PATH + "Shivey\\shivey-", + [NPCType.OLD_WITCH]: AUDIO_BASE_PATH + "OldWitch\\old-witch-" +}; + +function getAudioFile(npcName: NPCType, file: string): string { + return AudioPaths[npcName] + file + ".mp3"; +} + +function isInMythicPlus(playerId: number, instanceId: number): boolean { + const result = CharDBQuery("select guid, instanceId, difficulty from mp_player_instance_data where guid = " + playerId + " and instanceId = " + instanceId); + if(result && result.GetUInt32(2) >= 3) { + return true; + } + return false; +} + +/** + * the player will meet different characters at different times so the hello will check to see whch + * audio file and gossip menu needs to be shown. The list of locations an npc will show up is below + * + * Mick + * Rare Leather Fusion : Wailing Caverns Verdan the everliving + * Mythic Leather Fusion : Sartharion Obsidian Sanctum + * Rare Fire Fusion : Hellfire Ramparts Nazan + Vazruden + * Mythic Fire Fusion : Onyxia Lair Flamegor + * Casual Spawns : World ends Tavern Shatrath + * + * Thorin + * Rare Ore Fusion : BRD Magmus + * Mythic Ore Fusion : Ragnaros + * Casual Spawns : The Great Forge + + * + */ + + +// Track NPC dialog animation state +interface ActiveNpcState { + time: number; + intro: boolean; + audioActive: boolean; + players?: string[]; // list of players nearby when the NPC spawns + lastEmote?: number; + outro?: string[]; // list of players that have heard the outro +} + +interface PlayedAudioState { + playerName: string; + audioFile: string; + played: boolean; +} + +// Track the ai update loop for each instance of a NPC which +let npcState: Record = {}; + +// Create emote maps for npcs while they are talking keys are seconds into audio and values are emote types +const emotesMap: Record> = {}; + +// Track the audio that has been played for each for this instance id. +// key: InstanceId value: playerId: audioFile +let playedAudio: Record> = {}; + +// Mick + WC +emotesMap[`${NPCIds[NPCType.MICK_ASHWILD]}-${MapIds[MapNames.WAILING_CAVERNS]}`] = { + 1: EmoteType.STATE_TALK, + 6: EmoteType.STATE_EXCLAIM, + 8: EmoteType.STATE_TALK, + 10: EmoteType.ONESHOT_NONE, + 12: EmoteType.STATE_POINT, + 14: EmoteType.ONESHOT_NONE, + 15: EmoteType.STATE_TALK, + 26: EmoteType.STATE_EXCLAIM, + 28: EmoteType.STATE_TALK, + 35: EmoteType.ONESHOT_EXCLAMATION, + 37: EmoteType.STATE_TALK, + 41: EmoteType.STATE_POINT, + 43: EmoteType.ONESHOT_NONE +}; + +// Spell to spell MapId +const spellToMap: Record = { + [MapIds[MapNames.WAILING_CAVERNS]]: Spells.SPELL_LEATHER_FUSION +}; + +// Does this player already know what the special NPC is offering. +function PlayerHasSpell(player: Player): boolean { + const mapId = player.GetMapId(); + if(spellToMap[mapId]) { + return player.HasSpell(spellToMap[mapId]); + } + return false; +} + + +// This will prevent audio from being played more than once for the same player using a global map +function PlayAudioOnce(player: Player, audioFile: string): void { + const instanceId = player.GetInstanceId(); + + if(!playedAudio[instanceId]) { + playedAudio[instanceId] = {}; + } + // If nothing has been played to the player create the audio entry + if(!playedAudio[instanceId][player.GetName()]) { + playedAudio[instanceId][player.GetName()] = []; + playedAudio[instanceId][player.GetName()].push(audioFile); + PrintDebug(`Playing audio ${audioFile} for player ${player.GetName()} in instance ${instanceId}`); + aio.Handle(player, 'AIOAudioPlayer', 'PlaySingleSound', audioFile); + return; + } + + if(playedAudio[instanceId][player.GetName()].includes(audioFile)) { + return; + } + + playedAudio[instanceId][player.GetName()].push(audioFile); + aio.Handle(player, 'AIOAudioPlayer', 'PlaySingleSound', audioFile); + +} + +// Get the instance state of the NPC for this group and instance. +function GetNPCState(creature: Creature): ActiveNpcState { + const instanceId = creature.GetInstanceId(); + const creatureGuid = creature.GetGUIDLow(); + let state = npcState[`${instanceId}-${creatureGuid}`]; + + if(!state) { + state = { + time: 0, + intro: false, + outro: [], + audioActive: false, + players: [] + }; + npcState[`${instanceId}-${creatureGuid}`] = state; + } + return state; +} + + +/** + * Handle AI Dialogs for Mick and others. + */ +const handleMickAIUpdates: creature_event_on_aiupdate = (event: number, creature: Creature, diff: number): boolean => { + + const creatureGuid = creature.GetGUIDLow(); + const instanceId = creature.GetInstanceId(); + const state = npcState[`${instanceId}-${creatureGuid}`]; + const previousTime = state.time || 0; + + state.time = (previousTime + diff); + const seconds = Math.ceil(state.time / 1000); + + const npcEmoteMap = emotesMap[`${creature.GetEntry()}-${creature.GetMapId()}`]; + if(npcEmoteMap && npcEmoteMap[seconds] && npcEmoteMap[seconds] !== state.lastEmote) { + + PrintDebug(`Mick player state: ${state.players.length}`); + creature.EmoteState(npcEmoteMap[seconds]); + state.lastEmote = npcEmoteMap[seconds]; + } + + // Handle the AI Updates for Wailing Caverns + if(MapIds[MapNames.WAILING_CAVERNS] === creature.GetMapId()) { + if(seconds >= 43) { + ClearUniqueCreatureEvents(creature.GetGUID(), instanceId); + state.audioActive = false; + state.intro = true; + for(let i=0; i < state.players.length; i++) { + const playerId = state.players[i]; + + if(!playerId) { + PrintError(`${creature.GetName()} gossip menu failed no valid player in state`); + continue; + } + const player = GetPlayerByName(playerId); + if(player) { + npcHello(player, creature, player.GetMapId()); + } + } + + } + } + + return true; +} + +/** + * the player will meet different characters at different times so the hello will check to see whch + * audio file and gossip menu needs to be shown. The list of locations an npc will show up is below + * + * Mick + * Rare Leather Fusion : Wailing Caverns Verdan the everliving + * Mythic Leather Fusion : Sartharion Obsidian Sanctum + * Rare Fire Fusion : Hellfire Ramparts Nazan + Vazruden + * Mythic Fire Fusion : Onyxia Lair Flamegor + * Casual Spawns : World ends Tavern Shatrath + * + * Thorin + * Rare Ore Fusion : BRD Magmus + * Mythic Ore Fusion : Ragnaros + * Casual Spawns : The Great Forge + + * + */ + +// When verdan dies Mike should show phase in from a portal +const verdanDied: creature_event_on_died = (event: number, creature: Creature, killer: Creature): boolean => { + + // Spawn the portal and Mick + creature.SummonGameObject(GobjectIds[GobjectType.PORTAL], -79.273, 4.999, -30.962, 2.20); + const mick = creature.SpawnCreature(NPCIds[NPCType.MICK_ASHWILD], -79.273, 4.999, -30.962, 2.20, TempSummonType.TEMPSUMMON_TIMED_DESPAWN, 1200000); + mick.SetWalk(true); + mick.MoveTo(1, -83.252, 19.723, -31.076); + + return false; // return false to continue normal action +}; + +function npcHello(player: Player, creature: Creature, mapId: number, known?: boolean): void { + player.GossipClearMenu(); + PrintDebug("sending menu to player: " + player.GetName()); + + if(mapId === MapIds[MapNames.WAILING_CAVERNS]) { + if(known) { + player.GossipMenuAddItem(0, "Got nuthin' for ya friend.", 1, 999); + player.GossipSendMenu(1, creature, 90000); + return; + } + + player.GossipMenuAddItem(3, "Learn Leather Fusion (requires grandmaster)",1, Spells.SPELL_LEATHER_FUSION); + player.GossipMenuAddItem(0, "Best to come another time", 1, 999); + } + + player.GossipSendMenu(1, creature, 90000); +} + +const mickSelect: gossip_event_on_select = (event: number, player: Player, creature: Creature, sender: number, selection: number): boolean => { + + const state = GetNPCState(creature); + + PrintDebug(`Player ${player.GetName()} selected ${selection}`); + + // 999 is a signal nothing to do + if(selection === 999) { + // play the outro audio for the player if they are done. + if(!state.outro[player.GetName()]) { + aio.Handle(player, 'AIOAudioPlayer', 'PlaySingleSound', getAudioFile(NPCType.MICK_ASHWILD, "rare-goodbye")); + state.outro[player.GetName()] = true; + } + player.GossipClearMenu(); + player.GossipComplete(); + return true; + } + + // Check the range of the spell we are trying to learn to make sure there is not a problem. + if(selection < Spells.SPELL_ORE_FUSION || selection > Spells.SPELL_EARTH_FUSION_RANK_2) { + PrintError(`The selection ${selection} is not in the range of learnable spells check coding!!`); + return true; + } + PrintDebug(`Learning spell ${selection} for player ${player.GetName()}`); + PrintDebug(`Player skill ${player.GetSkillValue(165)}`); + + switch(player.GetMapId()) { + + case MapIds[MapNames.WAILING_CAVERNS]: + if(player.GetSkillValue(165) == 450) { // if they are a grandmaster leather worker. + player.LearnSpell(selection); + + PrintDebug(`Learning spell ${selection} for player ${player.GetName()}`); + PlayAudioOnce(player, getAudioFile(NPCType.MICK_ASHWILD, "teach-rare-yes")); + } else { + PrintDebug(`FAILING ${selection} for player ${player.GetName()}`); + PlayAudioOnce(player, getAudioFile(NPCType.MICK_ASHWILD, "teach-rare-no")); + } + break; + + default: + break; + } + + player.GossipComplete(); + return true; +} + +// Micks events +const mickHello: gossip_event_on_hello = (event: number, player: Player, creature: Creature): boolean => { + const zoneId = player.GetMapId(); + const instanceId = player.GetInstanceId(); + + // Get the global creature state for special NPCs + const myState = GetNPCState(creature); + + // if(isInMythicPlus(player.GetGUID(), instanceId)) { + if (MapIds[MapNames.WAILING_CAVERNS] === zoneId) { + + creature.SetFacingToObject(player); + + // Do play outro dialog if they already know the spell. + if(PlayerHasSpell(player)) { + npcHello(player, creature, zoneId, true); + if(!myState.outro[player.GetName()]) { + PlayAudioOnce(player, getAudioFile(NPCType.MICK_ASHWILD, "rare-goodbye")); + myState.outro[player.GetName()] = true; + } + return true; + } + + // Audio should play for all players in range when the NPC spawns.. + // however the menu will show up only to the player that clicked hello, + // or other players after the audio has played. + if (!myState.audioActive) { + + + const nearPlayers = creature.GetPlayersInRange(35); + for (let i = 0; i < nearPlayers.length; i++) { + myState.players.push(nearPlayers[i].GetName()); + PlayAudioOnce(player, getAudioFile(NPCType.MICK_ASHWILD, "rare-hello")); + } + + creature.EmoteState(EmoteType.ONESHOT_NONE); + myState.audioActive = true; + ClearUniqueCreatureEvents(creature.GetGUID(), instanceId, CreatureEvents.CREATURE_EVENT_ON_AIUPDATE); + RegisterUniqueCreatureEvent(creature.GetGUID(), instanceId, CreatureEvents.CREATURE_EVENT_ON_AIUPDATE, (...args) => handleMickAIUpdates(...args)); + } else { + myState.players.push(player.GetName()); // player wants to interact with the NPC + if(myState.players.length === 1 && myState.intro) { + npcHello(player, creature, player.GetMapId()); + } + } + + } + return true; +}; + +/**** Boss Kill Handlers ****/ + +// Wailing Caverns - Verdan the Everliving is killed +RegisterCreatureEvent(5775, CreatureEvents.CREATURE_EVENT_ON_DIED, (...args) => verdanDied(...args)); + +/**** NPC Gossip Events Handlers ****/ + +// Mick Ashwild +RegisterCreatureGossipEvent(NPCIds[NPCType.MICK_ASHWILD], GossipEvents.GOSSIP_EVENT_ON_HELLO, (...args) => mickHello(...args)); +RegisterCreatureGossipEvent(NPCIds[NPCType.MICK_ASHWILD], GossipEvents.GOSSIP_EVENT_ON_SELECT, (...args) => mickSelect(...args)); + + +/**** NPC Events Handlers ****/ + +// Registers the creature state to our instance state map and sets default state values. +function commonCreatureRegister(creature: Creature) { + ClearUniqueCreatureEvents(creature.GetGUID(), creature.GetInstanceId()); + npcState[`${creature.GetInstanceId()}-${creature.GetGUIDLow()}`] = { + time: 0, + intro: false, + outro: [], + audioActive: false, + players: [] + }; +} + +// Micks Events +RegisterCreatureEvent(NPCIds[NPCType.MICK_ASHWILD], CreatureEvents.CREATURE_EVENT_ON_SPAWN, (event: number, creature: Creature): boolean => { + commonCreatureRegister(creature); + return false; +}); +RegisterCreatureEvent(NPCIds[NPCType.MICK_ASHWILD], CreatureEvents.CREATURE_EVENT_ON_REMOVE, (event: number, creature: Creature): boolean => { + commonCreatureRegister(creature); + return false; +}); + +/**** Server events for state management around instances ****/ + +const cleanupAudio: map_event_on_destroy = (event: number, map: EMap) => { + playedAudio[map.GetInstanceId()] = {}; +}; + +// Register Map Event on Destroy +RegisterServerEvent(ServerEvents.MAP_EVENT_ON_DESTROY, (...args) => cleanupAudio(...args)); + +const createPlayedAudio: map_event_on_destroy = (event: number, map: EMap) => { + playedAudio[map.GetInstanceId()] = {}; +}; + +// Register Map Event on Destroy +RegisterServerEvent(ServerEvents.MAP_EVENT_ON_DESTROY, (...args) => createPlayedAudio(...args)); + +const resetPlayedAudio: eluna_event_on_lua_state_open = (event: number) => { + PrintDebug("resetting played audio"); + playedAudio = {}; +}; +const resetPlayedAudioClose: eluna_event_on_lua_state_close = (event: number) => { + PrintDebug("resetting played audio"); + playedAudio = {}; +} + + +// Register Server Event on Lua State Open +RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_OPEN, (...args) => resetPlayedAudio(...args)); +RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_CLOSE, (...args) => resetPlayedAudioClose(...args)); \ No newline at end of file diff --git a/modules/classes/logger.ts b/modules/classes/logger.ts index fe54636..9eb742f 100644 --- a/modules/classes/logger.ts +++ b/modules/classes/logger.ts @@ -1,4 +1,5 @@ // Purpose: Logger class to log messages to the console. + export class Logger { public logname: string; diff --git a/modules/classes/mapzones.ts b/modules/classes/mapzones.ts new file mode 100644 index 0000000..d556f1d --- /dev/null +++ b/modules/classes/mapzones.ts @@ -0,0 +1,536 @@ +import { Logger } from "./logger"; + +const logger = new Logger("MapZones"); + +export enum MapNames { + // Classic WoW dungeons + RAGEFIRE_CHASM = 'ragefire_chasm', + WAILING_CAVERNS = 'wailing_caverns', + DEADMINES = 'deadmines', + SHADOWFANG_KEEP = 'shadowfang_keep', + STOCKADE = 'stockade', + BLACKFATHOM_DEEPS = 'blackfathom_deeps', + GNOMEREGAN = 'gnomeregan', + RAZORFEN_KRAUL = 'razorfen_kraul', + SCARLET_MONASTERY = 'scarlet_monastery', + SCHOLOMANCE = 'scholomance', + SUNKEN_TEMPLE = 'sunken_temple', + RAZORFEN_DOWNS = 'razorfen_downs', + ULDAMAN = 'uldaman', + STRATHOLME = 'stratholme', + BLACKROCK_SPIRE_LOWER = 'blackrock_spire_lower', + BLACKROCK_SPIRE_UPPER = 'blackrock_spire_upper', + DIRE_MAUL = 'dire_maul', + ZUL_FARRAK = 'zul_farrak', + MARAUDON = 'maraudon', + TEMPLE_ATAL_HAKKAR = 'temple_atal_hakkar', + BLACKROCK_DEPTHS = 'blackrock_depths', + + // Classic WoW raids + MOLTEN_CORE = 'molten_core', + BLACKWING_LAIR = 'blackwing_lair', + RUINS_OF_AHNQIRAJ = 'ruins_of_ahnqiraj', + TEMPLE_OF_AHNQIRAJ = 'temple_of_ahnqiraj', + ZUL_GURUB = 'zul_gurub', + ONYXIAS_LAIR = 'onyxias_lair', + EMERALD_DREAM = 'emerald_dream', + + // The Burning Crusade dungeons + SHATTERED_HALLS = 'shattered_halls', + BLOOD_FURNACE = 'blood_furnace', + HELLFIRE_RAMPARTS = 'hellfire_ramparts', + STEAMVAULTS = 'steamvaults', + UNDERBOG = 'underbog', + SLAVE_PENS = 'slave_pens', + MANA_TOMBS = 'mana_tombs', + AUCHENAI_CRYPTS = 'auchenai_crypts', + SETHEKK_HALLS = 'sethekk_halls', + SHADOW_LABYRINTH = 'shadow_labyrinth', + OLD_HILLSBRAD = 'old_hillsbrad', + BOTANICA = 'botanica', + MECHANAR = 'mechanar', + ARCATRAZ = 'arcatraz', + MAGISTERS_TERRACE = 'magisters_terrace', + BLACK_MORASS = 'black_morass', + + // The Burning Crusade raids + KARAZHAN = 'karazhan', + GRUULS_LAIR = 'gruuls_lair', + MAGTHERIDONS_LAIR = 'magtheridons_lair', + SERPENTSHRINE_CAVERN = 'serpentshrine_cavern', + TEMPEST_KEEP = 'tempest_keep', + MOUNT_HYJAL = 'mount_hyjal', + BLACK_TEMPLE = 'black_temple', + SUNWELL_PLATEAU = 'sunwell_plateau', + ZUL_AMAN = 'zul_aman', + + // Wrath of the Lich King dungeons + UTGARDE_KEEP = 'utgarde_keep', + UTGARDE_PINNACLE = 'utgarde_pinnacle', + AHNKAHET = 'ahnkahet', + NEXUS = 'nexus', + CULLING_STRATHOLME = 'culling_stratholme', + DRAK_THARON = 'drak_tharon', + AZJOL_NERUB = 'azjol_nerub', + VIOLET_HOLD = 'violet_hold', + GUNDRAK = 'gundrak', + HALLS_OF_STONE = 'halls_of_stone', + HALLS_OF_LIGHTNING = 'halls_of_lightning', + OCULUS = 'oculus', + TRIAL_OF_CHAMPION = 'trial_of_champion', + FORGE_OF_SOULS = 'forge_of_souls', + PIT_OF_SARON = 'pit_of_saron', + HALLS_OF_REFLECTION = 'halls_of_reflection', + + // Wrath of the Lich King raids + NAXXRAMAS = 'naxxramas', + OBSIDIAN_SANCTUM = 'obsidian_sanctum', + EYE_OF_ETERNITY = 'eye_of_eternity', + ULDUAR = 'ulduar', + TRIAL_OF_CRUSADER = 'trial_of_crusader', + VAULT_OF_ARCHAVON = 'vault_of_archavon', + ICECROWN_CITADEL = 'icecrown_citadel', + RUBY_SANCTUM = 'ruby_sanctum' +} + +export const MapIds: Record = { + // Classic WoW dungeons + [MapNames.RAGEFIRE_CHASM]: 389, + [MapNames.WAILING_CAVERNS]: 43, + [MapNames.DEADMINES]: 36, + [MapNames.SHADOWFANG_KEEP]: 33, + [MapNames.STOCKADE]: 34, + [MapNames.BLACKFATHOM_DEEPS]: 48, + [MapNames.GNOMEREGAN]: 90, + [MapNames.RAZORFEN_KRAUL]: 47, + [MapNames.SCARLET_MONASTERY]: 189, + [MapNames.SCHOLOMANCE]: 289, + [MapNames.SUNKEN_TEMPLE]: 109, + [MapNames.RAZORFEN_DOWNS]: 129, + [MapNames.ULDAMAN]: 70, + [MapNames.STRATHOLME]: 329, + [MapNames.BLACKROCK_SPIRE_LOWER]: 229, + [MapNames.BLACKROCK_SPIRE_UPPER]: 230, + [MapNames.DIRE_MAUL]: 429, + [MapNames.ZUL_FARRAK]: 209, + [MapNames.MARAUDON]: 349, + [MapNames.TEMPLE_ATAL_HAKKAR]: 269, + [MapNames.BLACKROCK_DEPTHS]: 230, + + // Classic WoW raids + [MapNames.MOLTEN_CORE]: 409, + [MapNames.BLACKWING_LAIR]: 469, + [MapNames.RUINS_OF_AHNQIRAJ]: 509, + [MapNames.TEMPLE_OF_AHNQIRAJ]: 531, + [MapNames.ZUL_GURUB]: 309, + [MapNames.ONYXIAS_LAIR]: 249, + [MapNames.EMERALD_DREAM]: 169, + + // The Burning Crusade dungeons + [MapNames.SHATTERED_HALLS]: 540, + [MapNames.BLOOD_FURNACE]: 542, + [MapNames.HELLFIRE_RAMPARTS]: 543, + [MapNames.STEAMVAULTS]: 545, + [MapNames.UNDERBOG]: 546, + [MapNames.SLAVE_PENS]: 547, + [MapNames.MANA_TOMBS]: 557, + [MapNames.AUCHENAI_CRYPTS]: 558, + [MapNames.SETHEKK_HALLS]: 556, + [MapNames.SHADOW_LABYRINTH]: 555, + [MapNames.OLD_HILLSBRAD]: 560, + [MapNames.BOTANICA]: 553, + [MapNames.MECHANAR]: 554, + [MapNames.ARCATRAZ]: 552, + [MapNames.MAGISTERS_TERRACE]: 585, + [MapNames.BLACK_MORASS]: 269, + + // The Burning Crusade raids + [MapNames.KARAZHAN]: 532, + [MapNames.GRUULS_LAIR]: 565, + [MapNames.MAGTHERIDONS_LAIR]: 544, + [MapNames.SERPENTSHRINE_CAVERN]: 548, + [MapNames.TEMPEST_KEEP]: 550, + [MapNames.MOUNT_HYJAL]: 534, + [MapNames.BLACK_TEMPLE]: 564, + [MapNames.SUNWELL_PLATEAU]: 580, + [MapNames.ZUL_AMAN]: 568, + + // Wrath of the Lich King dungeons + [MapNames.UTGARDE_KEEP]: 574, + [MapNames.UTGARDE_PINNACLE]: 575, + [MapNames.AHNKAHET]: 619, + [MapNames.NEXUS]: 576, + [MapNames.CULLING_STRATHOLME]: 595, + [MapNames.DRAK_THARON]: 600, + [MapNames.AZJOL_NERUB]: 601, + [MapNames.VIOLET_HOLD]: 608, + [MapNames.GUNDRAK]: 604, + [MapNames.HALLS_OF_STONE]: 599, + [MapNames.HALLS_OF_LIGHTNING]: 602, + [MapNames.OCULUS]: 578, + [MapNames.TRIAL_OF_CHAMPION]: 650, + [MapNames.FORGE_OF_SOULS]: 632, + [MapNames.PIT_OF_SARON]: 658, + [MapNames.HALLS_OF_REFLECTION]: 668, + + // Wrath of the Lich King raids + [MapNames.NAXXRAMAS]: 533, + [MapNames.OBSIDIAN_SANCTUM]: 615, + [MapNames.EYE_OF_ETERNITY]: 616, + [MapNames.ULDUAR]: 603, + [MapNames.TRIAL_OF_CRUSADER]: 649, + [MapNames.VAULT_OF_ARCHAVON]: 624, + [MapNames.ICECROWN_CITADEL]: 631, + [MapNames.RUBY_SANCTUM]: 724 +}; + +export const DungeonLevels: Record = { + // Classic WoW dungeons + [MapNames.RAGEFIRE_CHASM]: 18, + [MapNames.WAILING_CAVERNS]: 25, + [MapNames.DEADMINES]: 23, + [MapNames.SHADOWFANG_KEEP]: 30, + [MapNames.STOCKADE]: 30, + [MapNames.BLACKFATHOM_DEEPS]: 32, + [MapNames.GNOMEREGAN]: 38, + [MapNames.RAZORFEN_KRAUL]: 40, + [MapNames.SCARLET_MONASTERY]: 45, + [MapNames.SCHOLOMANCE]: 60, + [MapNames.SUNKEN_TEMPLE]: 60, + [MapNames.RAZORFEN_DOWNS]: 33, + [MapNames.ULDAMAN]: 40, + [MapNames.STRATHOLME]: 60, + [MapNames.BLACKROCK_SPIRE_LOWER]: 60, + [MapNames.BLACKROCK_SPIRE_UPPER]: 60, + [MapNames.DIRE_MAUL]: 60, + [MapNames.ZUL_FARRAK]: 50, + [MapNames.MARAUDON]: 55, + [MapNames.TEMPLE_ATAL_HAKKAR]: 57, + [MapNames.BLACKROCK_DEPTHS]: 60, + + // Classic WoW raids + [MapNames.MOLTEN_CORE]: 60, + [MapNames.BLACKWING_LAIR]: 60, + [MapNames.RUINS_OF_AHNQIRAJ]: 60, + [MapNames.TEMPLE_OF_AHNQIRAJ]: 60, + [MapNames.ZUL_GURUB]: 60, + [MapNames.ONYXIAS_LAIR]: 60, + [MapNames.EMERALD_DREAM]: 60, + + // The Burning Crusade dungeons + [MapNames.SHATTERED_HALLS]: 70, + [MapNames.BLOOD_FURNACE]: 65, + [MapNames.HELLFIRE_RAMPARTS]: 62, + [MapNames.STEAMVAULTS]: 64, + [MapNames.UNDERBOG]: 65, + [MapNames.SLAVE_PENS]: 64, + [MapNames.MANA_TOMBS]: 66, + [MapNames.AUCHENAI_CRYPTS]: 67, + [MapNames.SETHEKK_HALLS]: 70, + [MapNames.SHADOW_LABYRINTH]: 70, + [MapNames.OLD_HILLSBRAD]: 68, + [MapNames.BOTANICA]: 70, + [MapNames.MECHANAR]: 70, + [MapNames.ARCATRAZ]: 70, + [MapNames.MAGISTERS_TERRACE]: 70, + [MapNames.BLACK_MORASS]: 70, + + // The Burning Crusade raids + [MapNames.KARAZHAN]: 70, + [MapNames.GRUULS_LAIR]: 70, + [MapNames.MAGTHERIDONS_LAIR]: 70, + [MapNames.SERPENTSHRINE_CAVERN]: 70, + [MapNames.TEMPEST_KEEP]: 70, + [MapNames.MOUNT_HYJAL]: 70, + [MapNames.BLACK_TEMPLE]: 70, + [MapNames.SUNWELL_PLATEAU]: 70, + [MapNames.ZUL_AMAN]: 70, + + // Wrath of the Lich King dungeons + [MapNames.UTGARDE_KEEP]: 72, + [MapNames.UTGARDE_PINNACLE]: 76, + [MapNames.AHNKAHET]: 75, + [MapNames.NEXUS]: 73, + [MapNames.CULLING_STRATHOLME]: 80, + [MapNames.DRAK_THARON]: 76, + [MapNames.AZJOL_NERUB]: 75, + [MapNames.VIOLET_HOLD]: 77, + [MapNames.GUNDRAK]: 78, + [MapNames.HALLS_OF_STONE]: 78, + [MapNames.HALLS_OF_LIGHTNING]: 80, + [MapNames.OCULUS]: 78, + [MapNames.TRIAL_OF_CHAMPION]: 80, + [MapNames.FORGE_OF_SOULS]: 80, + [MapNames.PIT_OF_SARON]: 80, + [MapNames.HALLS_OF_REFLECTION]: 80, + + // Wrath of the Lich King raids + [MapNames.NAXXRAMAS]: 80, + [MapNames.OBSIDIAN_SANCTUM]: 80, + [MapNames.EYE_OF_ETERNITY]: 80, + [MapNames.ULDUAR]: 80, + [MapNames.TRIAL_OF_CRUSADER]: 80, + [MapNames.VAULT_OF_ARCHAVON]: 80, + [MapNames.ICECROWN_CITADEL]: 80, + [MapNames.RUBY_SANCTUM]: 80 +}; + +// Boss info +export type Boss = { + entry: number; + name: string; + location: string; + isFinalBoss?: boolean; +} + +export const Bosses: Boss[] = [ + // Classic WoW dungeons + { entry: 11520, name: "Taragaman the Hungerer", location: "Ragefire Chasm" }, + { entry: 3654, name: "Mutanus the Devourer", location: "Wailing Caverns", isFinalBoss: true }, + { entry: 639, name: "Edwin VanCleef", location: "The Deadmines", isFinalBoss: true }, + { entry: 4275, name: "Archmage Arugal", location: "Shadowfang Keep", isFinalBoss: true }, + { entry: 4829, name: "Aku'mai", location: "Blackfathom Deeps", isFinalBoss: true }, + { entry: 1716, name: "Bazil Thredd", location: "Stormwind Stockade", isFinalBoss: true }, + { entry: 7800, name: "Mekgineer Thermaplugg", location: "Gnomeregan", isFinalBoss: true }, + { entry: 4421, name: "Charlga Razorflank", location: "Razorfen Kraul", isFinalBoss: true }, + { entry: 4543, name: "Bloodmage Thalnos", location: "Scarlet Monastery Graveyard", isFinalBoss: true }, + { entry: 6487, name: "Arcanist Doan", location: "Scarlet Monastery Library", isFinalBoss: true }, + { entry: 3975, name: "Herod", location: "Scarlet Monastery Armory", isFinalBoss: true }, + { entry: 3977, name: "High Inquisitor Whitemane", location: "Scarlet Monastery Cathedral", isFinalBoss: true }, + { entry: 7358, name: "Amnennar the Coldbringer", location: "Razorfen Downs", isFinalBoss: true }, + { entry: 2748, name: "Archaedas", location: "Uldaman", isFinalBoss: true }, + { entry: 7267, name: "Chief Ukorz Sandscalp", location: "Zul'Farrak", isFinalBoss: true }, + { entry: 12201, name: "Princess Theradras", location: "Maraudon", isFinalBoss: true }, + { entry: 8443, name: "Avatar of Hakkar", location: "Sunken Temple", isFinalBoss: true }, + { entry: 9019, name: "Emperor Dagran Thaurissan", location: "Blackrock Depths", isFinalBoss: true }, + { entry: 9568, name: "Overlord Wyrmthalak", location: "Lower Blackrock Spire", isFinalBoss: true }, + { entry: 10363, name: "General Drakkisath", location: "Upper Blackrock Spire", isFinalBoss: true }, + { entry: 11492, name: "Alzzin the Wildshaper", location: "Dire Maul East", isFinalBoss: true }, + { entry: 11489, name: "Tendris Warpwood", location: "Dire Maul West", isFinalBoss: true }, + { entry: 11501, name: "King Gordok", location: "Dire Maul North", isFinalBoss: true }, + { entry: 10440, name: "Baron Rivendare", location: "Stratholme Undead Side", isFinalBoss: true }, + { entry: 10813, name: "Balnazzar", location: "Stratholme Live Side", isFinalBoss: true }, + { entry: 1853, name: "Darkmaster Gandling", location: "Scholomance", isFinalBoss: true }, + + // The Burning Crusade dungeons + { entry: 17307, name: "Vazruden", location: "Hellfire Ramparts" }, + { entry: 17536, name: "Nazan", location: "Hellfire Ramparts", isFinalBoss: true }, + { entry: 17377, name: "Keli'dan the Breaker", location: "The Blood Furnace", isFinalBoss: true }, + { entry: 16808, name: "Warchief Kargath Bladefist", location: "The Shattered Halls", isFinalBoss: true }, + { entry: 17942, name: "Quagmirran", location: "The Slave Pens", isFinalBoss: true }, + { entry: 17826, name: "Swamplord Musel'ek", location: "The Underbog", isFinalBoss: true }, + { entry: 17798, name: "Warlord Kalithresh", location: "The Steamvault", isFinalBoss: true }, + { entry: 18344, name: "Nexus-Prince Shaffar", location: "Mana-Tombs", isFinalBoss: true }, + { entry: 18373, name: "Exarch Maladaar", location: "Auchenai Crypts", isFinalBoss: true }, + { entry: 18473, name: "Talon King Ikiss", location: "Sethekk Halls", isFinalBoss: true }, + { entry: 18708, name: "Murmur", location: "Shadow Labyrinth", isFinalBoss: true }, + { entry: 19220, name: "Pathaleon the Calculator", location: "The Mechanar", isFinalBoss: true }, + { entry: 17977, name: "Warp Splinter", location: "The Botanica", isFinalBoss: true }, + { entry: 20912, name: "Harbinger Skyriss", location: "The Arcatraz", isFinalBoss: true }, + { entry: 17881, name: "Aeonus", location: "The Black Morass", isFinalBoss: true }, + { entry: 18096, name: "Epoch Hunter", location: "Old Hillsbrad Foothills", isFinalBoss: true }, + { entry: 24664, name: "Kael'thas Sunstrider", location: "Magisters' Terrace", isFinalBoss: true }, + + // The Burning Crusade raids + { entry: 22887, name: "Attumen the Huntsman", location: "Karazhan" }, + { entry: 22888, name: "Moroes", location: "Karazhan" }, + { entry: 22889, name: "Maiden of Virtue", location: "Karazhan" }, + { entry: 22890, name: "The Big Bad Wolf", location: "Karazhan" }, + { entry: 22891, name: "The Crone", location: "Karazhan" }, + { entry: 22892, name: "Romulo", location: "Karazhan" }, + { entry: 22893, name: "Julianne", location: "Karazhan" }, + { entry: 22894, name: "The Curator", location: "Karazhan" }, + { entry: 22895, name: "Terestian Illhoof", location: "Karazhan" }, + { entry: 22896, name: "Shade of Aran", location: "Karazhan" }, + { entry: 22897, name: "Netherspite", location: "Karazhan" }, + { entry: 22898, name: "Chess Event", location: "Karazhan" }, + { entry: 22899, name: "Prince Malchezaar", location: "Karazhan", isFinalBoss: true }, + { entry: 22900, name: "Nightbane", location: "Karazhan" }, + { entry: 22901, name: "Gruul the Dragonkiller", location: "Gruul's Lair", isFinalBoss: true }, + { entry: 22902, name: "Magtheridon", location: "Magtheridon's Lair", isFinalBoss: true }, + { entry: 22903, name: "Lady Vashj", location: "Serpentshrine Cavern", isFinalBoss: true }, + { entry: 22904, name: "Kael'thas Sunstrider", location: "The Eye", isFinalBoss: true }, + { entry: 22905, name: "Al'ar", location: "The Eye", isFinalBoss: true }, + { entry: 22906, name: "Solarian", location: "The Eye", isFinalBoss: true }, + { entry: 22907, name: "Void Reaver", location: "The Eye", isFinalBoss: true }, + { entry: 22908, name: "High Astromancer Solarian", location: "The Eye", isFinalBoss: true }, + { entry: 22909, name: "Kael'thas Sunstrider", location: "The Eye", isFinalBoss: true }, + { entry: 22910, name: "Rage Winterchill", location: "Mount Hyjal", isFinalBoss: true }, + { entry: 22911, name: "Anetheron", location: "Mount Hyjal", isFinalBoss: true }, + { entry: 22912, name: "Kaz'rogal", location: "Mount Hyjal", isFinalBoss: true }, + { entry: 22913, name: "Azgalor", location: "Mount Hyjal", isFinalBoss: true }, + { entry: 22914, name: "Archimonde", location: "Mount Hyjal", isFinalBoss: true }, + { entry: 22915, name: "Najentus", location: "Black Temple", isFinalBoss: true }, + { entry: 22916, name: "Supremus", location: "Black Temple", isFinalBoss: true }, + { entry: 22917, name: "Shade of Akama", location: "Black Temple", isFinalBoss: true }, + { entry: 22918, name: "Teron Gorefiend", location: "Black Temple", isFinalBoss: true }, + { entry: 22919, name: "Gurtogg Bloodboil", location: "Black Temple", isFinalBoss: true }, + { entry: 22920, name: "Reliquary of Souls", location: "Black Temple", isFinalBoss: true }, + { entry: 22921, name: "Mother Shahraz", location: "Black Temple", isFinalBoss: true }, + { entry: 22922, name: "The Illidari Council", location: "Black Temple", isFinalBoss: true }, + { entry: 22923, name: "Illidan Stormrage", location: "Black Temple", isFinalBoss: true }, + { entry: 22924, name: "Kalecgos", location: "Sunwell Plateau", isFinalBoss: true }, + { entry: 22925, name: "Brutallus", location: "Sunwell Plateau", isFinalBoss: true }, + { entry: 22926, name: "Felmyst", location: "Sunwell Plateau", isFinalBoss: true }, + { entry: 22927, name: "Eredar Twins", location: "Sunwell Plateau", isFinalBoss: true }, + { entry: 22928, name: "M'uru", location: "Sunwell Plateau", isFinalBoss: true }, + { entry: 22929, name: "Kil'jaeden", location: "Sunwell Plateau", isFinalBoss: true }, + + // Wrath of the Lich King dungeons + { entry: 23954, name: "Ingvar the Plunderer", location: "Utgarde Keep", isFinalBoss: true }, + { entry: 26723, name: "Keristrasza", location: "The Nexus", isFinalBoss: true }, + { entry: 29120, name: "Anub'arak", location: "Azjol-Nerub", isFinalBoss: true }, + { entry: 29311, name: "Herald Volazj", location: "Ahn'kahet: The Old Kingdom", isFinalBoss: true }, + { entry: 26632, name: "The Prophet Tharon'ja", location: "Drak'Tharon Keep", isFinalBoss: true }, + { entry: 31134, name: "Cyanigosa", location: "Violet Hold", isFinalBoss: true }, + { entry: 29306, name: "Gal'darah", location: "Gundrak", isFinalBoss: true }, + { entry: 27978, name: "Sjonnir the Ironshaper", location: "Halls of Stone", isFinalBoss: true }, + { entry: 28923, name: "Loken", location: "Halls of Lightning", isFinalBoss: true }, + { entry: 27656, name: "Ley-Guardian Eregos", location: "The Oculus", isFinalBoss: true }, + { entry: 26533, name: "Mal'Ganis", location: "Culling of Stratholme", isFinalBoss: true }, + { entry: 26861, name: "King Ymiron", location: "Utgarde Pinnacle", isFinalBoss: true }, + { entry: 35451, name: "The Black Knight", location: "Trial of the Champion", isFinalBoss: true }, + { entry: 36502, name: "Devourer of Souls", location: "Forge of Souls", isFinalBoss: true }, + { entry: 36658, name: "Scourgelord Tyrannus", location: "Pit of Saron", isFinalBoss: true }, + { entry: 37226, name: "The Lich King", location: "Halls of Reflection", isFinalBoss: true }, + + // Wrath of the Lich King raids + { entry: 29341, name: "Anub'Rekhan", location: "Naxxramas", isFinalBoss: true }, + { entry: 29342, name: "Grand Widow Faerlina", location: "Naxxramas", isFinalBoss: true }, + { entry: 29343, name: "Maexxna", location: "Naxxramas", isFinalBoss: true }, + { entry: 29344, name: "Noth the Plaguebringer", location: "Naxxramas", isFinalBoss: true }, + { entry: 29345, name: "Heigan the Unclean", location: "Naxxramas", isFinalBoss: true }, + { entry: 29346, name: "Loatheb", location: "Naxxramas", isFinalBoss: true }, + { entry: 29347, name: "Instructor Razuvious", location: "Naxxramas", isFinalBoss: true }, + { entry: 29348, name: "Gothik the Harvester", location: "Naxxramas", isFinalBoss: true }, + { entry: 29349, name: "The Four Horsemen", location: "Naxxramas", isFinalBoss: true }, + { entry: 29350, name: "Patchwerk", location: "Naxxramas", isFinalBoss: true }, + { entry: 29351, name: "Grobbulus", location: "Naxxramas", isFinalBoss: true }, + { entry: 29352, name: "Gluth", location: "Naxxramas", isFinalBoss: true }, + { entry: 29353, name: "Thaddius", location: "Naxxramas", isFinalBoss: true }, + { entry: 29354, name: "Sapphiron", location: "Naxxramas", isFinalBoss: true }, + { entry: 29355, name: "Kel'Thuzad", location: "Naxxramas", isFinalBoss: true }, + { entry: 29356, name: "Sartharion", location: "Obsidian Sanctum", isFinalBoss: true }, + { entry: 29357, name: "Malygos", location: "Eye of Eternity", isFinalBoss: true }, + { entry: 29358, name: "Ignis the Furnace Master", location: "Ulduar", isFinalBoss: true }, + { entry: 29359, name: "XT-002 Deconstructor", location: "Ulduar", isFinalBoss: true }, + { entry: 29360, name: "Kologarn", location: "Ulduar", isFinalBoss: true }, + { entry: 29361, name: "Auriaya", location: "Ulduar", isFinalBoss: true }, + { entry: 29362, name: "Hodir", location: "Ulduar", isFinalBoss: true }, + { entry: 29363, name: "Thorim", location: "Ulduar", isFinalBoss: true }, + { entry: 29364, name: "Freya", location: "Ulduar", isFinalBoss: true }, + { entry: 29365, name: "Mimiron", location: "Ulduar", isFinalBoss: true }, + { entry: 29366, name: "General Vezax", location: "Ulduar", isFinalBoss: true }, + { entry: 29367, name: "Yogg-Saron", location: "Ulduar", isFinalBoss: true }, + { entry: 29368, name: "The Northrend Beasts", location: "Trial of the Crusader", isFinalBoss: true }, + { entry: 29369, name: "Lord Jaraxxus", location: "Trial of the Crusader", isFinalBoss: true }, + { entry: 29370, name: "The Faction Champions", location: "Trial of the Crusader", isFinalBoss: true }, + { entry: 29371, name: "Twin Val'kyr", location: "Trial of the Crusader", isFinalBoss: true }, + { entry: 29372, name: "Anub'arak", location: "Trial of the Crusader", isFinalBoss: true }, + { entry: 29373, name: "Onyxia", location: "Onyxia's Lair", isFinalBoss: true }, + { entry: 29374, name: "Lord Marrowgar", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29375, name: "Lady Deathwhisper", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29376, name: "Gunship Battle", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29377, name: "Deathbringer Saurfang", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29378, name: "Festergut", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29379, name: "Rotface", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29380, name: "Professor Putricide", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29381, name: "Blood-Queen Lana'thel", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29382, name: "Valithria Dreamwalker", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29383, name: "Sindragosa", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29384, name: "The Lich King", location: "Icecrown Citadel", isFinalBoss: true }, + { entry: 29385, name: "Halion", location: "Ruby Sanctum", isFinalBoss: true }, + + // Molten Core + { entry: 12118, name: "Lucifron", location: "Molten Core" }, + { entry: 11982, name: "Magmadar", location: "Molten Core" }, + { entry: 12259, name: "Gehennas", location: "Molten Core" }, + { entry: 12057, name: "Garr", location: "Molten Core" }, + { entry: 12264, name: "Shazzrah", location: "Molten Core" }, + { entry: 12056, name: "Baron Geddon", location: "Molten Core" }, + { entry: 12098, name: "Sulfuron Harbinger", location: "Molten Core" }, + { entry: 11988, name: "Golemagg the Incinerator", location: "Molten Core" }, + { entry: 12018, name: "Majordomo Executus", location: "Molten Core" }, + { entry: 11502, name: "Ragnaros", location: "Molten Core", isFinalBoss: true }, + + // Blackwing Lair + { entry: 12435, name: "Razorgore the Untamed", location: "Blackwing Lair" }, + { entry: 13020, name: "Vaelastrasz the Corrupt", location: "Blackwing Lair" }, + { entry: 12017, name: "Broodlord Lashlayer", location: "Blackwing Lair" }, + { entry: 11983, name: "Firemaw", location: "Blackwing Lair" }, + { entry: 14601, name: "Ebonroc", location: "Blackwing Lair" }, + { entry: 11981, name: "Flamegor", location: "Blackwing Lair" }, + { entry: 14020, name: "Chromaggus", location: "Blackwing Lair" }, + { entry: 11583, name: "Nefarian", location: "Blackwing Lair", isFinalBoss: true }, + + // Ruins of Ahn'Qiraj + { entry: 15348, name: "Kurinnaxx", location: "Ruins of Ahn'Qiraj" }, + { entry: 15341, name: "General Rajaxx", location: "Ruins of Ahn'Qiraj" }, + { entry: 15340, name: "Moam", location: "Ruins of Ahn'Qiraj" }, + { entry: 15370, name: "Buru the Gorger", location: "Ruins of Ahn'Qiraj" }, + { entry: 15369, name: "Ayamiss the Hunter", location: "Ruins of Ahn'Qiraj" }, + { entry: 15339, name: "Ossirian the Unscarred", location: "Ruins of Ahn'Qiraj", isFinalBoss: true }, + + // Temple of Ahn'Qiraj + { entry: 15263, name: "The Prophet Skeram", location: "Temple of Ahn'Qiraj" }, + { entry: 15516, name: "Battleguard Sartura", location: "Temple of Ahn'Qiraj" }, + { entry: 15510, name: "Fankriss the Unyielding", location: "Temple of Ahn'Qiraj" }, + { entry: 15509, name: "Princess Huhuran", location: "Temple of Ahn'Qiraj" }, + { entry: 15275, name: "Emperor Vek'lor", location: "Temple of Ahn'Qiraj" }, + { entry: 15276, name: "Emperor Vek'nilash", location: "Temple of Ahn'Qiraj" }, + { entry: 15727, name: "C'Thun", location: "Temple of Ahn'Qiraj", isFinalBoss: true }, + + // Zul'Gurub + { entry: 14517, name: "High Priestess Jeklik", location: "Zul'Gurub" }, + { entry: 14507, name: "High Priest Venoxis", location: "Zul'Gurub" }, + { entry: 14510, name: "High Priestess Mar'li", location: "Zul'Gurub" }, + { entry: 14509, name: "High Priest Thekal", location: "Zul'Gurub" }, + { entry: 14515, name: "High Priestess Arlokk", location: "Zul'Gurub" }, + { entry: 14834, name: "Hakkar the Soulflayer", location: "Zul'Gurub", isFinalBoss: true }, + { entry: 11382, name: "Bloodlord Mandokir", location: "Zul'Gurub" }, + { entry: 11380, name: "Jin'do the Hexxer", location: "Zul'Gurub" }, + { entry: 15114, name: "Gahz'ranka", location: "Zul'Gurub" }, + { entry: 15082, name: "Renataki", location: "Zul'Gurub" }, + { entry: 15083, name: "Grilek", location: "Zul'Gurub" }, + { entry: 15084, name: "Hazza'rah", location: "Zul'Gurub" }, + { entry: 15085, name: "Wushoolay", location: "Zul'Gurub" }, + + // Karazhan + { entry: 16152, name: "Attumen the Huntsman", location: "Karazhan" }, + { entry: 15687, name: "Moroes", location: "Karazhan" }, + { entry: 16457, name: "Maiden of Virtue", location: "Karazhan" }, + { entry: 17521, name: "The Big Bad Wolf", location: "Karazhan" }, + { entry: 18168, name: "The Crone", location: "Karazhan" }, + { entry: 17533, name: "Romulo", location: "Karazhan" }, + { entry: 17534, name: "Julianne", location: "Karazhan" }, + { entry: 15691, name: "The Curator", location: "Karazhan" }, + { entry: 15688, name: "Terestian Illhoof", location: "Karazhan" }, + { entry: 16524, name: "Shade of Aran", location: "Karazhan" }, + { entry: 15689, name: "Netherspite", location: "Karazhan" }, + { entry: 16816, name: "Chess Event", location: "Karazhan" }, + { entry: 15690, name: "Prince Malchezaar", location: "Karazhan", isFinalBoss: true }, + { entry: 17225, name: "Nightbane", location: "Karazhan" } +]; + +// Quickly check if a creature is a final boss +export const FinalBossIDs = new Set(Bosses.filter(b => b.isFinalBoss).map(b => b.entry)); + +// Example usage: +export function isFinalBoss(entry: number): boolean { + return FinalBossIDs.has(entry); +} + +// Look up by name +export const BossesByName = Object.fromEntries( + Bosses.map(b => [b.name.toLowerCase(), b]) +); + +// Example usage: +export function getBossByName(name: string): Boss | undefined { + return BossesByName[name.toLowerCase()]; +} + +// For backward compatibility +export const BossIDs: Record = Object.fromEntries( + Bosses.map(b => [b.entry, true]) +); diff --git a/modules/npcs/soulswapper.ts b/modules/npcs/soulswapper.ts index 186f0c9..f112eb1 100644 --- a/modules/npcs/soulswapper.ts +++ b/modules/npcs/soulswapper.ts @@ -147,7 +147,7 @@ const GossipSelect: gossip_event_on_select = (event: number, player: Player, cre 1 ); - player.RemoveItem(PlayerItem, PlayerItem.GetEntry(), 1); + player.RemoveItem(PlayerItem, 1); player.GossipClearMenu(); player.GossipComplete();