annotate ObjectiveWidgets.lua @ 18:d1812fb10ae6

ObjectiveStyle - move tag/template logic into the corresponding GetInfo
author Nenue
date Tue, 05 Apr 2016 02:38:01 -0400
parents 880828018bf4
children 605e8f0e46db
rev   line source
Nenue@13 1 local B = select(2,...).frame
Nenue@13 2 local mod = B:RegisterModule("ObjectiveTracker", _G.VeneerObjectiveWrapper, 'BuffFrame')
Nenue@13 3 local print = B.print('WidgetFactory')
Nenue@13 4 local UIParent = UIParent
Nenue@13 5 local GetQuestLogSpecialItemInfo, IsQuestLogSpecialItemInRange, GetQuestLogSpecialItemCooldown = GetQuestLogSpecialItemInfo, IsQuestLogSpecialItemInRange, GetQuestLogSpecialItemCooldown
Nenue@13 6 local CooldownFrame_SetTimer, SetItemButtonTextureVertexColor, CreateFrame, VeneerObjectiveScroll = CooldownFrame_SetTimer, SetItemButtonTextureVertexColor, CreateFrame, VeneerObjectiveScroll
Nenue@13 7 local tremove, tinsert, tContains, pairs, setmetatable = tremove, tinsert, tContains, pairs, setmetatable
Nenue@14 8
Nenue@14 9 --- frame refs
Nenue@14 10 local Wrapper = _G.VeneerObjectiveWrapper
Nenue@14 11 local Scroller = Wrapper.scrollArea
Nenue@14 12 local CloseButton = Wrapper.CloseButton
Nenue@14 13 local QuestMapButton = Wrapper.QuestMapButton
Nenue@14 14 local Scroll = _G.VeneerObjectiveScroll
Nenue@14 15
Nenue@14 16 local panelButtons = {
Nenue@14 17 CloseButton = {
Nenue@14 18 closedSwatch = {
Nenue@14 19 [[Interface\Buttons\UI-Panel-QuestHideButton]],
Nenue@14 20 [[Interface\Buttons\UI-Panel-QuestHideButton]],
Nenue@14 21 0, 0.5, 0.5, 1,
Nenue@14 22 0.5, 1, 0.5, 1,
Nenue@14 23 },
Nenue@14 24 openSwatch = {
Nenue@14 25 [[Interface\Buttons\UI-Panel-QuestHideButton]],
Nenue@14 26 [[Interface\Buttons\UI-Panel-QuestHideButton]],
Nenue@14 27 0.5, 1, 0.5, 1,
Nenue@14 28 0, 0.5, 0.5, 1,
Nenue@14 29 },
Nenue@14 30 parent = 'VeneerObjectiveWrapper'
Nenue@14 31 },
Nenue@14 32 QuestMapButton = {
Nenue@14 33 closedSwatch = {
Nenue@14 34 [[Interface\QUESTFRAME\UI-QUESTMAP_BUTTON]],
Nenue@14 35 [[Interface\QUESTFRAME\UI-QUESTMAP_BUTTON]],
Nenue@14 36 0, 1, 0.5, 1,
Nenue@14 37 0, 1, 0, 0.5,
Nenue@14 38 },
Nenue@14 39 openSwatch = {
Nenue@14 40 [[Interface\QUESTFRAME\UI-QUESTMAP_BUTTON]],
Nenue@14 41 [[Interface\QUESTFRAME\UI-QUESTMAP_BUTTON]],
Nenue@14 42 0, 1, 0, 0.5,
Nenue@14 43 0, 1, 0.5, 1,
Nenue@14 44 }
Nenue@14 45 }
Nenue@14 46 }
Nenue@14 47
Nenue@14 48 local Scroller_OnShow = function()
Nenue@14 49 Wrapper.watchMoneyReasons = 0;
Nenue@14 50 mod.UpdateWrapper()
Nenue@14 51 mod.SetEvents()
Nenue@14 52 for i, region in ipairs(Wrapper.headerComplex) do
Nenue@14 53 region:Show()
Nenue@14 54 end
Nenue@14 55 end
Nenue@14 56
Nenue@14 57 local Scroller_OnHide = function()
Nenue@14 58 local self = Wrapper
Nenue@14 59 Wrapper:UnregisterAllEvents()
Nenue@14 60 Wrapper:SetScript('OnEvent', nil)
Nenue@14 61 for i, region in ipairs(Wrapper.headerComplex) do
Nenue@14 62 region:Hide()
Nenue@14 63 end
Nenue@14 64 end
Nenue@14 65
Nenue@14 66 local Scroller_OnMouseWheel = function(self, delta)
Nenue@14 67 local r = Scroll:GetHeight() - Scroller:GetHeight()
Nenue@14 68 local s = B.Conf.ObjectiveScroll - delta * floor(r/5+.5)
Nenue@14 69 local from = self:GetVerticalScroll()
Nenue@14 70 if s >= r then
Nenue@14 71 s = r
Nenue@14 72 elseif s < 1 then
Nenue@14 73 s = 0
Nenue@14 74 end
Nenue@14 75 self:SetVerticalScroll(s)
Nenue@14 76 B.Conf.ObjectiveScroll = s
Nenue@14 77 print('|cFF00FF00OnMouseWheel', 'from = ', from, 'scroll =', s, ' range =', r, 'current =', self:GetVerticalScroll())
Nenue@14 78
Nenue@14 79 mod.UpdateActionButtons('SCROLLING')
Nenue@14 80 end
Nenue@14 81
Nenue@14 82 local UpdatePanelButton = function (self, state)
Nenue@14 83 state = state and B.Conf.FrameState[state] or 1
Nenue@14 84 local swatch = (state == 1) and self.openSwatch or self.closedSwatch
Nenue@14 85 self:SetNormalTexture(swatch[1])
Nenue@14 86 self:SetPushedTexture(swatch[2])
Nenue@14 87 if #swatch >= 6 then
Nenue@14 88 self:GetNormalTexture():SetTexCoord(swatch[3], swatch[4], swatch[5], swatch[6])
Nenue@14 89 end
Nenue@14 90 if #swatch == 10 then
Nenue@14 91 self:GetPushedTexture():SetTexCoord(swatch[7], swatch[8], swatch[9], swatch[10])
Nenue@14 92 end
Nenue@14 93
Nenue@14 94 end
Nenue@14 95
Nenue@14 96 local OnClick = {}
Nenue@14 97 OnClick.CloseButton = function(self)
Nenue@14 98 Wrapper:Minimize()
Nenue@14 99 UpdatePanelButton(self, self.parent)
Nenue@14 100 end
Nenue@14 101
Nenue@14 102 OnClick.QuestMapButton = function()
Nenue@14 103 ToggleWorldMap()
Nenue@14 104 end
Nenue@14 105
Nenue@14 106 mod.InitializeWrapperWidgets = function()
Nenue@14 107 --- tracker scroll
Nenue@14 108 Scroller:SetScript('OnMouseWheel', Scroller_OnMouseWheel)
Nenue@14 109 Scroller:SetScript('OnShow', Scroller_OnShow)
Nenue@14 110 Scroller:SetScript('OnHide', Scroller_OnHide)
Nenue@14 111 for name, swatch in pairs(panelButtons) do
Nenue@14 112 local source = swatch and swatch or panelButtons.CloseButton
Nenue@14 113 local button = Wrapper[name]
Nenue@14 114 button.parent = swatch.parent
Nenue@14 115 button.openSwatch = source.openSwatch
Nenue@14 116 button.closedSwatch = source.closedSwatch
Nenue@14 117 if OnClick[name] then
Nenue@14 118 button:SetScript('OnClick', OnClick[name])
Nenue@14 119 end
Nenue@14 120 UpdatePanelButton(button, button.parent)
Nenue@14 121 end
Nenue@14 122 end
Nenue@14 123
Nenue@13 124 ----------------------------------------------------------------------------------------
Nenue@13 125 --- XML and script code lifted from "QuestKing 2" by Barjack,
Nenue@13 126 --- found at http://mods.curse.com/addons/wow/questking
Nenue@13 127 ----------------------------------------------------------------------------------------
Nenue@13 128 local usedButtons = mod.Quest.itemButtons
Nenue@13 129 local freeButtons = mod.Quest.freeButtons
Nenue@13 130 mod.SetItemButton = function(block, info)
Nenue@13 131 local itemInfo = info.specialItem
Nenue@13 132 if not itemInfo then
Nenue@13 133 return
Nenue@13 134 end
Nenue@13 135
Nenue@13 136 --- Quest.GetInfo().specialItem :: {link = link, charges = charges, icon = icon, start = start, duration = duration, enable = enable}
Nenue@13 137
Nenue@13 138
Nenue@13 139 local itemButton
Nenue@13 140 if not info.itemButton then
Nenue@13 141 if #freeButtons >= 1 then
Nenue@13 142 print(' |cFF00FFFFfound a free button')
Nenue@13 143 itemButton = freeButtons[#freeButtons]
Nenue@13 144 freeButtons[#freeButtons] = nil
Nenue@13 145 if itemButton.block then
Nenue@13 146 itemButton.block.itemButton = nil
Nenue@13 147 itemButton.block = nil
Nenue@13 148 end
Nenue@13 149 else
Nenue@13 150 local buttonIndex = mod.Quest.numButtons + #freeButtons + 1
Nenue@13 151 itemButton = CreateFrame('Button', 'VeneerQuestItemButton' .. buttonIndex, UIParent, 'VeneerItemButtonTemplate')
Nenue@13 152 itemButton.buttonIndex = buttonIndex
Nenue@13 153 itemButton:SetSize(36, 36)
Nenue@13 154 itemButton:GetNormalTexture():SetSize(36 * (5/3), 36 * (5/3))
Nenue@13 155 print(' |cFFFF4400starting new button', itemButton:GetName())
Nenue@13 156 end
Nenue@13 157 mod.Quest.numButtons = mod.Quest.numButtons + 1
Nenue@13 158 else
Nenue@13 159 itemButton = info.itemButton
Nenue@13 160 print(' |cFF00FF00found assigned button', itemButton:GetName())
Nenue@13 161
Nenue@13 162 end
Nenue@13 163 -- set values
Nenue@13 164
Nenue@13 165 info.itemButton = itemButton
Nenue@13 166 usedButtons[info.questID] = itemButton
Nenue@13 167 print(' |cFF8800FFassigning|r', itemButton:GetName(), 'to quest|cFF00FF00', info.questID, '|rat|cFFFFFF00', block:GetName(),'|r')
Nenue@13 168
Nenue@13 169 for k,v in pairs(usedButtons) do
Nenue@13 170 print('|cFFFF44DD'..k..'|r', v:GetName())
Nenue@13 171 end
Nenue@13 172
Nenue@13 173 itemButton:SetAttribute("type", "item")
Nenue@13 174 itemButton:SetAttribute("item", itemInfo.link)
Nenue@13 175
Nenue@13 176 itemButton.questID = info.questID
Nenue@13 177 itemButton.questLogIndex = info.questLogIndex
Nenue@13 178 itemButton.charges = itemInfo.charges
Nenue@13 179 itemButton.rangeTimer = -1
Nenue@13 180 itemButton.block = block
Nenue@13 181
Nenue@13 182 SetItemButtonTexture(itemButton, itemInfo.icon)
Nenue@13 183 SetItemButtonCount(itemButton, itemInfo.charges)
Nenue@13 184 Veneer_QuestObjectiveItem_UpdateCooldown(itemButton);
Nenue@13 185
Nenue@13 186 return itemButton
Nenue@13 187 end
Nenue@13 188 --- Clear an itemButton from the given block
Nenue@13 189 mod.FreeItemButtons = function(block)
Nenue@13 190
Nenue@13 191 if block.itemButton then
Nenue@13 192 local itemButton = block.itemButton
Nenue@13 193 if itemButton.questID ~= block.info.questID then
Nenue@13 194 block.itemButton = nil
Nenue@13 195 itemButton.block = mod.Quest.InfoBlock[itemButton.questID]
Nenue@13 196 else
Nenue@13 197 itemButton.block = nil
Nenue@13 198 itemButton:Hide()
Nenue@13 199
Nenue@13 200 usedButtons[itemButton.questID] = nil
Nenue@13 201 freeButtons[#freeButtons + 1] = itemButton
Nenue@13 202 mod.Quest.numButtons = mod.Quest.numButtons - 1
Nenue@13 203 print('|cFFFF0088released', itemButton:GetName(),'and', block:GetName())
Nenue@13 204 end
Nenue@13 205 end
Nenue@13 206 end
Nenue@13 207
Nenue@13 208 function Veneer_QuestObjectiveItem_OnUpdate (self, elapsed)
Nenue@13 209 -- Handle range indicator
Nenue@13 210 local rangeTimer = self.rangeTimer
Nenue@13 211 if (rangeTimer) then
Nenue@13 212 rangeTimer = rangeTimer - elapsed
Nenue@13 213 if (rangeTimer <= 0) then
Nenue@13 214 local link, item, charges, showItemWhenComplete = GetQuestLogSpecialItemInfo(self.questLogIndex)
Nenue@13 215 if ((not charges) or (charges ~= self.charges)) then
Nenue@13 216 mod.UpdateWrapper()
Nenue@13 217 return
Nenue@13 218 end
Nenue@13 219
Nenue@13 220 local count = self.HotKey
Nenue@13 221 local valid = IsQuestLogSpecialItemInRange(self.questLogIndex)
Nenue@13 222 if (valid == 0) then
Nenue@13 223 count:Show()
Nenue@13 224 count:SetVertexColor(1.0, 0.1, 0.1)
Nenue@13 225 elseif (valid == 1) then
Nenue@13 226 count:Show()
Nenue@13 227 count:SetVertexColor(0.6, 0.6, 0.6)
Nenue@13 228 else
Nenue@13 229 count:Hide()
Nenue@13 230 end
Nenue@13 231 rangeTimer = TOOLTIP_UPDATE_TIME
Nenue@13 232 end
Nenue@13 233
Nenue@13 234 self.rangeTimer = rangeTimer
Nenue@13 235 end
Nenue@13 236 end
Nenue@13 237
Nenue@13 238 function Veneer_QuestObjectiveItem_UpdateCooldown (itemButton)
Nenue@13 239 local start, duration, enable = GetQuestLogSpecialItemCooldown(itemButton.questLogIndex)
Nenue@13 240 if (start) then
Nenue@13 241 CooldownFrame_SetTimer(itemButton.Cooldown, start, duration, enable)
Nenue@13 242 if (duration > 0 and enable == 0) then
Nenue@13 243 SetItemButtonTextureVertexColor(itemButton, 0.4, 0.4, 0.4)
Nenue@13 244 else
Nenue@13 245 SetItemButtonTextureVertexColor(itemButton, 1, 1, 1)
Nenue@13 246 end
Nenue@13 247 end
Nenue@13 248 end
Nenue@13 249
Nenue@13 250 -----------------------------------------
Nenue@13 251 -- Criteria frames
Nenue@13 252
Nenue@13 253 --[[
Nenue@13 254 text = description,
Nenue@13 255 type = type,
Nenue@13 256 finished = completed,
Nenue@13 257 quantity = quantity,
Nenue@13 258 requiredQuantity = requiredQuantity,
Nenue@13 259 characterName = characterName,
Nenue@13 260 flags = flags,
Nenue@13 261 assetID = assetID,
Nenue@13 262 quantityString = quantityString,
Nenue@13 263 criteriaID = criteriaID,
Nenue@13 264 ]]
Nenue@13 265 local newWidgetID = 0
Nenue@13 266 mod.WidgetRegistry = {}
Nenue@13 267 local wr = mod.WidgetRegistry
Nenue@13 268
Nenue@13 269 --- Get a usable widget for the given achievement criteria set.
Nenue@13 270 -- Returns a frame object with dimensioning parameters needed to size the receiving tracker block
Nenue@14 271 mod.SetWidget = function(line, info, objectiveType, objectiveKey)
Nenue@13 272 local print = B.print('ObjectiveWidgets')
Nenue@14 273 local widgetType = objectiveType
Nenue@13 274 local widget
Nenue@14 275 if wr[widgetType] and wr[widgetType].used[objectiveKey] then
Nenue@14 276 widget = wr[widgetType].used[objectiveKey]
Nenue@14 277 print('|cFF00FF00Updating ('..objectiveKey..')', widget)
Nenue@13 278 elseif not wr[widgetType] or #wr[widgetType].free == 0 then
Nenue@13 279 widget = CreateFrame('Frame', 'VeneerObjective' .. widgetType .. (wr[widgetType] and (wr[widgetType].lastn+1) or (1)), VeneerObjectiveScroll, 'VeneerObjectiveCriteria' .. widgetType)
Nenue@13 280
Nenue@13 281 print('|cFFFF0088Creating `'..widget:GetName()..'` id', wr[widgetType].lastn)
Nenue@13 282 else
Nenue@13 283 widget = tremove(wr[widgetType].free)
Nenue@13 284 print('|cFFFFFF00Acquiring released widget', widget:GetName())
Nenue@13 285 end
Nenue@13 286
Nenue@14 287
Nenue@14 288 wr[widgetType].used[objectiveKey] = widget
Nenue@14 289 widget.line = line
Nenue@14 290 widget.info = info
Nenue@14 291 widget.key = objectiveKey
Nenue@13 292 mod.InitializeWidget(widget)
Nenue@13 293 return widget
Nenue@13 294 end
Nenue@13 295
Nenue@13 296 --- WidgetTemplate 'OnLoad'
Nenue@13 297 mod.RegisterWidget = function(frame)
Nenue@13 298 local print = B.print('ObjectiveWidgets')
Nenue@13 299 local widgetType = frame.widgetType
Nenue@13 300 if not wr[frame.widgetType] then
Nenue@13 301 print('|cFFFF4400[[WidgetTemplate]]|r', widgetType)
Nenue@13 302 wr[widgetType] = { lastn = 1, free = {}, used = {}, usedIndex = {}, freeIndex = {} }
Nenue@13 303 else
Nenue@13 304 print('|cFF0088FF+ [[WidgetTemplate]]r', widgetType, wr[widgetType].lastn)
Nenue@13 305 wr[widgetType].lastn = wr[widgetType].lastn + 1
Nenue@13 306 end
Nenue@13 307 end
Nenue@13 308
Nenue@13 309 --- WidgetTemplate 'OnShow'
Nenue@13 310 mod.InitializeWidget = setmetatable({}, {
Nenue@13 311 __call = function(t, frame)
Nenue@13 312 -- todo: config pull
Nenue@13 313
Nenue@15 314 frame:SetWidth(mod.Conf.Wrapper.WrapperWidth - mod.Conf.Style.Format.status.Indent * 2)
Nenue@13 315 frame:SetScript('OnEvent', mod.UpdateWidget[frame.widgetType])
Nenue@13 316 if frame.info.isCurrency then
Nenue@13 317 frame:RegisterEvent('CHAT_MSG_CURRENCY')
Nenue@13 318 frame:RegisterEvent('CURRENCY_LIST_UPDATE')
Nenue@13 319 end
Nenue@13 320 frame:RegisterEvent('TRACKED_ACHIEVEMENT_UPDATE')
Nenue@13 321 frame:RegisterEvent('TRACKED_ACHIEVEMENT_LIST_CHANGED')
Nenue@13 322 frame:RegisterEvent('CRITERIA_UPDATE')
Nenue@13 323 frame:RegisterEvent('CRITERIA_COMPLETE')
Nenue@13 324 frame:RegisterEvent('CRITERIA_EARNED')
Nenue@14 325 t[frame.widgetType](frame)
Nenue@14 326 mod.UpdateWidget[frame.widgetType](frame)
Nenue@13 327 end,
Nenue@13 328 })
Nenue@13 329
Nenue@13 330 --- WidgetTemplate 'OnEvent'
Nenue@13 331 mod.UpdateWidget = setmetatable({}, {
Nenue@13 332 __call = function(t, frame)
Nenue@13 333 if not frame.widgetType then
Nenue@13 334 error('Invalid widget template, needs .widgetType')
Nenue@13 335 return
Nenue@13 336 end
Nenue@13 337
Nenue@13 338 return t[frame.widgetType](frame)
Nenue@13 339 end
Nenue@13 340 })
Nenue@13 341
Nenue@13 342 --- WidgetTemplate 'OnHide'
Nenue@13 343 mod.ReleaseWidget = function(frame)
Nenue@16 344 --[[
Nenue@13 345 local print = B.print('ObjectiveWidgets')
Nenue@13 346 local reg = wr[frame.widgetType]
Nenue@14 347 if reg and reg.used[frame.key] then
Nenue@14 348 reg.used[frame.key] = nil
Nenue@14 349 frame.line = nil
Nenue@13 350 frame.info = nil
Nenue@13 351 frame:UnregisterAllEvents()
Nenue@13 352 tinsert(reg.free, frame)
Nenue@13 353 print('|cFFBBBBBBreleased from service', frame:GetName())
Nenue@13 354 end
Nenue@16 355 ]]
Nenue@13 356 end
Nenue@13 357
Nenue@13 358 --- RemoveTrackedAchievement post-hook
Nenue@13 359 mod.CleanWidgets = function()
Nenue@13 360 local print = B.print('ObjectiveWidgets')
Nenue@13 361 local tracked = {GetTrackedAchievements() }
Nenue@14 362 local tasks = GetTasksTable()
Nenue@13 363 for type, reg in pairs(mod.WidgetRegistry) do
Nenue@13 364 print('collecting', type)
Nenue@14 365 for key, frame in pairs(reg.used) do
Nenue@14 366 if frame.info.cheevID then
Nenue@14 367 local id = frame.info.cheevID
Nenue@13 368
Nenue@14 369 if id and not tContains(tracked, id) then
Nenue@13 370
Nenue@14 371 print(' untracked achievement', id, 'associated with', key, frame:GetName())
Nenue@14 372 frame:Hide()
Nenue@14 373 end
Nenue@14 374 elseif frame.info.questID then
Nenue@14 375 -- do something for quest task
Nenue@13 376 end
Nenue@13 377 end
Nenue@13 378 end
Nenue@13 379 end
Nenue@13 380
Nenue@13 381
Nenue@14 382
Nenue@14 383 mod.defaults.WidgetStyle = {
Nenue@13 384 ProgressBar = {
Nenue@14 385 Spacing = 4,
Nenue@13 386 bg = {
Nenue@13 387 Height = 20,
Nenue@13 388 },
Nenue@13 389 fg = {
Nenue@13 390 Height = 16,
Nenue@13 391 },
Nenue@13 392 status = {
Nenue@14 393 FontObject = _G.VeneerCriteriaFontNormal
Nenue@13 394 }
Nenue@13 395 }
Nenue@13 396 }
Nenue@13 397 mod.InitializeWidget.ProgressBar = function(self)
Nenue@14 398 local c = mod.defaults.WidgetStyle.ProgressBar
Nenue@14 399 self.height = c.bg.Height + c.Spacing
Nenue@13 400 self:SetHeight(c.bg.Height)
Nenue@13 401 self.bg:SetHeight(c.bg.Height)
Nenue@13 402 self.fg:ClearAllPoints()
Nenue@14 403 self.indent = (c.bg.Height - c.fg.Height) / 2
Nenue@14 404 self.fg:SetPoint('BOTTOMLEFT', self, 'BOTTOMLEFT', self.indent, self.indent)
Nenue@13 405 self.fg:SetHeight(c.fg.Height)
Nenue@13 406 self.status:SetFontObject(c.status.FontObject)
Nenue@13 407 self.status:SetText(self.info.quantityString)
Nenue@13 408 end
Nenue@13 409
Nenue@13 410 mod.UpdateWidget.ProgressBar = function (self)
Nenue@14 411 local quantity, requiredQuantity = self.line.value, self.line.maxValue
Nenue@14 412 print('update vals:')
Nenue@14 413 for k,v in pairs(self.line) do
Nenue@14 414 print(k, v)
Nenue@14 415 end
Nenue@13 416
Nenue@14 417 if self.line.format then
Nenue@14 418 self.status:SetFormattedText(self.line.format, self.line.value, self.line.maxValue)
Nenue@14 419 end
Nenue@14 420
Nenue@14 421
Nenue@15 422 if quantity == 0 then
Nenue@15 423 self.fg:Hide()
Nenue@15 424 elseif quantity >= requiredQuantity then
Nenue@14 425 self.fg:SetWidth(self.bg:GetWidth() - self.indent)
Nenue@13 426 else
Nenue@13 427 self.fg:Show()
Nenue@14 428 self.fg:SetWidth((self.bg:GetWidth() -self.indent) * (quantity / requiredQuantity))
Nenue@13 429 end
Nenue@13 430 end
Nenue@13 431
Nenue@13 432
Nenue@13 433 mod.InitializeWidget.Hidden = function (self)
Nenue@13 434 self.height = 0
Nenue@13 435 end
Nenue@13 436 mod.UpdateWidget.Hidden = function (self)
Nenue@13 437 self.height= 0
Nenue@13 438 end