---------------------------------------------------- -- SamUI v0.3.3 -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("lootfenster") local default_settings = { size = 100, summary_enabled = true, summary_gold_enabled = true, summary_quality_threshold = 1, summary_duration = 4, } local window_width = 200 local window_padding = 8 local header_height = 30 local row_height = 40 local row_spacing = 6 local icon_size = 30 local maximum_visible_rows = 4 local summary_slot_count = 5 local summary_row_height = 24 local summary_border_gap = 5 local summary_icon_inset = 2 local summary_icon_size = summary_row_height - (summary_icon_inset * 2) local valid_summary_quality_thresholds = { [0] = true, [1] = true, [2] = true, [3] = true, } local valid_summary_durations = { [2] = true, [4] = true, [6] = true, [8] = true, } local loot_message_format_definitions = { { global_name = "LOOT_ITEM_SELF_MULTIPLE", multiple = true }, { global_name = "LOOT_ITEM_PUSHED_SELF_MULTIPLE", multiple = true }, { global_name = "LOOT_ITEM_CREATED_SELF_MULTIPLE", multiple = true }, { global_name = "LOOT_ITEM_SELF", multiple = false }, { global_name = "LOOT_ITEM_PUSHED_SELF", multiple = false }, { global_name = "LOOT_ITEM_CREATED_SELF", multiple = false }, } local loot_frame_states = setmetatable({}, { __mode = "k" }) local missing_frame_warning_shown = false local missing_template_warning_shown = false local loot_frame_show_hooked = false local loot_frame_update_hooked = false local loot_message_patterns = {} local native_visual_global_names = { "LootFramePortraitOverlay", "LootFramePrev", "LootFrameNext", "LootFrameBg", "LootFrameTitleBg", "LootFrameTopTileStreaks", "LootFrameTopLeftCorner", "LootFrameTopRightCorner", "LootFrameBotLeftCorner", "LootFrameBotRightCorner", "LootFrameTopBorder", "LootFrameBottomBorder", "LootFrameLeftBorder", "LootFrameRightBorder", "LootFramePortrait", "LootFramePortraitFrame", "LootFramePortraitContainer", "LootFrameTitleContainer", "LootFrameInset", "LootFrameNineSlice", "LootFrameTitleText", } local native_visual_field_names = { "PortraitOverlay", "portraitOverlay", "Bg", "bg", "TitleBg", "titleBg", "TopTileStreaks", "topTileStreaks", "TopLeftCorner", "topLeftCorner", "TopRightCorner", "topRightCorner", "BotLeftCorner", "botLeftCorner", "BotRightCorner", "botRightCorner", "TopBorder", "topBorderBar", "BottomBorder", "bottomBorderBar", "LeftBorder", "leftBorderBar", "RightBorder", "rightBorderBar", "Portrait", "portrait", "PortraitFrame", "portraitFrame", "PortraitContainer", "portraitContainer", "TitleContainer", "titleContainer", "Inset", "inset", "NineSlice", "nineSlice", "Prev", "Next", "PrevButton", "NextButton", "TitleText", } local native_scroll_global_names = { "LootFrameUpButton", "LootFrameDownButton", } local native_scroll_field_names = { "UpButton", "DownButton", } local function copy_default_settings() return { size = default_settings.size, summary_enabled = default_settings.summary_enabled, summary_gold_enabled = default_settings.summary_gold_enabled, summary_quality_threshold = default_settings.summary_quality_threshold, summary_duration = default_settings.summary_duration, } end local function collect_profile_settings() return { size = module.settings.size, summary_enabled = module.settings.summary_enabled, summary_gold_enabled = module.settings.summary_gold_enabled, summary_quality_threshold = module.settings.summary_quality_threshold, summary_duration = module.settings.summary_duration, } end local function is_valid_size(value) return type(value) == "number" and value >= 50 and value <= 150 and value % 10 == 0 end local function apply_frame_scale() local frame = _G.LootFrame if not frame then return false end frame:SetScale(module.settings.size / 100) return true end local function apply_profile_settings(profile_settings) local settings = type(profile_settings) == "table" and profile_settings or {} local size = settings.size if not is_valid_size(size) then module.settings.size = default_settings.size ns:chat_message( "Ungültige Lootfenster-Größe im Profil; Standardwert 100 wird verwendet", true ) else module.settings.size = size end if type(settings.summary_enabled) ~= "boolean" then module.settings.summary_enabled = default_settings.summary_enabled ns:chat_message( "Ungültige Loot-Summary-Aktivierung im Profil; Standardwert An wird verwendet", true ) else module.settings.summary_enabled = settings.summary_enabled end if type(settings.summary_gold_enabled) ~= "boolean" then module.settings.summary_gold_enabled = default_settings.summary_gold_enabled ns:chat_message( "Ungültige Loot-Summary-Goldanzeige im Profil; Standardwert An wird verwendet", true ) else module.settings.summary_gold_enabled = settings.summary_gold_enabled end if not valid_summary_quality_thresholds[settings.summary_quality_threshold] then module.settings.summary_quality_threshold = default_settings.summary_quality_threshold ns:chat_message( "Ungültiger Loot-Summary-Anzeigeschwellwert im Profil; Standardwert Gewöhnlich wird verwendet", true ) else module.settings.summary_quality_threshold = settings.summary_quality_threshold end if not valid_summary_durations[settings.summary_duration] then module.settings.summary_duration = default_settings.summary_duration ns:chat_message( "Ungültige Loot-Summary-Anzeigedauer im Profil; Standardwert 4 Sekunden wird verwendet", true ) else module.settings.summary_duration = settings.summary_duration end apply_frame_scale() return true end local function escape_lua_pattern(value) return value:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") end local function build_loot_message_pattern(format_string, multiple) if type(format_string) ~= "string" or format_string == "" then return nil end local string_token = "\001" local number_token = "\002" local pattern = format_string:gsub("%%s", string_token) pattern = pattern:gsub("%%d", number_token) pattern = escape_lua_pattern(pattern) pattern = pattern:gsub(string_token, "(.+)") pattern = pattern:gsub(number_token, "(%%d+)") return { pattern = "^" .. pattern .. "$", multiple = multiple == true, } end local function build_loot_message_patterns() loot_message_patterns = {} for index = 1, #loot_message_format_definitions do local definition = loot_message_format_definitions[index] local pattern = build_loot_message_pattern( _G[definition.global_name], definition.multiple ) if pattern then loot_message_patterns[#loot_message_patterns + 1] = pattern end end end local function parse_self_loot_message(message) if type(message) ~= "string" or message == "" then return nil end for index = 1, #loot_message_patterns do local definition = loot_message_patterns[index] local item_link, quantity = message:match(definition.pattern) if item_link then if definition.multiple then quantity = tonumber(quantity) or 1 else quantity = 1 end return item_link, math.max(quantity, 1) end end return nil end local function resolve_summary_environment() local minimap_module = ns.module_registry and ns.module_registry["minimap"] or nil local bag_bar = minimap_module and minimap_module.minimap_bag_bar or nil local top_bar = minimap_module and minimap_module.top_bar or nil local minimap = bag_bar and bag_bar:GetParent() or _G.Minimap if not minimap then return nil end return minimap, bag_bar, top_bar end local function set_summary_border_color(slot, data) if not slot or not slot.border or not data then return end if data.kind == "item" then local r, g, b = ns.palette:get_quality_color(data.quality) if not r then r, g, b = ns.palette:get_quality_color(1) end slot.border:SetBackdropBorderColor(r, g, b, ns.palette:get_alpha("opaque")) return end slot.border:SetBackdropBorderColor( ns.palette:get_rgba("border", ns.palette:get_alpha("opaque")) ) end local function format_summary_gold(amount) local gold = math.floor(amount / 10000) local silver = math.floor(amount / 100) % 100 local copper = amount % 100 return ns.palette:wrap("gold", tostring(gold)) .. "g " .. ns.palette:wrap("text_muted", tostring(silver)) .. "s " .. ns.palette:wrap("copper", tostring(copper)) .. "c" end local function render_summary_slot(slot, data) if not slot or not data then return end slot.background:SetColorTexture(ns.palette:get_surface("button_deep")) slot.item_name:SetTextColor(ns.palette:get_rgb("text")) slot.item_count:SetTextColor(ns.palette:get_rgb("text_muted")) slot.gold_text:SetTextColor(ns.palette:get_rgb("text")) set_summary_border_color(slot, data) if data.kind == "item" then slot.icon:SetTexture(data.texture) slot.icon:Show() slot.item_name:SetText(data.name) slot.item_name:Show() if data.quantity > 1 then slot.item_count:SetText("x" .. data.quantity) slot.item_count:Show() else slot.item_count:SetText("") slot.item_count:Hide() end slot.gold_text:SetText("") slot.gold_text:Hide() return end slot.icon:Hide() slot.item_name:SetText("") slot.item_name:Hide() slot.item_count:SetText("") slot.item_count:Hide() slot.gold_text:SetText(format_summary_gold(data.amount)) slot.gold_text:Show() end local function create_summary_slot(parent) local frame = CreateFrame("Frame", nil, parent) frame:SetHeight(summary_row_height) frame:SetFrameLevel(parent:GetFrameLevel() + 14) frame:SetAlpha(ns.palette:get_alpha("opaque")) frame:Hide() local background = frame:CreateTexture(nil, "BACKGROUND") background:SetAllPoints(frame) background:SetColorTexture(ns.palette:get_surface("button_deep")) local border = ns.designelemente:create_border_overlay( frame, frame, "frame", "border" ) local icon = frame:CreateTexture(nil, "ARTWORK") icon:SetSize(summary_icon_size, summary_icon_size) icon:SetPoint("LEFT", frame, "LEFT", summary_icon_inset, 0) local item_count = frame:CreateFontString(nil, "OVERLAY") item_count:SetFontObject(samui_font_11) item_count:SetPoint("RIGHT", frame, "RIGHT", -6, 0) item_count:SetJustifyH("RIGHT") item_count:SetTextColor(ns.palette:get_rgb("text_muted")) local item_name = frame:CreateFontString(nil, "OVERLAY") item_name:SetFontObject(samui_font_11) item_name:SetPoint("LEFT", icon, "RIGHT", 5, 0) item_name:SetPoint("RIGHT", item_count, "LEFT", -4, 0) item_name:SetHeight(summary_icon_size) item_name:SetJustifyH("LEFT") item_name:SetJustifyV("MIDDLE") item_name:SetTextColor(ns.palette:get_rgb("text")) local gold_text = frame:CreateFontString(nil, "OVERLAY") gold_text:SetFontObject(samui_font_11) gold_text:SetPoint("CENTER", frame, "CENTER", 0, 0) gold_text:SetJustifyH("CENTER") gold_text:SetTextColor(ns.palette:get_rgb("text")) return { frame = frame, background = background, border = border, icon = icon, item_name = item_name, item_count = item_count, gold_text = gold_text, active = false, elapsed = 0, duration = default_settings.summary_duration, started_sequence = 0, data = nil, } end local function ensure_summary_slots() if module.summary_slots then return true end local minimap = resolve_summary_environment() if not minimap or not ns.palette or not ns.designelemente then return false end module.summary_slots = {} for index = 1, summary_slot_count do module.summary_slots[index] = create_summary_slot(minimap) end return true end local function layout_summary_slots() if not ensure_summary_slots() then return false end local minimap, bag_bar, top_bar = resolve_summary_environment() if not minimap then return false end local anchor local anchor_gap if type(ns.get_minimap_stack_anchor) == "function" then anchor, anchor_gap = ns:get_minimap_stack_anchor() end if not anchor then local bag_bar_visible = bag_bar and bag_bar:IsShown() == true or false anchor = bag_bar_visible and bag_bar or minimap end if not anchor_gap and top_bar and type(top_bar.GetPoint) == "function" then local _, relative_to, _, _, offset_y = top_bar:GetPoint(1) if relative_to == minimap and type(offset_y) == "number" then anchor_gap = math.abs(offset_y) end end if not anchor_gap or anchor_gap <= 0 then local border_offset = ns.designelemente:get_border_offset("frame") or 0 anchor_gap = summary_border_gap + (border_offset * 2) end module.summary_anchor_frame = anchor for index = 1, #module.summary_slots do local slot = module.summary_slots[index] local frame = slot.frame frame:ClearAllPoints() if index == 1 then frame:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -anchor_gap) frame:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -anchor_gap) else local previous_frame = module.summary_slots[index - 1].frame frame:SetPoint("TOPLEFT", previous_frame, "BOTTOMLEFT", 0, -anchor_gap) frame:SetPoint("TOPRIGHT", previous_frame, "BOTTOMRIGHT", 0, -anchor_gap) end end return true end local function dequeue_summary_entry() local queue = module.summary_queue if not queue or #queue == 0 then return nil end return table.remove(queue, 1) end local function enqueue_summary_entry(data) module.summary_queue = module.summary_queue or {} module.summary_queue[#module.summary_queue + 1] = data end local function activate_summary_slot(slot, data) if not slot or not data then return false end layout_summary_slots() module.summary_sequence = (module.summary_sequence or 0) + 1 slot.active = true slot.elapsed = 0 slot.duration = module.settings.summary_duration slot.started_sequence = module.summary_sequence slot.data = data slot.frame:SetAlpha(ns.palette:get_alpha("opaque")) render_summary_slot(slot, data) slot.frame:Show() if module.summary_update_frame then module.summary_update_frame:Show() end return true end local function find_first_free_summary_slot() if not ensure_summary_slots() then return nil end for index = 1, #module.summary_slots do if not module.summary_slots[index].active then return module.summary_slots[index] end end return nil end local function submit_summary_entry(data) if module.settings.summary_enabled ~= true or not data then return false end local slot = find_first_free_summary_slot() if slot then return activate_summary_slot(slot, data) end enqueue_summary_entry(data) return true end local function find_active_gold_summary_slot() local slots = module.summary_slots if not slots then return nil end local newest_slot for index = 1, #slots do local slot = slots[index] if slot.active and slot.data and slot.data.kind == "gold" then if not newest_slot or slot.started_sequence > newest_slot.started_sequence then newest_slot = slot end end end return newest_slot end local function merge_active_gold_summary(amount) local slot = find_active_gold_summary_slot() if not slot then return false end slot.data.amount = (slot.data.amount or 0) + amount slot.elapsed = 0 slot.duration = module.settings.summary_duration slot.frame:SetAlpha(ns.palette:get_alpha("opaque")) render_summary_slot(slot, slot.data) slot.frame:Show() if module.summary_update_frame then module.summary_update_frame:Show() end return true end local function merge_queued_gold_summary(amount) local queue = module.summary_queue if not queue then return false end for index = #queue, 1, -1 do local data = queue[index] if data and data.kind == "gold" then data.amount = (data.amount or 0) + amount return true end end return false end local function submit_gold_summary(amount) if type(amount) ~= "number" or amount <= 0 then return false end if merge_active_gold_summary(amount) then return true end if merge_queued_gold_summary(amount) then return true end return submit_summary_entry({ kind = "gold", amount = amount, }) end local function finish_summary_slot(slot) slot.active = false slot.elapsed = 0 slot.data = nil slot.frame:SetAlpha(ns.palette:get_alpha("opaque")) slot.frame:Hide() end local function update_summary_slots(_, elapsed) local slots = module.summary_slots if not slots then module.summary_update_frame:Hide() return end local current_anchor if type(ns.get_minimap_stack_anchor) == "function" then current_anchor = ns:get_minimap_stack_anchor() else local minimap, bag_bar = resolve_summary_environment() current_anchor = bag_bar and bag_bar:IsShown() and bag_bar or minimap end if module.summary_anchor_frame ~= current_anchor then layout_summary_slots() end local expired_slots = {} local active_count = 0 for index = 1, #slots do local slot = slots[index] if slot.active then slot.elapsed = slot.elapsed + elapsed if slot.elapsed >= slot.duration then expired_slots[#expired_slots + 1] = slot else active_count = active_count + 1 local fade_start = math.max(slot.duration - 1, 0) if slot.elapsed >= fade_start then slot.frame:SetAlpha(math.max(slot.duration - slot.elapsed, 0)) else slot.frame:SetAlpha(ns.palette:get_alpha("opaque")) end end end end table.sort(expired_slots, function(left, right) return left.started_sequence < right.started_sequence end) for index = 1, #expired_slots do local slot = expired_slots[index] finish_summary_slot(slot) local queued_entry = dequeue_summary_entry() if queued_entry then activate_summary_slot(slot, queued_entry) active_count = active_count + 1 end end if active_count == 0 then module.summary_update_frame:Hide() end end local function resolve_item_summary_data(item_link, quantity) local item_name, _, quality, _, _, _, _, _, _, texture = GetItemInfo(item_link) if not item_name or type(quality) ~= "number" or not texture then return nil end return { kind = "item", name = item_name, texture = texture, quality = quality, quantity = quantity, } end local function submit_item_summary(item_link, quantity) if module.settings.summary_enabled ~= true then return false end local data = resolve_item_summary_data(item_link, quantity) if data then if data.quality >= module.settings.summary_quality_threshold then return submit_summary_entry(data) end return false end local item_id = tonumber(item_link:match("item:(%d+)")) if not item_id then return false end module.pending_item_entries[item_id] = module.pending_item_entries[item_id] or {} module.pending_item_entries[item_id][#module.pending_item_entries[item_id] + 1] = { item_link = item_link, quantity = quantity, } return true end local function handle_item_info_received(item_id, success) local pending = module.pending_item_entries[item_id] if not pending then return end module.pending_item_entries[item_id] = nil if success ~= true or module.settings.summary_enabled ~= true then return end for index = 1, #pending do local entry = pending[index] submit_item_summary(entry.item_link, entry.quantity) end end local function handle_loot_summary_message(message) if module.settings.summary_enabled ~= true then return end local item_link, quantity = parse_self_loot_message(message) if item_link then submit_item_summary(item_link, quantity) end end local function handle_money_change() local current_money = GetMoney() local previous_money = module.last_money module.last_money = current_money if type(previous_money) ~= "number" or type(current_money) ~= "number" then return end local gained_money = current_money - previous_money if gained_money <= 0 or module.settings.summary_enabled ~= true or module.settings.summary_gold_enabled ~= true then return end submit_gold_summary(gained_money) end local function on_summary_event(_, event, ...) if event == "CHAT_MSG_LOOT" then handle_loot_summary_message(...) elseif event == "PLAYER_MONEY" then handle_money_change() elseif event == "GET_ITEM_INFO_RECEIVED" then handle_item_info_received(...) end end local function add_unique_object(list, seen, object) if object and not seen[object] then seen[object] = true list[#list + 1] = object end end local function resolve_loot_frame_parts(frame) if not frame then return nil end local visual_objects = {} local scroll_buttons = {} local native_loot_buttons = {} local seen_visuals = {} local seen_scroll_buttons = {} for index = 1, #native_visual_global_names do add_unique_object( visual_objects, seen_visuals, _G[native_visual_global_names[index]] ) end for index = 1, #native_visual_field_names do add_unique_object( visual_objects, seen_visuals, frame[native_visual_field_names[index]] ) end for index = 1, #native_scroll_global_names do add_unique_object( scroll_buttons, seen_scroll_buttons, _G[native_scroll_global_names[index]] ) end for index = 1, #native_scroll_field_names do add_unique_object( scroll_buttons, seen_scroll_buttons, frame[native_scroll_field_names[index]] ) end local native_button_count = type(LOOTFRAME_NUMBUTTONS) == "number" and LOOTFRAME_NUMBUTTONS or 4 for index = 1, native_button_count do local button = _G["LootButton" .. index] if button then native_loot_buttons[#native_loot_buttons + 1] = button end end return { visual_objects = visual_objects, scroll_buttons = scroll_buttons, native_loot_buttons = native_loot_buttons, } end local function make_native_object_invisible(object, disable_mouse) if not object then return end if type(object.SetAlpha) == "function" then object:SetAlpha(0) end if disable_mouse and type(object.EnableMouse) == "function" then object:EnableMouse(false) end end local function suppress_native_title_region(frame) if not frame or type(frame.GetRegions) ~= "function" then return end local regions = { frame:GetRegions() } for index = 1, #regions do local region = regions[index] if region and type(region.GetObjectType) == "function" and region:GetObjectType() == "FontString" then make_native_object_invisible(region, false) end end end local function suppress_native_parts(frame) local resolved = resolve_loot_frame_parts(frame) if not resolved then return false end for index = 1, #resolved.visual_objects do make_native_object_invisible(resolved.visual_objects[index], false) end suppress_native_title_region(frame) for index = 1, #resolved.scroll_buttons do make_native_object_invisible(resolved.scroll_buttons[index], true) end for index = 1, #resolved.native_loot_buttons do make_native_object_invisible(resolved.native_loot_buttons[index], true) end return true end local function get_loot_slot_data(slot) local texture, name, quantity, fourth, fifth, sixth = GetLootSlotInfo(slot) local quality local locked if type(fifth) == "number" then quality = fifth locked = sixth == true else quality = fourth locked = fifth == true end if type(quality) ~= "number" then quality = 1 end if type(quantity) ~= "number" then quantity = 0 end return texture, name, quantity, quality, locked end local function set_border_quality(border, quality) if not border then return end local r, g, b = ns.palette:get_quality_color(quality) if not r then r, g, b = ns.palette:get_quality_color(1) end border:SetBackdropBorderColor(r, g, b, ns.palette:get_alpha("opaque")) end local function apply_row_palette(row_state, quality, locked) row_state.background:SetColorTexture(ns.palette:get_surface("row")) set_border_quality(row_state.border, quality) set_border_quality(row_state.icon_border, quality) local r, g, b = ns.palette:get_quality_color(quality) if not r then r, g, b = ns.palette:get_quality_color(1) end row_state.text:SetTextColor(r, g, b) if locked then row_state.icon:SetVertexColor(ns.palette:get_rgb("red")) else row_state.icon:SetVertexColor(ns.palette:get_rgb("text")) end end local function on_mouse_wheel(_, delta) local frame = _G.LootFrame local state = frame and loot_frame_states[frame] or nil if not state or state.active_count <= maximum_visible_rows then return end local maximum_offset = state.active_count - maximum_visible_rows if delta > 0 then state.scroll_offset = math.max(state.scroll_offset - 1, 0) elseif delta < 0 then state.scroll_offset = math.min(state.scroll_offset + 1, maximum_offset) else return end module:refresh_loot_rows() end local function create_loot_row(state, slot) local button_name = "samui_loot_button_" .. slot local button = CreateFrame( "Button", button_name, state.scroll_area, "LootButtonTemplate", slot ) button:SetSize(window_width - (window_padding * 2), row_height) button:EnableMouseWheel(true) button:SetScript("OnMouseWheel", on_mouse_wheel) local background = button:CreateTexture(nil, "BACKGROUND") background:SetAllPoints(button) background:SetColorTexture(ns.palette:get_surface("row")) local border = ns.designelemente:create_border_overlay( button, state.scroll_area, "content", "border" ) local icon = button.icon or button.Icon or _G[button_name .. "IconTexture"] local text = button.Text or _G[button_name .. "Text"] local count = button.Count or button.count or _G[button_name .. "Count"] local name_frame = button.NameFrame or _G[button_name .. "NameFrame"] if not icon or not text or not count then button:Hide() if not missing_template_warning_shown then missing_template_warning_shown = true ns:chat_message( "LootButtonTemplate konnte nicht vollständig aufgelöst werden", true ) end return { button = button, invalid = true, } end if name_frame then name_frame:SetAlpha(0) end local normal_texture = button:GetNormalTexture() if normal_texture then normal_texture:SetAlpha(0) end button:SetHighlightTexture(ns.designelemente:get_texture("action_button_highlight")) local highlight_texture = button:GetHighlightTexture() if highlight_texture then highlight_texture:ClearAllPoints() highlight_texture:SetPoint("TOPLEFT", button, "TOPLEFT", 5, -5) highlight_texture:SetSize(icon_size, icon_size) highlight_texture:SetBlendMode("ADD") end local icon_anchor = CreateFrame("Frame", nil, button) icon_anchor:SetSize(icon_size, icon_size) icon_anchor:SetPoint("LEFT", button, "LEFT", 5, 0) icon_anchor:EnableMouse(false) icon:ClearAllPoints() icon:SetAllPoints(icon_anchor) local icon_border = ns.designelemente:create_border_overlay( icon_anchor, button, "itemslot", "border" ) text:ClearAllPoints() text:SetPoint("LEFT", icon_anchor, "RIGHT", 6, 0) text:SetPoint("RIGHT", button, "RIGHT", -6, 0) text:SetHeight(row_height - 4) text:SetJustifyH("LEFT") text:SetJustifyV("MIDDLE") text:SetWordWrap(false) text:SetFontObject(samui_font_12) count:ClearAllPoints() count:SetPoint("BOTTOMRIGHT", icon_anchor, "BOTTOMRIGHT", -1, 1) return { button = button, background = background, border = border, icon_anchor = icon_anchor, icon = icon, icon_border = icon_border, text = text, count = count, quality = 1, locked = false, } end local function ensure_state(frame) local state = loot_frame_states[frame] if state then return state end local background = CreateFrame("Frame", nil, frame) background:SetAllPoints(frame) background:SetFrameLevel(frame:GetFrameLevel()) background:EnableMouse(false) local background_texture = background:CreateTexture(nil, "BACKGROUND") background_texture:SetAllPoints(background) background_texture:SetColorTexture(ns.palette:get_surface("panel")) local border = ns.designelemente:create_border_overlay( frame, frame, "frame", "border" ) local scroll_area = CreateFrame("Frame", nil, frame) scroll_area:SetPoint( "TOPLEFT", frame, "TOPLEFT", window_padding, -(window_padding + header_height) ) scroll_area:SetWidth(window_width - (window_padding * 2)) scroll_area:SetFrameLevel(frame:GetFrameLevel() + 2) scroll_area:EnableMouse(true) scroll_area:EnableMouseWheel(true) scroll_area:SetScript("OnMouseWheel", on_mouse_wheel) state = { background = background, background_texture = background_texture, border = border, scroll_area = scroll_area, rows = {}, row_count = 0, active_slots = {}, active_count = 0, scroll_offset = 0, } loot_frame_states[frame] = state return state end local function position_at_cursor(frame) if not frame or not UIParent then return false end local x, y = GetCursorPosition() local parent_scale = UIParent:GetEffectiveScale() if not parent_scale or parent_scale == 0 then return false end x = x / parent_scale y = y / parent_scale local frame_scale = module.settings.size / 100 local cursor_offset_x = window_padding * frame_scale local cursor_offset_y = ( window_padding + header_height + (row_height / 2) ) * frame_scale frame:ClearAllPoints() frame:SetPoint( "TOPLEFT", UIParent, "BOTTOMLEFT", x - cursor_offset_x, y + cursor_offset_y ) frame:SetClampedToScreen(true) frame:Raise() return true end local function apply_static_palette(state) if not state then return end state.background_texture:SetColorTexture(ns.palette:get_surface("panel")) if state.border then state.border:SetBackdropBorderColor( ns.palette:get_rgba("border", ns.palette:get_alpha("opaque")) ) end end function module:refresh_loot_rows() local frame = _G.LootFrame if not frame then return false end local state = ensure_state(frame) suppress_native_parts(frame) apply_static_palette(state) apply_frame_scale() local num_loot_items = GetNumLootItems() if type(num_loot_items) ~= "number" or num_loot_items < 0 then num_loot_items = 0 end local active_slots = state.active_slots for index = #active_slots, 1, -1 do active_slots[index] = nil end for slot = 1, num_loot_items do local texture, name, quantity, quality, locked = get_loot_slot_data(slot) local row_state = state.rows[slot] if not row_state then row_state = create_loot_row(state, slot) state.rows[slot] = row_state state.row_count = math.max(state.row_count, slot) end if row_state and not row_state.invalid and type(name) == "string" and name ~= "" and texture then row_state.button.slot = slot row_state.button.quality = quality row_state.button:SetID(slot) row_state.icon:SetTexture(texture) row_state.text:SetText(name) row_state.quality = quality row_state.locked = locked if quantity > 1 then row_state.count:SetText(quantity) row_state.count:Show() else row_state.count:Hide() end apply_row_palette(row_state, quality, locked) active_slots[#active_slots + 1] = slot elseif row_state then row_state.button:Hide() end end for slot = num_loot_items + 1, state.row_count do local row_state = state.rows[slot] if row_state then row_state.button:Hide() end end state.active_count = #active_slots local maximum_offset = math.max(state.active_count - maximum_visible_rows, 0) state.scroll_offset = math.min(state.scroll_offset, maximum_offset) for slot = 1, state.row_count do local row_state = state.rows[slot] if row_state then row_state.button:Hide() end end local visible_count = math.min(state.active_count, maximum_visible_rows) for visible_index = 1, visible_count do local active_index = state.scroll_offset + visible_index local slot = active_slots[active_index] local row_state = slot and state.rows[slot] or nil if row_state then row_state.button:ClearAllPoints() row_state.button:SetPoint( "TOPLEFT", state.scroll_area, "TOPLEFT", 0, -((visible_index - 1) * (row_height + row_spacing)) ) row_state.button:Show() end end local rows_height = visible_count > 0 and (visible_count * row_height) + ((visible_count - 1) * row_spacing) or 0 local window_height = (window_padding * 2) + header_height + rows_height frame:SetSize(window_width, window_height) state.scroll_area:SetHeight(math.max(rows_height, 1)) return true end local function configure_loot_frame(frame) if not frame then return false end ensure_state(frame) suppress_native_parts(frame) apply_frame_scale() module:refresh_loot_rows() return true end local function on_loot_frame_show(frame) local state = ensure_state(frame) state.scroll_offset = 0 configure_loot_frame(frame) position_at_cursor(frame) end local function hook_loot_frame_show(frame) if not frame or type(frame.HookScript) ~= "function" then return false end local state = ensure_state(frame) if state.show_hooked then return true end frame:HookScript("OnShow", on_loot_frame_show) state.show_hooked = true return true end local function on_loot_frame_show_complete(frame) if frame then local state = ensure_state(frame) state.scroll_offset = 0 configure_loot_frame(frame) position_at_cursor(frame) end end local function on_loot_frame_update_complete() local frame = _G.LootFrame if frame and frame:IsShown() then module:refresh_loot_rows() end end local function on_palette_changed() local frame = _G.LootFrame local state = frame and loot_frame_states[frame] or nil if state then apply_static_palette(state) for slot = 1, state.row_count do local row_state = state.rows[slot] if row_state and not row_state.invalid then apply_row_palette(row_state, row_state.quality, row_state.locked) end end end if module.summary_slots then for index = 1, #module.summary_slots do local summary_slot = module.summary_slots[index] summary_slot.background:SetColorTexture(ns.palette:get_surface("button_deep")) if summary_slot.data then render_summary_slot(summary_slot, summary_slot.data) end end end end local function on_event(_, event, loaded_addon_name) if event == "ADDON_LOADED" then if loaded_addon_name == "Blizzard_UIPanels_Game" and _G.LootFrame then configure_loot_frame(_G.LootFrame) hook_loot_frame_show(_G.LootFrame) end return end local frame = _G.LootFrame if not frame then if event == "LOOT_OPENED" and not missing_frame_warning_shown then missing_frame_warning_shown = true ns:chat_message("LootFrame konnte nicht aufgelöst werden", true) end return end if event == "LOOT_OPENED" then local state = ensure_state(frame) state.scroll_offset = 0 configure_loot_frame(frame) position_at_cursor(frame) elseif event == "LOOT_SLOT_CHANGED" or event == "LOOT_SLOT_CLEARED" then module:refresh_loot_rows() end end function module:initialize() self.settings = copy_default_settings() self.summary_queue = {} self.summary_sequence = 0 self.pending_item_entries = {} self.last_money = GetMoney() self.summary_update_frame = CreateFrame("Frame") self.summary_update_frame:SetScript("OnUpdate", update_summary_slots) self.summary_update_frame:Hide() build_loot_message_patterns() ns:register_profile_provider("lootfenster", collect_profile_settings, apply_profile_settings) ns:register_event("ADDON_LOADED", self, on_event) ns:register_event("LOOT_OPENED", self, on_event) ns:register_event("LOOT_SLOT_CHANGED", self, on_event) ns:register_event("LOOT_SLOT_CLEARED", self, on_event) ns:register_event("CHAT_MSG_LOOT", self, on_summary_event) ns:register_event("PLAYER_MONEY", self, on_summary_event) ns:register_event("GET_ITEM_INFO_RECEIVED", self, on_summary_event) local frame = _G.LootFrame if frame then configure_loot_frame(frame) hook_loot_frame_show(frame) end if type(hooksecurefunc) == "function" and type(LootFrame_Show) == "function" then hooksecurefunc("LootFrame_Show", on_loot_frame_show_complete) loot_frame_show_hooked = true end if type(hooksecurefunc) == "function" and type(LootFrame_Update) == "function" then hooksecurefunc("LootFrame_Update", on_loot_frame_update_complete) loot_frame_update_hooked = true end if not loot_frame_show_hooked and not frame then ns:chat_message("LootFrame-Öffnungslogik konnte nicht eingebunden werden", true) end if not loot_frame_update_hooked then ns:chat_message( "LootFrame_Update konnte nicht eingebunden werden; Aktualisierung erfolgt über Loot-Events", true ) end if ns.palette and type(ns.palette.register_callback) == "function" then ns.palette:register_callback("lootfenster", on_palette_changed) end end ns:register_module("lootfenster")