new release

Leandro Silva [04-01-20 - 17:03]
new release
Filename
Inviter.lua
LeoDolmenRunner.lua
LeoDolmenRunner.txt
LeoDolmenRunner.xml
Runner.lua
Settings.lua
Ui.lua
diff --git a/Inviter.lua b/Inviter.lua
new file mode 100644
index 0000000..b9c6319
--- /dev/null
+++ b/Inviter.lua
@@ -0,0 +1,96 @@
+local Inviter = {
+    started = false,
+    message = "x",
+    kickList = {}
+}
+
+function Inviter:Kick()
+    if not Inviter.started then return end
+    for name, data in pairs(Inviter.kickList) do
+        if data ~= nil and GetTimeStamp() - data.added > LeoDolmenRunner.settings.inviter.kickDelay then
+            LeoDolmenRunner.debug("Kicking " .. name)
+            for i = 1, GetGroupSize() do
+                local tag = GetGroupUnitTagByIndex(i)
+                if GetUnitName(tag) == name then
+                    Inviter.kickList[name] = nil
+                    GroupKick(tag)
+                    break
+                end
+            end
+        end
+    end
+end
+
+function Inviter:CheckOfflines()
+    if not Inviter.started then return end
+    for i = 1, GetGroupSize() do
+        local unitTag = GetGroupUnitTagByIndex(i)
+        local name = GetUnitName(unitTag)
+        if not IsUnitOnline(unitTag) and Inviter.kickList[name] == nil then
+            LeoDolmenRunner.debug("Adding " .. name .. " to kick list")
+            Inviter.kickList[name] = {
+                unitTag = unitTag,
+                added  = GetTimeStamp()
+            }
+        elseif IsUnitOnline(unitTag) and Inviter.kickList[name] ~= nil then
+            LeoDolmenRunner.debug("Removing " .. name .. " from kick list, back online")
+            Inviter.kickList[name] = nil
+        end
+    end
+end
+
+function Inviter:Update(tick)
+    if not Inviter.started or not IsUnitSoloOrGroupLeader("player") or tick ~= 5 then return end
+
+    self:CheckOfflines()
+    self:Kick()
+end
+
+function Inviter:ChatMessage(type, from, message)
+    if not Inviter.started or not IsUnitSoloOrGroupLeader("player") then return end
+
+    if GetGroupSize() >= LeoDolmenRunner.settings.inviter.maxSize then return end
+
+    if type ~= CHAT_CHANNEL_SAY and type ~= CHAT_CHANNEL_YELL and type ~= CHAT_CHANNEL_ZONE and type ~= CHAT_CHANNEL_ZONE_LANGUAGE_1 and
+    type ~= CHAT_CHANNEL_ZONE_LANGUAGE_2 and type ~= CHAT_CHANNEL_ZONE_LANGUAGE_3 and type ~= CHAT_CHANNEL_ZONE_LANGUAGE_4 then return end
+
+    if message ~= Inviter.message or from == nil or from == "" then return end
+
+    LeoDolmenRunner.log(zo_strformat("Inviting <<1>>", from))
+    from = from:gsub("%^.+", "")
+    GroupInviteByName(from)
+end
+
+function Inviter:StartStop()
+    if not Inviter.started then
+        Inviter:Start()
+    else
+        Inviter:Stop()
+    end
+end
+
+function Inviter:Stop()
+    LeoDolmenRunner.log("Stopping auto invite")
+    Inviter.started = false
+    EVENT_MANAGER:UnregisterForEvent(LeoDolmenRunner.name, EVENT_CHAT_MESSAGE_CHANNEL)
+    LeoDolmenRunnerWindowInviterPanelStartStopLabel:SetText("Start")
+end
+
+function Inviter:Start(message)
+    if not IsUnitSoloOrGroupLeader("player") then
+        LeoDolmenRunner.log("You need to be group leader to invite.")
+        return
+    end
+
+    if message ~= nil then Inviter.message = message end
+
+    LeoDolmenRunner.log("Starting auto invite. Listening to " .. Inviter.message)
+    Inviter.started = true
+    LeoDolmenRunnerWindowInviterPanelStartStopLabel:SetText("Stop")
+    EVENT_MANAGER:RegisterForEvent(LeoDolmenRunner.name, EVENT_CHAT_MESSAGE_CHANNEL, function(event,...) Inviter:ChatMessage(...) end)
+end
+
+function Inviter:Initialize()
+end
+
+LeoDolmenRunner.inviter = Inviter
diff --git a/LeoDolmenRunner.lua b/LeoDolmenRunner.lua
index 4cb43bd..dd137ed 100644
--- a/LeoDolmenRunner.lua
+++ b/LeoDolmenRunner.lua
@@ -1,371 +1,127 @@
-LeoDolmenRunner = {
-    name = "LeoDolmenRunner",
-    displayName = "Leo's Dolmen Runner",
-    version = "1.0.1",
-    chatPrefix = "|c39B027LeoDolmenRunner|r: ",
-    hidden = true,
-    timeout = 900,
-    defaults = {
-        autoTravel = false,
-        direction = 1,
-        started = false,
-        run = {
-            startedTime = 0,
-            dolmensClosed = 0,
-            xpPerMinute = 0,
-            minutesToLevel = 0,
-            hoursToLevel = 0,
-            events = {}
-        }
-    },
-    directions = {
-        cw = 1,
-        ccw = -1
-    },
-    wayshrines = {
-        -- summerset 353 358 349 357 365
-        [59] = { region="Alik'r Desert", cw = 60, ccw = 155},
-        [60] = { region="Alik'r Desert", cw = 155, ccw = 59},
-        [155] = { region="Alik'r Desert", cw = 59, ccw = 60},
-        [127] = { region="Auridon", cw = 175, ccw = 176},
-        [175] = { region="Auridon", cw = 176, ccw = 127},
-        [176] = { region="Auridon", cw = 127, ccw = 175},
-        [39] = { region="Bangkorai", cw = 206, ccw = 35},
-        [206] = { region="Bangkorai", cw = 35, ccw = 39},
-        [35] = { region="Bangkorai", cw = 39, ccw = 206},
-        [30] = { region="Deshaan", cw = 27, ccw = 80},
-        [27] = { region="Deshaan", cw = 80, ccw = 30},
-        [80] = { region="Deshaan", cw = 30, ccw = 27},
-        [95] = { region="Eastmarch", cw = 92, ccw = 90},
-        [92] = { region="Eastmarch", cw = 90, ccw = 95},
-        [90] = { region="Eastmarch", cw = 95, ccw = 92},
-        [20] = { region="Glenumbra", cw = 6, ccw = 2},
-        [6] = { region="Glenumbra", cw = 2, ccw = 20},
-        [2] = { region="Glenumbra", cw = 20, ccw = 6},
-        [207] = { region="Grahtwood", cw = 164, ccw = 21},
-        [164] = { region="Grahtwood", cw = 21, ccw = 207},
-        [21] = { region="Grahtwood", cw = 207, ccw = 164},
-        [148] = { region="Greenshade", cw = 152, ccw = 149},
-        [152] = { region="Greenshade", cw = 149, ccw = 148},
-        [149] = { region="Greenshade", cw = 148, ccw = 152},
-        [103] = { region="Malabal Tor", cw = 100, ccw = 105},
-        [100] = { region="Malabal Tor", cw = 105, ccw = 103},
-        [105] = { region="Malabal Tor", cw = 103, ccw = 100},
-        [162] = { region="Reaper's March", cw = 158, ccw = 157},
-        [158] = { region="Reaper's March", cw = 157, ccw = 162},
-        [157] = { region="Reaper's March", cw = 162, ccw = 158},
-        [10] = { region="Ravenspire", cw = 13, ccw = 86},
-        [13] = { region="Ravenspire", cw = 86, ccw = 10},
-        [86] = { region="Ravenspire", cw = 10, ccw = 13},
-        [51] = { region="Shadowfen", cw = 53, ccw = 47},
-        [53] = { region="Shadowfen", cw = 47, ccw = 51},
-        [47] = { region="Shadowfen", cw = 51, ccw = 53},
-        [73] = { region="Stonefalls", cw = 67, ccw = 71},
-        [67] = { region="Stonefalls", cw = 71, ccw = 73},
-        [71] = { region="Stonefalls", cw = 73, ccw = 67},
-        [17] = { region="Stormhaven", cw = 19, ccw = 31},
-        [19] = { region="Stormhaven", cw = 31, ccw = 17},
-        [31] = { region="Stormhaven", cw = 17, ccw = 19},
-        [110] = { region="The Rift", cw = 116, ccw = 114},
-        [116] = { region="The Rift", cw = 114, ccw = 110},
-        [114] = { region="The Rift", cw = 110, ccw = 116}
-    }
-}
-
-local LastUpdateTimestamp = GetTimeStamp()
-
-function LeoDolmenRunner.log(message)
-    d(LeoDolmenRunner.chatPrefix .. message)
-end
-
-function LeoDolmenRunner.onFastTravelInteraction(currentWayshrine)
-    if LeoDolmenRunner.wayshrines[currentWayshrine] and LeoDolmenRunner.data.autoTravel then
-        local nextWayshrine = nil
-        if LeoDolmenRunner.data.direction == LeoDolmenRunner.directions.cw then
-            nextWayshrine = LeoDolmenRunner.wayshrines[currentWayshrine].cw
-        else
-            nextWayshrine = LeoDolmenRunner.wayshrines[currentWayshrine].ccw
-        end
-        local discovered, name = GetFastTravelNodeInfo(nextWayshrine)
-        if not discovered then
-            LeoDolmenRunner.log("Can't fast travel: " .. name .. " not discovered yet.")
-            return
-        end
-        FastTravelToNode(nextWayshrine)
-    end
-end
-
-local function calculateXpPerMinute(xp, timestamp)
-    local diff = GetDiffBetweenTimeStamps(GetTimeStamp(), timestamp)
-    diff = (diff == 0) and 1 or diff
-
-    return xp / (diff / 60)
-end
-
-local function TimeToLevel()
-    local xpToLevel = 0
-    if (CanUnitGainChampionPoints("player")) then
-        xpToLevel = GetNumChampionXPInChampionPoint(GetPlayerChampionPointsEarned("player")) - GetPlayerChampionXP()
-    else
-        xpToLevel = GetNumExperiencePointsInLevel(GetUnitLevel("player")) - GetUnitXP("player")
-    end
-
-    local minutesToLevel = math.ceil(xpToLevel / LeoDolmenRunner.data.run.xpPerMinute)
-
-    if minutesToLevel < 60 then return 0, minutesToLevel end
-
-    local hoursToLevel = math.floor(minutesToLevel / 60)
-    minutesToLevel = minutesToLevel - (hoursToLevel * 60)
-
-    return hoursToLevel, minutesToLevel
-end
-
-function LeoDolmenRunner:UpdateData()
-    local xpGained = 0
-    local firstEventTimestamp = 0
-    if (#LeoDolmenRunner.data.run.events > 0) then
-        for i, event in pairs(LeoDolmenRunner.data.run.events) do
-            if GetDiffBetweenTimeStamps(GetTimeStamp(), event.timestamp) > LeoDolmenRunner.timeout then
-                LeoDolmenRunner.data.run.events[i] = nil
-            else
-                xpGained = xpGained + event.xp
-
-                if firstEventTimestamp == 0 or event.timestamp < firstEventTimestamp then
-                    firstEventTimestamp = event.timestamp
-                end
-
-            end
-        end
-    end
-
-    LeoDolmenRunner.data.run.xpPerMinute = calculateXpPerMinute(xpGained, firstEventTimestamp)
-
-    if xpGained > 0 then
-        LeoDolmenRunner.data.run.hoursToLevel, LeoDolmenRunner.data.run.minutesToLevel = TimeToLevel()
-    end
-end
-
-function LeoDolmenRunner:OnExperienceGain(reason, level, previousExperience, currentExperience, championPoints)
-    local event = {
-        timestamp = GetTimeStamp(),
-        xp = currentExperience - previousExperience
-    }
-    table.insert(LeoDolmenRunner.data.run.events, event)
-
-    if reason == 7 then LeoDolmenRunner.data.run.dolmensClosed = LeoDolmenRunner.data.run.dolmensClosed + 1 end
-
-    self:UpdateData()
-end
-
-local function getColor(value, max)
-    local rate = value / max
-    if rate > 0.8 then return '10FF10' end
-    if rate > 0.5 then return 'FFFF00' end
-    return 'FF1010'
-end
-
-function LeoDolmenRunner:UpdateTrainingGear()
-    local stack = 0
-    local stackRepair = 0
-    local maxItems = 9
-    local numItems = 0
-    local bag = SHARED_INVENTORY:GenerateFullSlotData(nil,BAG_WORN)
-    for _, data in pairs(bag) do
-        local link = GetItemLink(data.bagId, data.slotIndex)
-        if GetItemLinkTraitType(link) == ITEM_TRAIT_TYPE_ARMOR_TRAINING or GetItemLinkTraitType(link) == ITEM_TRAIT_TYPE_WEAPON_TRAINING then
-            local activeWeaponPair = GetActiveWeaponPairInfo()
-            if ( GetItemLinkItemType(link) == ITEMTYPE_ARMOR
-                or
-                (activeWeaponPair == ACTIVE_WEAPON_PAIR_MAIN and (data.slotIndex == EQUIP_SLOT_MAIN_HAND or data.slotIndex == EQUIP_SLOT_OFF_HAND))
-                or
-                (activeWeaponPair == ACTIVE_WEAPON_PAIR_BACKUP and (data.slotIndex == EQUIP_SLOT_BACKUP_MAIN or data.slotIndex == EQUIP_SLOT_BACKUP_OFF)))
-            then
-                local quality = select(8, GetItemInfo(data.bagId, data.slotIndex))
-                local condition = GetItemCondition(data.bagId, data.slotIndex)
-                local equipType = select(6, GetItemInfo(data.bagId, data.slotIndex))
-                numItems = numItems + 1
-                local xp = 0
-                if GetItemLinkItemType(link) == ITEMTYPE_ARMOR then
-                    xp = 6 + quality
-                elseif equipType == EQUIP_TYPE_TWO_HAND then
-                    numItems = numItems + 1
-                    xp = 4 + quality
-                else
-                    xp = 2 + (0.5 * quality)
-                end
-                stack = stack + xp
-                if condition == 0 then stackRepair = stackRepair + xp end
-            end
-        end
-    end
-
-    local text = ""
-
-    if stackRepair > 0 then
-        text = "|c" .. getColor(stack - stackRepair, stack) .. " " .. (stack - stackRepair) .. " / " .. stack .. "%|r (Repair)"
-    else
-        text = "|c" .. getColor(stack, stack) .. stack .. "%|r"
-    end
-
-    LeoDolmenRunnerWindowPanelXpFromGear:SetText(text)
-    LeoDolmenRunnerWindowPanelTrainingGear:SetText("|c" .. getColor(numItems, maxItems) .. numItems .. " / " .. maxItems .. "|r")
-end
-
-function LeoDolmenRunner:CreateUI()
-    if LeoDolmenRunner.data.started then
-        LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Stop")
-    else
-        LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Start")
-    end
-    self:UpdateTrainingGear()
-end
-
-local function FormatNumber(num)
-    return zo_strformat("<<1>>", ZO_LocalizeDecimalNumber(num))
-end
-
-local function FormatTime(seconds, short, colorizeCountdown)
-    if short == nil then short = false end
-    local formats = {
-        dhm = SI_TIME_FORMAT_DDHHMM_DESC_SHORT,
-        day = SI_TIME_FORMAT_DAYS,
-        hm = SI_TIME_FORMAT_HHMM_DESC_SHORT,
-        hms = SI_TIME_FORMAT_HHMMSS_DESC_SHORT,
-        hour = SI_TIME_FORMAT_HOURS,
-        ms = SI_TIME_FORMAT_MMSS_DESC_SHORT,
-        m = SI_TIME_FORMAT_MINUTES
-    }
-    if seconds and seconds > 0 then
-        local ss = seconds % 60
-        local mm = math.floor(seconds / 60)
-        local hh = math.floor(mm / 60)
-        mm = mm % 60
-        local dn = math.floor(hh / 24)
-        local hhdn = hh - (dn*24)
-
-        local ssF = string.format("%02d", ss)
-        local mmF = string.format("%02d", mm)
-        local hhF = string.format("%02d", hh)
-        local hhdnF = string.format("%02d", hhdn)
-
-        local result = ''
-        if dn > 0 then
-            if short then
-                result = ZO_CachedStrFormat(GetString(formats.day), dn) .." "..ZO_CachedStrFormat(GetString(formats.hour), hhdnF)
-            else
-                result = ZO_CachedStrFormat(GetString(formats.dhm), dn, hhdnF, mmF)
-            end
-        elseif hh > 0 then
-            if short then
-                result = ZO_CachedStrFormat(GetString(formats.hm), hhF, mmF)
-            else
-                result = ZO_CachedStrFormat(GetString(formats.hms), hhF, mmF, ssF)
-            end
-        elseif mm >= 0 then result = ZO_CachedStrFormat(GetString(formats.ms), mmF, ssF)
-        end
-        return result
-    else return ZO_CachedStrFormat(GetString(formats.m), 0) end
-end
-
-function LeoDolmenRunner:UpdateUI()
-
-    self:UpdateTrainingGear()
-
-    local currentTimestamp = GetTimeStamp()
-    if LeoDolmenRunner.data.started then
-        if GetDiffBetweenTimeStamps(currentTimestamp, LastUpdateTimestamp) >= 5 then
-            self:UpdateData()
-            LastUpdateTimestamp = currentTimestamp
-        end
-
-        local secs = currentTimestamp - LeoDolmenRunner.data.run.startedTime
-        if secs > 0 then
-            LeoDolmenRunnerWindowPanelTime:SetText(FormatTime(secs))
-        end
-    end
-
-    LeoDolmenRunnerWindowPanelDolmensClosed:SetText(LeoDolmenRunner.data.run.dolmensClosed)
-    LeoDolmenRunnerWindowPanelXP:SetText(FormatNumber(math.ceil(LeoDolmenRunner.data.run.xpPerMinute)) .. " / min")
-
-    LeoDolmenRunnerWindowPanelXPNext:SetText(ZO_CachedStrFormat(GetString(SI_TIME_FORMAT_HHMM_DESC_SHORT), LeoDolmenRunner.data.run.hoursToLevel, LeoDolmenRunner.data.run.minutesToLevel))
-
-    zo_callLater(function() self:UpdateUI() end, 1000)
-end
-
-function LeoDolmenRunner:StartStop()
-    if LeoDolmenRunner.data.started then
-        LeoDolmenRunner.data.started = false
-        LeoDolmenRunner.data.autoTravel = false
-        LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Start")
-    else
-        LeoDolmenRunner.data.run = LeoDolmenRunner.defaults.run
-        LeoDolmenRunner.data.run.startedTime = GetTimeStamp()
-        LeoDolmenRunner.data.started = true
-        LeoDolmenRunner.data.autoTravel = true
-        LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Stop")
-    end
-end
-
-function LeoDolmenRunner:OnWindowMoveStop()
-    LeoDolmenRunner.data.position = {
-        left = LeoDolmenRunnerWindow:GetLeft(),
-        top = LeoDolmenRunnerWindow:GetTop()
-    }
-end
-
-function LeoDolmenRunner:RestorePosition()
-    local position = LeoDolmenRunner.data.position or { left = 200; top = 200; }
-    local left = position.left
-    local top = position.top
-
-    LeoDolmenRunnerWindow:ClearAnchors()
-    LeoDolmenRunnerWindow:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, left, top)
-    LeoDolmenRunnerWindow:SetDrawTier(DT_MEDIUM)
-end
-
-function LeoDolmenRunner:Initialize()
-    local showButton, feedbackWindow = LibFeedback:initializeFeedbackWindow(LeoDolmenRunner,
-        LeoDolmenRunner.name,LeoDolmenRunnerWindow, "@LeandroSilva",
-            {TOPRIGHT, LeoDolmenRunnerWindow, TOPRIGHT,-50,3},
-            {0,1000,10000,"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Y9KM4PZU2UZ6A"},
-            "If you found a bug, have a request or a suggestion, or simply wish to donate, send a mail.")
-    LeoDolmenRunner.feedback = feedbackWindow
-    LeoDolmenRunner.feedback:SetDrawLayer(DL_OVERLAY)
-    LeoDolmenRunner.feedback:SetDrawTier(DT_MEDIUM)
-
-    LeoDolmenRunnerWindowTitle:SetText(LeoDolmenRunner.displayName .. " v" .. LeoDolmenRunner.version)
-
-    SLASH_COMMANDS["/ldr"] = function(cmd)
-        LeoDolmenRunnerWindow:ToggleHidden()
-    end
-
-    if GetDisplayName() == "@LeandroSilva" then
-        SLASH_COMMANDS["/rr"] = function(cmd)
-            ReloadUI()
-        end
-    end
-
-    self:RestorePosition()
-
-    self:CreateUI()
-    self:UpdateUI()
-end
-
-local function OnAddOnLoaded(event, addonName)
-    if addonName ~= LeoDolmenRunner.name then return end
-
-    EVENT_MANAGER:UnregisterForEvent(LeoDolmenRunner.name, EVENT_ADD_ON_LOADED)
-    LeoDolmenRunner.data = LibSavedVars:NewCharacterSettings( LeoDolmenRunner.name .. "_Data", "Characters", LeoDolmenRunner.defaults )
-
-    LeoDolmenRunner:Initialize()
-
-    CALLBACK_MANAGER:RegisterCallback("LAM-PanelControlsCreated", OnSettingsControlsCreated)
-    EVENT_MANAGER:RegisterForEvent(LeoDolmenRunner.name, EVENT_START_FAST_TRAVEL_INTERACTION, function(eventId, ...) LeoDolmenRunner.onFastTravelInteraction(...) end)
-    EVENT_MANAGER:RegisterForEvent(LeoDolmenRunner.name, EVENT_EXPERIENCE_GAIN, function(eventId, ...)
-        LeoDolmenRunner:OnExperienceGain(...)
-    end)
-
-    LeoDolmenRunner.log("started.")
-end
-
-EVENT_MANAGER:RegisterForEvent(LeoDolmenRunner.name, EVENT_ADD_ON_LOADED, OnAddOnLoaded)
+LeoDolmenRunner = {
+    name = "LeoDolmenRunner",
+    displayName = "Leo's Dolmen Runner",
+    version = "1.1.0",
+    chatPrefix = "|c39B027LeoDolmenRunner|r: ",
+    debug = false,
+    defaults = {
+        hidden = true,
+        runner = {
+            autoTravel = true,
+            autoDismiss = true,
+            reapplyBuff = true
+        },
+        inviter = {
+            maxSize = 24,
+            autoKick = false,
+            kickDelay = 10
+        }
+    }
+}
+
+local LDR = LeoDolmenRunner
+
+function LDR.log(message)
+    d(LDR.chatPrefix .. message)
+end
+
+function LDR.debug(message)
+    if not LDR.debug then return end
+    LDR.log("[D] " .. message)
+end
+
+local LastUpdateTimestamp = GetTimeStamp()
+
+function LDR:Update()
+    local currentTimestamp = GetTimeStamp()
+    local tick = 1
+    if GetDiffBetweenTimeStamps(currentTimestamp, LastUpdateTimestamp) >= 5 then
+        tick = 5
+        LastUpdateTimestamp = currentTimestamp
+    end
+
+    self.runner:Update(tick)
+    self.inviter:Update(tick)
+    self.ui:Update(tick)
+end
+
+function LDR:Initialize()
+    local showButton, feedbackWindow = LibFeedback:initializeFeedbackWindow(LeoDolmenRunner,
+    LDR.name,LeoDolmenRunnerWindow, "@LeandroSilva",
+            {TOPRIGHT, LeoDolmenRunnerWindow, TOPRIGHT,-35,-2},
+            {0,1000,10000,"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Y9KM4PZU2UZ6A"},
+            "If you find a bug, have a request, or simply wish to donate, send me a mail.")
+    LDR.feedback = feedbackWindow
+    LDR.feedback:SetDrawTier(DT_HIGH)
+
+    LeoDolmenRunnerWindowTitle:SetText(LeoDolmenRunner.displayName .. " v" .. LeoDolmenRunner.version)
+
+    SLASH_COMMANDS["/ldr"] = function(cmd)
+        local options = {}
+        local searchResult = { string.match(cmd,"^(%S*)%s*(.-)$") }
+        for i,v in pairs(searchResult) do
+            if (v ~= nil and v ~= "") then
+                options[i] = string.lower(v)
+            end
+        end
+
+        if #options == 0 or options[1] == "help" then
+            LDR.log("Available options: ")
+            d("    /ldr toggle: Toggles the main window")
+            d("    /ldr show: Shows the main window")
+            d("    /ldr hide: Hides the main window")
+            d("    /ldr start: Starts the runner")
+            d("    /ldr stop: Stops the runner")
+            d("    /ldr cw: Changes direction to clockwise")
+            d("    /ldr ccw: Changes direction to counter clockwise")
+            d("    /ldr <letter>: Starts auto invite listening to the letter <letter>")
+            d("    /ldr ai stop: Stops auto invite")
+            return
+        end
+
+        if options[1] == "toggle" then LeoDolmenRunnerWindow:ToggleHidden()
+        elseif options[1] == "show" then LeoDolmenRunnerWindow:SetHidden(false)
+        elseif options[1] == "hide" then LeoDolmenRunnerWindow:SetHidden(true)
+        elseif options[1] == "start" then LDR.runner:Start()
+        elseif options[1] == "stop" then LDR.runner:Stop()
+        elseif options[1] == "cw" then LDR.runner:CW()
+        elseif options[1] == "ccw" then LDR.runner:CCW()
+        elseif options[1] == "ai" and #options == 2 and options[2] == "stop" then LDR.inviter:Stop()
+        elseif string.len(options[1]) == 1 then LDR.inviter:Start(options[1])
+        else LDR.log("Listening message must be only 1 letter (eg: x).")
+        end
+    end
+
+    if GetDisplayName() == "@LeandroSilva" then
+        SLASH_COMMANDS["/rr"] = function(cmd)
+            ReloadUI()
+        end
+    end
+
+    self.runner:Initialize()
+    self.inviter:Initialize()
+    self.ui:Initialize()
+
+    LDR.settingsPanel = LeoDolmenRunner_Settings:New()
+    LDR.settingsPanel:CreatePanel()
+
+    EVENT_MANAGER:RegisterForUpdate(LeoDolmenRunner.name, 1000, function() LDR:Update() end)
+end
+
+local function OnAddOnLoaded(event, addonName)
+    if addonName ~= LDR.name then return end
+
+    EVENT_MANAGER:UnregisterForEvent(LDR.name, EVENT_ADD_ON_LOADED)
+    LDR.settings = LibSavedVars:NewAccountWide( LDR.name .. "_Data", "Account", LDR.defaults )
+
+    LDR:Initialize()
+
+    CALLBACK_MANAGER:RegisterCallback("LAM-PanelControlsCreated", OnSettingsControlsCreated)
+    EVENT_MANAGER:RegisterForEvent(LDR.name, EVENT_START_FAST_TRAVEL_INTERACTION, function(eventId, ...) LDR.runner:OnFastTravelInteraction(...) end)
+    EVENT_MANAGER:RegisterForEvent(LDR.name, EVENT_EXPERIENCE_GAIN, function(eventId, ...) LDR.runner:OnExperienceGain(...) end)
+    EVENT_MANAGER:RegisterForEvent(LDR.name, EVENT_PLAYER_COMBAT_STATE, function(eventId, ...) LDR.runner:OnCombatState(...) end)
+
+    LDR.log("started.")
+end
+
+EVENT_MANAGER:RegisterForEvent(LDR.name, EVENT_ADD_ON_LOADED, OnAddOnLoaded)
diff --git a/LeoDolmenRunner.txt b/LeoDolmenRunner.txt
index c065702..7d1cde2 100644
--- a/LeoDolmenRunner.txt
+++ b/LeoDolmenRunner.txt
@@ -1,10 +1,14 @@
-## Title: Leo's Dolmen Runner
-## APIVersion: 100029 100030
-## Version: 1.0.1
-## AddOnVersion: 101
-## Author: |c39B027@LeandroSilva|r
-## SavedVariables: LeoDolmenRunner_Data
-## DependsOn: LibFeedback LibAddonMenu-2.0 LibSavedVars
-
-LeoDolmenRunner.lua
-LeoDolmenRunner.xml
+## Title: Leo's Dolmen Runner
+## APIVersion: 100029 100030
+## Version: 1.1.0
+## AddOnVersion: 110
+## Author: |c39B027@LeandroSilva|r
+## SavedVariables: LeoDolmenRunner_Data
+## DependsOn: LibFeedback LibAddonMenu-2.0 LibSavedVars
+
+LeoDolmenRunner.lua
+Runner.lua
+Inviter.lua
+Ui.lua
+Settings.lua
+LeoDolmenRunner.xml
diff --git a/LeoDolmenRunner.xml b/LeoDolmenRunner.xml
index b904175..4a591bd 100644
--- a/LeoDolmenRunner.xml
+++ b/LeoDolmenRunner.xml
@@ -1,29 +1,30 @@
 <GuiXml>
-    <Font name="LeoDolmenRunnerLargeFont" font="$(MEDIUM_FONT)|18|soft-shadow-thin"/>
     <Font name="LeoDolmenRunnerNormalFont" font="$(MEDIUM_FONT)|16|soft-shadow-thin"/>
     <Font name="LeoDolmenRunnerSmallFont" font="$(MEDIUM_FONT)|14|soft-shadow-thin"/>
     <Controls>

         <TopLevelControl name="LeoDolmenRunnerWindow" movable="true" mouseEnabled="true" clampedToScreen="true" hidden="true" tier="MEDIUM">
-            <Dimensions x="350" y="250" />
+            <Dimensions x="300" y="250" />
             <Anchor point="TOP" relativeTo="GuiRoot" relativePoint="CENTER" offsetY="100" />

-            <OnMoveStop> LeoDolmenRunner:OnWindowMoveStop() </OnMoveStop>
+            <OnMoveStop> LeoDolmenRunner.ui:OnWindowMoveStop() </OnMoveStop>
+            <OnHide> LeoDolmenRunner.settings.hidden = true </OnHide>
+            <OnShow> LeoDolmenRunner.settings.hidden = false </OnShow>

             <Controls>
                 <Backdrop name="$(parent)BG" centerColor="000000" edgeColor="222222">
                     <Anchor point="TOPLEFT" relativePoint="TOPLEFT" relativeTo="$(parent)"/>
-                    <Dimensions x="350" y="50"/>
+                    <Dimensions x="300" y="40"/>
                     <Edge edgeSize="1"/>
                 </Backdrop>
-                <Label name="$(parent)Title" color="39B027" font="ZoFontWinH3" wrapMode="ELLIPSIS"
+                <Label name="$(parent)Title" color="39B027" font="ZoFontWinH5" wrapMode="ELLIPSIS"
                        verticalAlignment="CENTER" mouseEnabled="false">
-                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="16" offsetY="10"/>
+                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="10"/>
                 </Label>

                 <Button name="$(parent)Close" clickSound="Click">
                     <Anchor point="TOPRIGHT" relativePoint="TOPRIGHT" relativeTo="$(parent)" offsetX="-5" offsetY="4"/>
-                    <Dimensions x="40" y="40"/>
+                    <Dimensions x="30" y="30"/>
                     <OnClicked>self:GetParent():SetHidden(true)</OnClicked>
                     <Controls>
                         <Backdrop name="$(parent)BG" centerColor="101010" edgeColor="202020">
@@ -31,14 +32,14 @@
                             <Edge edgeSize="1"/>
                         </Backdrop>
                         <Texture name="$(parent)Texture" textureFile="esoui/art/buttons/decline_up.dds">
-                            <Dimensions y="25" x="25"/>
+                            <Dimensions y="20" x="20"/>
                             <Anchor point="128"/>
                         </Texture>
                     </Controls>
                 </Button>
                 <Button name="$(parent)FeedbackButton" clickSound="Click">
                     <Anchor point="TOPRIGHT" relativePoint="TOPLEFT" relativeTo="$(parent)Close" offsetX="-5"/>
-                    <Dimensions x="40" y="40"/>
+                    <Dimensions x="30" y="30"/>
                     <Controls>
                         <Backdrop name="$(parent)BG" centerColor="101010" edgeColor="202020">
                             <AnchorFill/>
@@ -48,103 +49,137 @@
                 </Button>

                 <Backdrop name="LeoDolmenRunnerWindowPanel" tier="1" centerColor="000000" edgeColor="202020" hidden="false" clampedToScreen="true" movable="false" mouseEnabled="true">
-                    <Anchor point="TOPLEFT" relativePoint="TOPLEFT" relativeTo="LeoDolmenRunnerWindow" offsetX="0" offsetY="52"/>
-                    <Dimensions x="350" y="200"/>
+                    <Anchor point="TOPLEFT" relativePoint="TOPLEFT" relativeTo="LeoDolmenRunnerWindow" offsetX="0" offsetY="42"/>
+                    <Dimensions x="300" y="200"/>
                     <Edge edgeSize="1"/>
                     <Controls>

-                        <Label name="$(parent)XPLabel" text="XP Rate" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
+                        <Label name="$(parent)XPLabel" text="XP Rate" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
                             <Dimensions x="120" y="35" />
-                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="10" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="5" offsetY="10" />
                         </Label>
-                        <Label name="$(parent)XP" text="0" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                        <Label name="$(parent)XP" text="0" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
                             <Dimensions x="100" y="35" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)XPLabel" relativePoint="TOPRIGHT" offsetX="10" offsetY="0" />
                         </Label>

-                        <Label name="$(parent)XPNextLabel" text="ETA to Level" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
+                        <Label name="$(parent)XPNextLabel" text="ETA to Level" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
                             <Dimensions x="120" y="35" />
-                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="40" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="5" offsetY="40" />
                         </Label>
-                        <Label name="$(parent)XPNext" text="0" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                        <Label name="$(parent)XPNext" text="0" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
                             <Dimensions x="100" y="35" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)XPNextLabel" relativePoint="TOPRIGHT" offsetX="10" offsetY="0" />
                         </Label>

-                        <Label name="$(parent)TrainingGearLabel" text="Training Gear" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
+                        <Label name="$(parent)TrainingGearLabel" text="Training Gear" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
                             <Dimensions x="120" y="35" />
-                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="70" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="5" offsetY="70" />
                         </Label>
-                        <Label name="$(parent)TrainingGear" text="" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                        <Label name="$(parent)TrainingGear" text="" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
                             <Dimensions x="100" y="35" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)TrainingGearLabel" relativePoint="TOPRIGHT" offsetX="10" offsetY="0" />
                         </Label>

-                        <Label name="$(parent)XpFromGearLabel" text="XP from Gear" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
+                        <Label name="$(parent)XpFromGearLabel" text="XP from Gear" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
                             <Dimensions x="120" y="35" />
-                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="100" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="5" offsetY="100" />
                         </Label>
-                        <Label name="$(parent)XpFromGear" text="0 %" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                        <Label name="$(parent)XpFromGear" text="0 %" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
                             <Dimensions x="160" y="35" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)XpFromGearLabel" relativePoint="TOPRIGHT" offsetX="10" offsetY="0" />
                         </Label>

-                        <Label name="$(parent)DolmensClosedLabel" text="Dolmens Closed" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
+                        <Label name="$(parent)DolmensClosedLabel" text="Dolmens Closed" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="RIGHT">
                             <Dimensions x="120" y="35" />
-                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="130" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="5" offsetY="130" />
                         </Label>
-                        <Label name="$(parent)DolmensClosed" text="0" font="LeoDolmenRunnerLargeFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                        <Label name="$(parent)DolmensClosed" text="0" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
                             <Dimensions x="100" y="35" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)DolmensClosedLabel" relativePoint="TOPRIGHT" offsetX="10" offsetY="0" />
                         </Label>

                         <Button name="$(parent)StartStop" clickSound="Click">
                             <Anchor point="BOTTOMLEFT" relativeTo="$(parent)Panel" relativePoint="BOTTOMLEFT" offsetX="5" offsetY="-5" />
-                            <Dimensions x="75" y="40"/>
-                            <OnClicked> LeoDolmenRunner:StartStop() </OnClicked>
+                            <Dimensions x="75" y="30"/>
+                            <OnClicked> LeoDolmenRunner.runner:StartStop() </OnClicked>
                             <Controls>
                                 <Backdrop name="$(parent)BG" centerColor="101010" edgeColor="202020">
                                     <AnchorFill/>
                                     <Edge edgeSize="1"/>
                                 </Backdrop>
                                 <Label name="$(parent)Label" text="Start" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="CENTER" >
-                                    <Dimensions x="45" y="30" />
-                                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="5"/>
+                                    <Dimensions x="45" y="25" />
+                                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="2"/>
                                 </Label>
                             </Controls>
                         </Button>

                         <Label name="$(parent)Time" text="" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER">
-                            <Dimensions x="60" y="40" />
+                            <Dimensions x="60" y="30" />
                             <Anchor point="TOPLEFT" relativeTo="$(parent)StartStop" relativePoint="TOPRIGHT" offsetX="10" />
                         </Label>

                         <Button name="$(parent)Orientation" clickSound="Click">
                             <Anchor point="BOTTOMRIGHT" relativeTo="$(parent)Panel" relativePoint="BOTTOMRIGHT" offsetX="-5" offsetY="-5" />
-                            <Dimensions x="60" y="40"/>
-                            <OnClicked>
-                                if LeoDolmenRunner.data.direction == LeoDolmenRunner.directions.cw then
-                                    LeoDolmenRunner.data.direction = LeoDolmenRunner.directions.ccw
-                                    self:GetNamedChild("Label"):SetText("CCW")
-                                else
-                                    LeoDolmenRunner.data.direction = LeoDolmenRunner.directions.cw
-                                    self:GetNamedChild("Label"):SetText("CW")
-                                end
-                            </OnClicked>
+                            <Dimensions x="60" y="30"/>
+                            <OnClicked> LeoDolmenRunner.runner:CWCCW() </OnClicked>
                             <Controls>
                                 <Backdrop name="$(parent)BG" centerColor="101010" edgeColor="202020">
                                     <AnchorFill/>
                                     <Edge edgeSize="1"/>
                                 </Backdrop>
                                 <Label name="$(parent)Label" text="CW" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="CENTER">
-                                    <Dimensions x="35" y="30" />
-                                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="5"/>
+                                    <Dimensions x="35" y="25" />
+                                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="2"/>
                                 </Label>
                             </Controls>
                         </Button>

                     </Controls>
                 </Backdrop>
+
+                <Backdrop name="LeoDolmenRunnerWindowInviterPanel" tier="1" centerColor="000000" edgeColor="202020" hidden="false" clampedToScreen="true" movable="false" mouseEnabled="true">
+                    <Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT" relativeTo="LeoDolmenRunnerWindowPanel" offsetX="0" offsetY="2"/>
+                    <Dimensions x="300" y="70"/>
+                    <Edge edgeSize="1"/>
+                    <Controls>
+
+                        <Label name="$(parent)Title" text="Auto Inviter" color="39B027" font="ZoFontWinH5" wrapMode="ELLIPSIS"
+                            verticalAlignment="CENTER" mouseEnabled="false">
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="10"/>
+                        </Label>
+
+                        <Button name="$(parent)StartStop" clickSound="Click">
+                            <Anchor point="BOTTOMLEFT" relativeTo="$(parent)" relativePoint="BOTTOMLEFT" offsetX="5" offsetY="-5" />
+                            <Dimensions x="75" y="30"/>
+                            <OnClicked> LeoDolmenRunner.inviter:StartStop() </OnClicked>
+                            <Controls>
+                                <Backdrop name="$(parent)BG" centerColor="101010" edgeColor="202020">
+                                    <AnchorFill/>
+                                    <Edge edgeSize="1"/>
+                                </Backdrop>
+                                <Label name="$(parent)Label" text="Start" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="CENTER" >
+                                    <Dimensions x="45" y="25" />
+                                    <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="2"/>
+                                </Label>
+                            </Controls>
+                        </Button>
+
+                        <Label name="$(parent)MessageLabel" text="Listening to" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                            <Dimensions x="80" y="35" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)StartStop" relativePoint="TOPRIGHT" offsetX="6" offsetY="0" />
+                        </Label>
+                        <Label name="$(parent)Message" text="x" color="39B027" font="LeoDolmenRunnerNormalFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                            <Dimensions x="20" y="35" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)MessageLabel" relativePoint="TOPRIGHT" offsetX="6" offsetY="0" />
+                        </Label>
+                        <Label name="$(parent)Hint" text="( /ldr <letter> )" font="LeoDolmenRunnerSmallFont" verticalAlignment="CENTER" horizontalAlignment="LEFT">
+                            <Dimensions x="80" y="35" />
+                            <Anchor point="TOPLEFT" relativeTo="$(parent)Message" relativePoint="TOPRIGHT" offsetX="6" offsetY="0" />
+                        </Label>
+                    </Controls>
+                </Backdrop>
             </Controls>
         </TopLevelControl>
     </Controls>
diff --git a/Runner.lua b/Runner.lua
new file mode 100644
index 0000000..2890d7f
--- /dev/null
+++ b/Runner.lua
@@ -0,0 +1,285 @@
+local Runner = {
+    started = false,
+    timeout = 900,
+    activeBuff = 0,
+    data = {
+        direction = 1,
+        startedTime = 0,
+        dolmensClosed = 0,
+        xpPerMinute = 0,
+        minutesToLevel = 0,
+        hoursToLevel = 0,
+        events = {}
+    },
+    defaultData = {
+        direction = 1,
+        startedTime = 0,
+        dolmensClosed = 0,
+        xpPerMinute = 0,
+        minutesToLevel = 0,
+        hoursToLevel = 0,
+        events = {}
+    },
+    directions = {
+        cw = 1,
+        ccw = -1
+    },
+    wayshrines = {
+        -- summerset 353 358 349 357 365
+        [59] = { region="Alik'r Desert", cw = 60, ccw = 155},
+        [60] = { region="Alik'r Desert", cw = 155, ccw = 59},
+        [155] = { region="Alik'r Desert", cw = 59, ccw = 60},
+        [127] = { region="Auridon", cw = 175, ccw = 176},
+        [175] = { region="Auridon", cw = 176, ccw = 127},
+        [176] = { region="Auridon", cw = 127, ccw = 175},
+        [39] = { region="Bangkorai", cw = 206, ccw = 35},
+        [206] = { region="Bangkorai", cw = 35, ccw = 39},
+        [35] = { region="Bangkorai", cw = 39, ccw = 206},
+        [30] = { region="Deshaan", cw = 27, ccw = 80},
+        [27] = { region="Deshaan", cw = 80, ccw = 30},
+        [80] = { region="Deshaan", cw = 30, ccw = 27},
+        [95] = { region="Eastmarch", cw = 92, ccw = 90},
+        [92] = { region="Eastmarch", cw = 90, ccw = 95},
+        [90] = { region="Eastmarch", cw = 95, ccw = 92},
+        [20] = { region="Glenumbra", cw = 6, ccw = 2},
+        [6] = { region="Glenumbra", cw = 2, ccw = 20},
+        [2] = { region="Glenumbra", cw = 20, ccw = 6},
+        [207] = { region="Grahtwood", cw = 164, ccw = 21},
+        [164] = { region="Grahtwood", cw = 21, ccw = 207},
+        [21] = { region="Grahtwood", cw = 207, ccw = 164},
+        [148] = { region="Greenshade", cw = 152, ccw = 149},
+        [152] = { region="Greenshade", cw = 149, ccw = 148},
+        [149] = { region="Greenshade", cw = 148, ccw = 152},
+        [103] = { region="Malabal Tor", cw = 100, ccw = 105},
+        [100] = { region="Malabal Tor", cw = 105, ccw = 103},
+        [105] = { region="Malabal Tor", cw = 103, ccw = 100},
+        [162] = { region="Reaper's March", cw = 158, ccw = 157},
+        [158] = { region="Reaper's March", cw = 157, ccw = 162},
+        [157] = { region="Reaper's March", cw = 162, ccw = 158},
+        [10] = { region="Ravenspire", cw = 13, ccw = 86},
+        [13] = { region="Ravenspire", cw = 86, ccw = 10},
+        [86] = { region="Ravenspire", cw = 10, ccw = 13},
+        [51] = { region="Shadowfen", cw = 53, ccw = 47},
+        [53] = { region="Shadowfen", cw = 47, ccw = 51},
+        [47] = { region="Shadowfen", cw = 51, ccw = 53},
+        [73] = { region="Stonefalls", cw = 67, ccw = 71},
+        [67] = { region="Stonefalls", cw = 71, ccw = 73},
+        [71] = { region="Stonefalls", cw = 73, ccw = 67},
+        [17] = { region="Stormhaven", cw = 19, ccw = 31},
+        [19] = { region="Stormhaven", cw = 31, ccw = 17},
+        [31] = { region="Stormhaven", cw = 17, ccw = 19},
+        [110] = { region="The Rift", cw = 116, ccw = 114},
+        [116] = { region="The Rift", cw = 114, ccw = 110},
+        [114] = { region="The Rift", cw = 110, ccw = 116}
+    }
+}
+
+function Runner:Start()
+    LeoDolmenRunner.log("Starting runner")
+    ZO_ShallowTableCopy(Runner.defaultData, Runner.data)
+    Runner.data.startedTime = GetTimeStamp()
+    Runner.started = true
+    LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Stop")
+    self:GetActiveBuff()
+end
+
+function Runner:Stop()
+    LeoDolmenRunner.log("Stopping runner")
+    Runner.started = false
+    LeoDolmenRunnerWindowPanelStartStopLabel:SetText("Start")
+end
+
+function Runner:StartStop()
+    if Runner.started then
+        Runner:Stop()
+    else
+        Runner:Start()
+    end
+end
+
+function Runner:CW()
+    LeoDolmenRunner.log("Changing direction to clock wise")
+    Runner.data.direction = Runner.directions.cw
+    LeoDolmenRunnerWindowPanelOrientationLabel:SetText("CW")
+end
+
+function Runner:CCW()
+    LeoDolmenRunner.log("Changing direction to counter clock wise")
+    Runner.data.direction = Runner.directions.ccw
+    LeoDolmenRunnerWindowPanelOrientationLabel:SetText("CCW")
+end
+
+function Runner:CWCCW()
+    if Runner.data.direction == Runner.directions.cw then
+        Runner:CCW()
+    else
+        Runner:CW()
+    end
+end
+
+function Runner:OnFastTravelInteraction(currentWayshrine)
+    if not Runner.started or Runner.wayshrines[currentWayshrine] == nil then return end
+
+    local nextWayshrine = nil
+    if Runner.data.direction == Runner.directions.cw then
+        nextWayshrine = Runner.wayshrines[currentWayshrine].cw
+    else
+        nextWayshrine = Runner.wayshrines[currentWayshrine].ccw
+    end
+    local discovered, name = GetFastTravelNodeInfo(nextWayshrine)
+    if not discovered then
+        LDR.log("Can't fast travel: " .. name .. " not discovered yet.")
+        return
+    end
+    FastTravelToNode(nextWayshrine)
+end
+
+local function calculateXpPerMinute(xp, timestamp)
+    local diff = GetDiffBetweenTimeStamps(GetTimeStamp(), timestamp)
+    diff = (diff == 0) and 1 or diff
+
+    return xp / (diff / 60)
+end
+
+local function TimeToLevel()
+    local xpToLevel = 0
+    if (CanUnitGainChampionPoints("player")) then
+        xpToLevel = GetNumChampionXPInChampionPoint(GetPlayerChampionPointsEarned("player")) - GetPlayerChampionXP()
+    else
+        xpToLevel = GetNumExperiencePointsInLevel(GetUnitLevel("player")) - GetUnitXP("player")
+    end
+
+    local minutesToLevel = math.ceil(xpToLevel / Runner.data.xpPerMinute)
+
+    if minutesToLevel < 60 then return 0, minutesToLevel end
+
+    local hoursToLevel = math.floor(minutesToLevel / 60)
+    minutesToLevel = minutesToLevel - (hoursToLevel * 60)
+
+    return hoursToLevel, minutesToLevel
+end
+
+function Runner:UpdateData()
+    local xpGained = 0
+    local firstEventTimestamp = 0
+    if (#Runner.data.events > 0) then
+        for i, event in pairs(Runner.data.events) do
+            if GetDiffBetweenTimeStamps(GetTimeStamp(), event.timestamp) > Runner.timeout then
+                Runner.data.events[i] = nil
+            else
+                xpGained = xpGained + event.xp
+
+                if firstEventTimestamp == 0 or event.timestamp < firstEventTimestamp then
+                    firstEventTimestamp = event.timestamp
+                end
+
+            end
+        end
+    end
+
+    Runner.data.xpPerMinute = calculateXpPerMinute(xpGained, firstEventTimestamp)
+
+    if xpGained > 0 then
+        Runner.data.hoursToLevel, Runner.data.minutesToLevel = TimeToLevel()
+    end
+end
+
+function Runner:Update(tick)
+    if not Runner.started or tick ~= 5 then return end
+
+    self:UpdateData()
+end
+
+
+local assistants = {
+    267, -- Nuzhimeh
+    300, -- Fence
+    301, -- Tythis
+    6376, -- Ezabi
+    6378 -- Fezez
+}
+
+local buffs = {
+    { id =  479, from = 1022, to = 1103 }, -- Witchmother's Whistle
+    { id = 1167, from =  326, to =  403 }, -- The Pie of Misrule
+    { id = 1168, from = 1217, to = 1231 }, -- Breda's Bottomless Mead Mug
+    { id = 1168, from =  101, to =  105 }, -- Breda's Bottomless Mead Mug
+    { id = 7619 }, -- Jubilee Cake 2020
+    { id = 5886 }, -- Jubilee Cake 2019
+    { id = 4786 }, -- Jubilee Cake 2018
+    { id = 1109 }, -- Jubilee Cake 2017
+    { id =  356 }, -- Jubilee Cake 2016
+}
+
+local function isBuffActive(collectibleId)
+    for i = 1, GetNumBuffs("player") do
+        local buffName = GetUnitBuffInfo("player", i)
+        local referenceId = GetCollectibleReferenceId(collectibleId)
+        local abilityName = GetAbilityName(referenceId)
+        if abilityName == buffName then return true end
+    end
+    return false
+end
+
+function Runner:OnCombatState(inCombat)
+    if IsUnitDead("player") then
+        zo_callLater(function() Runner:OnCombatState(inCombat) end, 500)
+        return
+    end
+
+    if not inCombat and LeoDolmenRunner.settings.runner.reapplyBuff then
+        if Runner.activeBuff == 0 then
+            local date = tonumber(os.date("%m%d", GetTimeStamp()))
+            for _, collectible in ipairs(buffs) do
+                local _, _, _, _, unlocked = GetCollectibleInfo(collectible.id)
+                if unlocked and collectible.from and collectible.from <= date and collectible.to >= date then
+                    Runner.activeBuff = collectible.id
+                    break
+                end
+            end
+        end
+
+        if not isBuffActive(Runner.activeBuff) then
+            LeoDolmenRunner.log("Reapplying " .. GetCollectibleInfo(Runner.activeBuff))
+            UseCollectible(Runner.activeBuff)
+        end
+    end
+
+    if LeoDolmenRunner.settings.runner.autoDismiss then
+        for _, id in pairs(assistants) do
+            if IsCollectibleActive(id) then
+                LeoDolmenRunner.log("Dismissing " .. GetCollectibleInfo(id))
+                UseCollectible(id)
+            end
+        end
+    end
+end
+
+function Runner:OnExperienceGain(reason, level, previousExperience, currentExperience, championPoints)
+    local event = {
+        timestamp = GetTimeStamp(),
+        xp = currentExperience - previousExperience
+    }
+    table.insert(Runner.data.events, event)
+
+    if reason == 7 then Runner.data.dolmensClosed = Runner.data.dolmensClosed + 1 end
+
+    self:UpdateData()
+end
+
+function Runner:GetActiveBuff()
+    for _, collectible in pairs(buffs) do
+        if isBuffActive(collectible.id) then
+            Runner.activeBuff = collectible.id
+            -- LeoDolmenRunner.debug("Found buff " .. Runner.activeBuff)
+            return
+        end
+    end
+end
+
+function Runner:Initialize()
+    Runner.activeBuff = 0
+    self:GetActiveBuff()
+end
+
+LeoDolmenRunner.runner = Runner
diff --git a/Settings.lua b/Settings.lua
new file mode 100644
index 0000000..2c3eeca
--- /dev/null
+++ b/Settings.lua
@@ -0,0 +1,80 @@
+
+LeoDolmenRunner_Settings = ZO_Object:Subclass()
+local LAM = LibAddonMenu2
+
+function LeoDolmenRunner_Settings:New(...)
+    local object = ZO_Object.New(self)
+    object:Initialize(...)
+    return object
+end
+
+function LeoDolmenRunner_Settings:Initialize()
+end
+
+function LeoDolmenRunner_Settings:CreatePanel()
+    local OptionsName = "LeoDolmenRunnerOptions"
+    local panelData = {
+        type = "panel",
+        name = LeoDolmenRunner.name,
+        slashCommand = "/ldropt",
+        displayName = "|c39B027"..LeoDolmenRunner.displayName.."|r",
+        author = "@LeandroSilva",
+        version = LeoDolmenRunner.version,
+        registerForRefresh = true,
+        registerForDefaults = true,
+        website = "https://www.esoui.com/downloads/info2534-LeosDolmenRunner.html"
+    }
+    LAM:RegisterAddonPanel(OptionsName, panelData)
+
+    local optionsData = {
+        {
+            type = "header",
+            name = "|c3f7fffRunner|r"
+        },{
+            type = "checkbox",
+            name = "Auto dismiss assistants",
+            default = true,
+            width = "full",
+            getFunc = function() return LeoDolmenRunner.settings.runner.autoDismiss end,
+            setFunc = function(value) LeoDolmenRunner.settings.runner.autoDismiss = value end,
+        },{
+            type = "checkbox",
+            name = "Auto reapply holiday buff",
+            default = true,
+            width = "full",
+            getFunc = function() return LeoDolmenRunner.settings.runner.reapplyBuff end,
+            setFunc = function(value) LeoDolmenRunner.settings.runner.reapplyBuff = value end,
+        },{
+            type = "header",
+            name = "|c3f7fffAuto Inviter|r"
+        },{
+            type = "checkbox",
+            name = "Auto kick offline",
+            default = true,
+            width = "full",
+            getFunc = function() return LeoDolmenRunner.settings.inviter.autoKick end,
+            setFunc = function(value) LeoDolmenRunner.settings.inviter.autoKick = value end,
+        },{
+            name = "Auto kick delay (secs)",
+            type = "slider",
+            getFunc = function() return LeoDolmenRunner.settings.inviter.kickDelay end,
+            setFunc = function(value) LeoDolmenRunner.settings.inviter.kickDelay = value end,
+            min = 10,
+            max = 600,
+            default = 60,
+        },{
+            name = "Max group size",
+            type = "dropdown",
+            choices = {4, 12, 24},
+            getFunc = function() return LeoDolmenRunner.settings.inviter.maxSize end,
+            setFunc = function(value) LeoDolmenRunner.settings.inviter.maxSize = value end,
+            default = 24,
+        }
+    }
+    LAM:RegisterOptionControls(OptionsName, optionsData)
+end
+
+function LeoDolmenRunner_Settings_OnMouseEnter(control, tooltip)
+    InitializeTooltip(InformationTooltip, control, BOTTOMLEFT, 0, -2, TOPLEFT)
+    SetTooltipText(InformationTooltip, tooltip)
+end
diff --git a/Ui.lua b/Ui.lua
new file mode 100644
index 0000000..92e4e26
--- /dev/null
+++ b/Ui.lua
@@ -0,0 +1,165 @@
+local Ui = {}
+local LDR = LeoDolmenRunner
+
+local function getColor(value, max)
+    local rate = value / max
+    if rate > 0.8 then return '10FF10' end
+    if rate > 0.5 then return 'FFFF00' end
+    return 'FF1010'
+end
+
+function Ui:UpdateTrainingGear()
+    local stack = 0
+    local stackRepair = 0
+    local maxItems = 9
+    local numItems = 0
+    local bag = SHARED_INVENTORY:GenerateFullSlotData(nil,BAG_WORN)
+    for _, data in pairs(bag) do
+        local link = GetItemLink(data.bagId, data.slotIndex)
+        if GetItemLinkTraitType(link) == ITEM_TRAIT_TYPE_ARMOR_TRAINING or GetItemLinkTraitType(link) == ITEM_TRAIT_TYPE_WEAPON_TRAINING then
+            local activeWeaponPair = GetActiveWeaponPairInfo()
+            if ( GetItemLinkItemType(link) == ITEMTYPE_ARMOR
+                or
+                (activeWeaponPair == ACTIVE_WEAPON_PAIR_MAIN and (data.slotIndex == EQUIP_SLOT_MAIN_HAND or data.slotIndex == EQUIP_SLOT_OFF_HAND))
+                or
+                (activeWeaponPair == ACTIVE_WEAPON_PAIR_BACKUP and (data.slotIndex == EQUIP_SLOT_BACKUP_MAIN or data.slotIndex == EQUIP_SLOT_BACKUP_OFF)))
+            then
+                local quality = select(8, GetItemInfo(data.bagId, data.slotIndex))
+                local condition = GetItemCondition(data.bagId, data.slotIndex)
+                local equipType = select(6, GetItemInfo(data.bagId, data.slotIndex))
+                numItems = numItems + 1
+                local xp = 0
+                if GetItemLinkItemType(link) == ITEMTYPE_ARMOR then
+                    xp = 6 + quality
+                elseif equipType == EQUIP_TYPE_TWO_HAND then
+                    numItems = numItems + 1
+                    xp = 4 + quality
+                else
+                    xp = 2 + (0.5 * quality)
+                end
+                stack = stack + xp
+                if condition == 0 then stackRepair = stackRepair + xp end
+            end
+        end
+    end
+
+    local text = ""
+
+    if stackRepair > 0 then
+        text = "|c" .. getColor(stack - stackRepair, stack) .. " " .. (stack - stackRepair) .. " / " .. stack .. "%|r (Repair)"
+    else
+        text = "|c" .. getColor(stack, stack) .. stack .. "%|r"
+    end
+
+    LeoDolmenRunnerWindowPanelXpFromGear:SetText(text)
+    LeoDolmenRunnerWindowPanelTrainingGear:SetText("|c" .. getColor(numItems, maxItems) .. numItems .. " / " .. maxItems .. "|r")
+end
+
+function Ui:CreateUI()
+    LeoDolmenRunnerWindowPanelStartStopLabel:SetText(LDR.runner.started and "Stop" or "Start")
+    LeoDolmenRunnerWindowPanelOrientationLabel:SetText(LDR.runner.data.direction == LDR.runner.directions.cw and "CW" or "CCW")
+
+    LeoDolmenRunnerWindowInviterPanel:SetHidden(not IsUnitSoloOrGroupLeader("player"))
+    LeoDolmenRunnerWindowInviterPanelMessage:SetText("x")
+
+    self:UpdateTrainingGear()
+end
+
+local function FormatNumber(num)
+    return zo_strformat("<<1>>", ZO_LocalizeDecimalNumber(num))
+end
+
+local function FormatTime(seconds, short, colorizeCountdown)
+    if short == nil then short = false end
+    local formats = {
+        dhm = SI_TIME_FORMAT_DDHHMM_DESC_SHORT,
+        day = SI_TIME_FORMAT_DAYS,
+        hm = SI_TIME_FORMAT_HHMM_DESC_SHORT,
+        hms = SI_TIME_FORMAT_HHMMSS_DESC_SHORT,
+        hour = SI_TIME_FORMAT_HOURS,
+        ms = SI_TIME_FORMAT_MMSS_DESC_SHORT,
+        m = SI_TIME_FORMAT_MINUTES
+    }
+    if seconds and seconds > 0 then
+        local ss = seconds % 60
+        local mm = math.floor(seconds / 60)
+        local hh = math.floor(mm / 60)
+        mm = mm % 60
+        local dn = math.floor(hh / 24)
+        local hhdn = hh - (dn*24)
+
+        local ssF = string.format("%02d", ss)
+        local mmF = string.format("%02d", mm)
+        local hhF = string.format("%02d", hh)
+        local hhdnF = string.format("%02d", hhdn)
+
+        local result = ''
+        if dn > 0 then
+            if short then
+                result = ZO_CachedStrFormat(GetString(formats.day), dn) .." "..ZO_CachedStrFormat(GetString(formats.hour), hhdnF)
+            else
+                result = ZO_CachedStrFormat(GetString(formats.dhm), dn, hhdnF, mmF)
+            end
+        elseif hh > 0 then
+            if short then
+                result = ZO_CachedStrFormat(GetString(formats.hm), hhF, mmF)
+            else
+                result = ZO_CachedStrFormat(GetString(formats.hms), hhF, mmF, ssF)
+            end
+        elseif mm >= 0 then result = ZO_CachedStrFormat(GetString(formats.ms), mmF, ssF)
+        end
+        return result
+    else return ZO_CachedStrFormat(GetString(formats.m), 0) end
+end
+
+function Ui:Update(tick)
+
+    if not LDR.runner.started then return end
+
+    if tick == 5 then
+        self:UpdateTrainingGear()
+    end
+
+    if LDR.runner.started and LeoDolmenRunner.runner.data.startedTime > 0 then
+        local secs = GetTimeStamp() - LeoDolmenRunner.runner.data.startedTime
+        if secs > 0 then
+            LeoDolmenRunnerWindowPanelTime:SetText(FormatTime(secs))
+        end
+    end
+
+    LeoDolmenRunnerWindowPanelDolmensClosed:SetText(LDR.runner.data.dolmensClosed)
+    LeoDolmenRunnerWindowPanelXP:SetText(FormatNumber(math.ceil(LDR.runner.data.xpPerMinute)) .. " / min")
+
+    LeoDolmenRunnerWindowPanelXPNext:SetText(ZO_CachedStrFormat(GetString(SI_TIME_FORMAT_HHMM_DESC_SHORT), LDR.runner.data.hoursToLevel, LDR.runner.data.minutesToLevel))
+
+    LeoDolmenRunnerWindowInviterPanelMessage:SetText("x")
+end
+
+function Ui:OnWindowMoveStop()
+    LDR.settings.position = {
+        left = self.frame:GetLeft(),
+        top = self.frame:GetTop()
+    }
+end
+
+function Ui:RestorePosition()
+    local position = LDR.settings.position or { left = 200; top = 200; }
+    local left = position.left
+    local top = position.top
+
+    self.frame:ClearAnchors()
+    self.frame:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, left, top)
+    self.frame:SetDrawTier(DT_MEDIUM)
+end
+
+function Ui:Initialize()
+    self.frame = LeoDolmenRunnerWindow
+
+    self:RestorePosition()
+
+    self:CreateUI()
+
+    self.frame:SetHidden(LeoDolmenRunner.settings.hidden)
+end
+
+LeoDolmenRunner.ui = Ui