---------------------------------------------------- -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("unitframes") local unit_definitions = { player = { width = 240, height = 30, has_values = true, has_castbar = true, }, target = { width = 240, height = 30, has_values = true, has_castbar = true, }, pet = { width = 100, height = 30, }, targettarget = { width = 100, height = 30, }, } local unit_order = { "player", "target", "pet", "targettarget", } local unit_profile_limits = { player = { width_min = 200, width_max = 400, height_min = 30, height_max = 100 }, target = { width_min = 200, width_max = 400, height_min = 30, height_max = 100 }, pet = { width_min = 100, width_max = 200, height_min = 20, height_max = 70 }, targettarget = { width_min = 100, width_max = 200, height_min = 20, height_max = 70 }, } local unit_profile_offset_x_minimum = -1000 local unit_profile_offset_x_maximum = 1000 local unit_profile_offset_y_minimum = -500 local unit_profile_offset_y_maximum = 500 local unit_anchors = { player = { point = "BOTTOM", relative_point = "BOTTOM", offset_x = -260, offset_y = 200, }, target = { point = "CENTER", relative_unit = "player", relative_point = "CENTER", offset_x = 520, offset_y = 0, }, pet = { point = "LEFT", relative_unit = "player", relative_point = "RIGHT", offset_x = 20, offset_y = 0, }, targettarget = { point = "RIGHT", relative_unit = "target", relative_point = "LEFT", offset_x = -20, offset_y = 0, }, } local threat_targets = { player = "target", target = "targettarget", pet = "pettarget", targettarget = "targettargettarget", } local cast_units = { player = true, target = true, } local frames = {} local pending_geometry = {} local pending_visibility = {} local unit_enabled = { player = true, target = true, pet = true, targettarget = true, } local unit_aggro_glow_enabled = { player = true, target = true, pet = true, targettarget = true, } local unit_health_smoothing_enabled = { player = true, target = true, pet = true, targettarget = true, } local health_animation_states = {} local power_animation_states = {} local health_animation_driver local power_animation_driver local hovered_unitframes = {} local combat_values_visible = false local pending_profile_validation_data local cast_terminal_hold_time = 1 local cast_success_hold_time = 0.12 local health_smoothing_duration = 0.5 local unit_bar_gap = 1 local unit_value_text_width = 48 local unit_name_value_gap = 4 local unit_text_ellipsis = "..." local function set_statusbar_color(statusbar, red, green, blue) statusbar:SetStatusBarColor(red, green, blue) end local function get_health_color(unit, palette) if unit == "target" and UnitIsTapDenied(unit) then return palette:get_rgb("text_muted") end if UnitIsPlayer(unit) then local _, class = UnitClass(unit) local red, green, blue = palette:get_class_color(class) if red then return red, green, blue end return palette:get_rgb("text_muted") end local reaction = UnitReaction(unit, "player") if reaction == nil then return palette:get_rgb("text_muted") end if reaction >= 5 then return palette:get_rgb("green") elseif reaction == 4 then return palette:get_rgb("gold") elseif reaction == 3 then return palette:get_rgb("orange") end return palette:get_rgb("red") end local function get_power_color(unit, palette) local power_type = UnitPowerType(unit) local power_type_enum = Enum and Enum.PowerType or nil if power_type_enum then if power_type == power_type_enum.Mana then return palette:get_rgb("blue") elseif power_type == power_type_enum.Rage then return palette:get_rgb("red") elseif power_type == power_type_enum.Focus then return palette:get_rgb("orange") elseif power_type == power_type_enum.Energy then return palette:get_rgb("gold") elseif power_type == power_type_enum.ComboPoints then return palette:get_rgb("orange") elseif power_type == power_type_enum.RunicPower then return palette:get_rgb("cyan") end end return palette:get_rgb("text_muted") end local function abbreviate_value(value) value = tonumber(value) or 0 if value >= 1000000000 then return string.format("%.1fb", value / 1000000000) elseif value >= 1000000 then return string.format("%.1fm", value / 1000000) elseif value >= 1000 then return string.format("%.1fk", value / 1000) end return tostring(math.floor(value + 0.5)) end local function clear_level_display(frame) frame.level_text:SetText("") frame.level_skull:Hide() end local function remove_last_utf8_character(text) local index = #text if index <= 0 then return "" end while index > 1 do local byte_value = string.byte(text, index) if not byte_value or byte_value < 128 or byte_value >= 192 then break end index = index - 1 end return string.sub(text, 1, index - 1) end local function set_truncated_font_text(font_string, text, maximum_width) font_string:SetText(text) if maximum_width <= 0 then font_string:SetText("") return end if font_string:GetStringWidth() <= maximum_width then return end font_string:SetText(unit_text_ellipsis) if font_string:GetStringWidth() > maximum_width then font_string:SetText("") return end local candidate = text repeat candidate = remove_last_utf8_character(candidate) font_string:SetText(candidate .. unit_text_ellipsis) until candidate == "" or font_string:GetStringWidth() <= maximum_width end local function get_level_display_width(frame) if frame.level_skull:IsShown() then return frame.level_skull:GetWidth() + 4 end local level_text = frame.level_text:GetText() if type(level_text) == "string" and level_text ~= "" then return frame.level_text:GetStringWidth() end return 0 end local function update_name_text_layout(frame, text) frame.name_source_text = text if not frame.health_value or not frame.power_value then frame.name_text:ClearAllPoints() frame.name_text:SetPoint("CENTER", frame.name_region, "CENTER", 0, 0) frame.name_text:SetText(text) return end local level_width = get_level_display_width(frame) local available_width = math.max( frame:GetWidth() - ((unit_value_text_width + unit_name_value_gap) * 2), 0 ) local maximum_name_width = math.max(available_width - level_width, 0) if frame.name_layout_text ~= text or frame.name_layout_maximum_width ~= maximum_name_width then set_truncated_font_text(frame.name_text, text, maximum_name_width) frame.name_layout_text = text frame.name_layout_maximum_width = maximum_name_width end frame.name_text:ClearAllPoints() frame.name_text:SetPoint( "CENTER", frame.name_region, "CENTER", -(level_width / 2), 0 ) end local function update_level_display(frame, palette) local unit = frame.unit local level = UnitLevel(unit) or 0 clear_level_display(frame) if unit == "player" or unit == "pet" then if level > 0 then frame.level_text:SetText(" " .. tostring(level)) frame.level_text:SetTextColor(palette:get_rgb("text")) end return end if UnitIsDeadOrGhost(unit) or level <= 0 then frame.level_skull:Show() return end frame.level_text:SetText(" " .. tostring(level)) if UnitCanAttack("player", unit) then local difficulty_color = GetCreatureDifficultyColor(level) if difficulty_color then frame.level_text:SetTextColor( difficulty_color.r, difficulty_color.g, difficulty_color.b ) return end end frame.level_text:SetTextColor(palette:get_rgb("text")) end local function update_name_and_level(frame, palette) local unit = frame.unit if cast_units[unit] then local spell_name, display_name = UnitCastingInfo(unit) if not spell_name then spell_name, display_name = UnitChannelInfo(unit) end if spell_name then clear_level_display(frame) frame.name_text:SetTextColor(palette:get_rgb("text")) update_name_text_layout(frame, string.upper(display_name)) return end end local red, green, blue = get_health_color(unit, palette) local display_text = UnitName(unit) or "" update_level_display(frame, palette) frame.name_text:SetTextColor(red, green, blue) update_name_text_layout(frame, string.upper(display_text)) end local function set_health_bar_value(frame, current, maximum, force_immediate) local unit = frame.unit local state = health_animation_states[unit] if not state then state = { initialized = false, current_value = current, start_value = current, target_value = current, elapsed = 0, active = false, } health_animation_states[unit] = state end local displayed = state.current_value or current if displayed < 0 then displayed = 0 elseif displayed > maximum then displayed = maximum end state.current_value = displayed if force_immediate or not state.initialized or unit_health_smoothing_enabled[unit] == false then state.initialized = true state.current_value = current state.start_value = current state.target_value = current state.elapsed = 0 state.active = false frame.health_bar:SetValue(current) return end if state.active and state.target_value == current then return end if not state.active and state.current_value == current then return end state.start_value = state.current_value state.target_value = current state.elapsed = 0 state.active = true if health_animation_driver then health_animation_driver:Show() end end local function update_health_animation(_, elapsed) local has_active_animation = false for index = 1, #unit_order do local unit = unit_order[index] local state = health_animation_states[unit] if state and state.active then local frame = frames[unit] if not frame or unit_health_smoothing_enabled[unit] == false then state.active = false else state.elapsed = state.elapsed + elapsed local progress = state.elapsed / health_smoothing_duration if progress >= 1 then progress = 1 state.active = false else has_active_animation = true end state.current_value = state.start_value + ((state.target_value - state.start_value) * progress) frame.health_bar:SetValue(state.current_value) end end end if not has_active_animation and health_animation_driver then health_animation_driver:Hide() end end local function update_health(frame, palette, force_immediate) local unit = frame.unit local maximum = UnitHealthMax(unit) or 0 local current = UnitHealth(unit) or 0 if maximum <= 0 then maximum = 1 end if current < 0 then current = 0 elseif current > maximum then current = maximum end frame.health_bar:SetMinMaxValues(0, maximum) set_health_bar_value(frame, current, maximum, force_immediate) local red, green, blue = get_health_color(unit, palette) set_statusbar_color(frame.health_bar, red, green, blue) if frame.health_value then frame.health_value:SetText(abbreviate_value(current)) frame.health_value:SetTextColor(red, green, blue) end update_name_and_level(frame, palette) end local function set_power_bar_value(frame, current, maximum, force_immediate) local unit = frame.unit local state = power_animation_states[unit] if not state then state = { initialized = false, current_value = current, start_value = current, target_value = current, elapsed = 0, active = false, } power_animation_states[unit] = state end local displayed = state.current_value or current if displayed < 0 then displayed = 0 elseif displayed > maximum then displayed = maximum end state.current_value = displayed if force_immediate or not state.initialized or unit_health_smoothing_enabled[unit] == false then state.initialized = true state.current_value = current state.start_value = current state.target_value = current state.elapsed = 0 state.active = false frame.power_bar:SetValue(current) return end if state.active and state.target_value == current then return end if not state.active and state.current_value == current then return end state.start_value = state.current_value state.target_value = current state.elapsed = 0 state.active = true if power_animation_driver then power_animation_driver:Show() end end local function update_power_animation(_, elapsed) local has_active_animation = false for index = 1, #unit_order do local unit = unit_order[index] local state = power_animation_states[unit] if state and state.active then local frame = frames[unit] if not frame or unit_health_smoothing_enabled[unit] == false then state.active = false else state.elapsed = state.elapsed + elapsed local progress = state.elapsed / health_smoothing_duration if progress >= 1 then progress = 1 state.active = false else has_active_animation = true end state.current_value = state.start_value + ((state.target_value - state.start_value) * progress) frame.power_bar:SetValue(state.current_value) end end end if not has_active_animation and power_animation_driver then power_animation_driver:Hide() end end local function update_power(frame, palette, force_immediate) local unit = frame.unit local maximum = UnitPowerMax(unit) or 0 local current = UnitPower(unit) or 0 if maximum <= 0 then maximum = 1 end if current < 0 then current = 0 elseif current > maximum then current = maximum end frame.power_bar:SetMinMaxValues(0, maximum) set_power_bar_value(frame, current, maximum, force_immediate) local red, green, blue = get_power_color(unit, palette) set_statusbar_color(frame.power_bar, red, green, blue) if frame.power_value then frame.power_value:SetText(abbreviate_value(current)) frame.power_value:SetTextColor(red, green, blue) end end local function update_threat(frame, palette) local threat_target = threat_targets[frame.unit] local token = "border" if unit_aggro_glow_enabled[frame.unit] ~= false and threat_target and UnitExists(threat_target) then local _, status, scaled_percentage = UnitDetailedThreatSituation(frame.unit, threat_target) if scaled_percentage and scaled_percentage >= 100 then token = "red" elseif scaled_percentage and scaled_percentage > 80 then token = "orange" elseif not scaled_percentage then status = UnitThreatSituation(frame.unit, threat_target) or status if status == 2 or status == 3 then token = "red" elseif status == 1 then token = "orange" end end end frame.border:SetBackdropBorderColor( palette:get_rgba(token, palette:get_alpha("opaque")) ) end local function get_cast_info(unit) local name, _, texture, start_time_ms, end_time_ms = UnitCastingInfo(unit) if name then return name, texture, start_time_ms, end_time_ms, false end name, _, texture, start_time_ms, end_time_ms = UnitChannelInfo(unit) if name then return name, texture, start_time_ms, end_time_ms, true end return nil end local function get_cast_color_token(unit, cast_state) if cast_state == "failed" then return unit == "player" and "red" or "orange" elseif cast_state == "interrupted" then return "red" end return "gold" end local function apply_cast_color(frame, palette) local token = get_cast_color_token(frame.unit, frame.cast_state) frame.cast_bar:SetStatusBarColor(palette:get_rgb(token)) end local function cancel_cast_hide_timer(frame) if frame.cast_hide_timer then frame.cast_hide_timer:Cancel() frame.cast_hide_timer = nil end end local function hide_cast(frame) cancel_cast_hide_timer(frame) frame.cast_active = false frame.cast_state = nil frame.cast_start_time = nil frame.cast_end_time = nil frame.cast_is_channel = nil frame.cast_bar:Hide() frame.cast_icon_frame:Hide() end local function complete_cast(frame, palette) if not frame.cast_active or not frame.cast_start_time or not frame.cast_end_time then hide_cast(frame) update_name_and_level(frame, palette) return end local duration = frame.cast_end_time - frame.cast_start_time if duration <= 0 then hide_cast(frame) update_name_and_level(frame, palette) return end cancel_cast_hide_timer(frame) frame.cast_state = "complete" frame.cast_bar:SetMinMaxValues(0, duration) if frame.cast_is_channel then frame.cast_bar:SetValue(0) else frame.cast_bar:SetValue(duration) end frame.cast_bar:Show() frame.cast_icon_frame:Show() update_name_and_level(frame, palette) frame.cast_hide_timer = C_Timer.NewTimer(cast_success_hold_time, function() frame.cast_hide_timer = nil if frame.cast_state ~= "complete" then return end hide_cast(frame) update_name_and_level(frame, palette) end) end local function update_cast_progress(frame) if not frame.cast_active or not frame.cast_start_time or not frame.cast_end_time then return end if frame.cast_state == "failed" or frame.cast_state == "interrupted" or frame.cast_state == "complete" then return end local now = GetTime() local duration = frame.cast_end_time - frame.cast_start_time if duration <= 0 then complete_cast(frame, ns.palette) return end if now >= frame.cast_end_time then complete_cast(frame, ns.palette) return end local value if frame.cast_is_channel then value = frame.cast_end_time - now else value = now - frame.cast_start_time end if value < 0 then value = 0 elseif value > duration then value = duration end frame.cast_bar:SetMinMaxValues(0, duration) frame.cast_bar:SetValue(value) end local function refresh_cast(frame, palette) local name, texture, start_time_ms, end_time_ms, is_channel = get_cast_info(frame.unit) if not name or not start_time_ms or not end_time_ms then if not frame.cast_state then hide_cast(frame) end update_name_and_level(frame, palette) return end cancel_cast_hide_timer(frame) frame.cast_active = true frame.cast_state = "normal" frame.cast_start_time = start_time_ms / 1000 frame.cast_end_time = end_time_ms / 1000 frame.cast_is_channel = is_channel frame.cast_icon:SetTexture(texture) frame.cast_bar:Show() frame.cast_icon_frame:Show() apply_cast_color(frame, palette) update_cast_progress(frame) update_name_and_level(frame, palette) end local function set_terminal_cast_state(frame, palette, state) if not frame.cast_active then return end update_cast_progress(frame) if not frame.cast_active then return end frame.cast_state = state apply_cast_color(frame, palette) frame.cast_bar:Show() frame.cast_icon_frame:Show() update_name_and_level(frame, palette) cancel_cast_hide_timer(frame) frame.cast_hide_timer = C_Timer.NewTimer(cast_terminal_hold_time, function() frame.cast_hide_timer = nil if frame.cast_state ~= state then return end hide_cast(frame) update_name_and_level(frame, palette) end) end local function stop_cast(frame, palette) if frame.cast_state == "failed" or frame.cast_state == "interrupted" then update_name_and_level(frame, palette) return end complete_cast(frame, palette) end local function create_statusbar(parent, designelemente, palette) local statusbar = CreateFrame("StatusBar", nil, parent) statusbar:SetStatusBarTexture(designelemente:get_texture("statusbar")) local background = statusbar:CreateTexture(nil, "BACKGROUND") background:SetAllPoints(statusbar) background:SetTexture(designelemente:get_texture("statusbar_background")) background:SetVertexColor(palette:get_surface("unit_bar")) return statusbar end local function create_name_region(frame, definition, designelemente) local name_region = CreateFrame("Frame", nil, frame) name_region:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -6) name_region:SetPoint("TOPRIGHT", frame, "BOTTOMRIGHT", 0, -6) name_region:SetHeight(16) local name_text = name_region:CreateFontString(nil, "OVERLAY") name_text:SetFontObject(samui_font_12_outline) name_text:SetPoint("CENTER", name_region, "CENTER", 0, 0) name_text:SetJustifyH("CENTER") local level_text = name_region:CreateFontString(nil, "OVERLAY") level_text:SetFontObject(samui_font_12_outline) level_text:SetPoint("LEFT", name_text, "RIGHT", 0, 0) level_text:SetJustifyH("LEFT") local level_skull = name_region:CreateTexture(nil, "OVERLAY") level_skull:SetSize(12, 12) level_skull:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Skull") level_skull:SetPoint("LEFT", name_text, "RIGHT", 4, 0) level_skull:Hide() frame.name_region = name_region frame.name_text = name_text frame.level_text = level_text frame.level_skull = level_skull if definition.has_values then local health_value = name_region:CreateFontString(nil, "OVERLAY") health_value:SetFontObject(samui_font_12_outline) health_value:SetWidth(unit_value_text_width) health_value:SetPoint("LEFT", name_region, "LEFT", 0, 0) health_value:SetJustifyH("LEFT") health_value:Hide() local power_value = name_region:CreateFontString(nil, "OVERLAY") power_value:SetFontObject(samui_font_12_outline) power_value:SetWidth(unit_value_text_width) power_value:SetPoint("RIGHT", name_region, "RIGHT", 0, 0) power_value:SetJustifyH("RIGHT") power_value:Hide() frame.health_value = health_value frame.power_value = power_value end end local function update_unitframe_value_visibility(unit_frame) local unit = unit_frame.unit local show_values = combat_values_visible or hovered_unitframes[unit] == true if unit_frame.health_value then unit_frame.health_value:SetShown(show_values) end if unit_frame.power_value then unit_frame.power_value:SetShown(show_values) end end local function update_all_unitframe_value_visibility() for index = 1, #unit_order do local frame = frames[unit_order[index]] if frame then update_unitframe_value_visibility(frame) end end end local function on_unitframe_enter(unit_frame) hovered_unitframes[unit_frame.unit] = true update_unitframe_value_visibility(unit_frame) local unit = unit_frame.unit if not unit or not UnitExists(unit) or not GameTooltip then return end if type(GameTooltip_SetDefaultAnchor) == "function" then GameTooltip_SetDefaultAnchor(GameTooltip, unit_frame) else GameTooltip:SetOwner(unit_frame, "ANCHOR_RIGHT") end GameTooltip:SetUnit(unit) GameTooltip:Show() end local function on_unitframe_leave(unit_frame) hovered_unitframes[unit_frame.unit] = nil update_unitframe_value_visibility(unit_frame) if GameTooltip then GameTooltip:Hide() end end local function create_cast_elements(frame, designelemente, palette) local cast_bar = create_statusbar(frame, designelemente, palette) cast_bar:SetAllPoints(frame.power_bar) cast_bar:SetFrameLevel(frame.power_bar:GetFrameLevel() + 2) cast_bar:Hide() cast_bar:SetScript("OnUpdate", function(unit_cast_bar) update_cast_progress(unit_cast_bar.owner) end) cast_bar.owner = frame local icon_frame = CreateFrame("Frame", nil, frame) icon_frame:SetSize(24, 24) icon_frame:SetPoint("CENTER", frame, "TOP", 0, 0) icon_frame:SetFrameLevel(frame:GetFrameLevel() + 20) icon_frame:Hide() local icon = icon_frame:CreateTexture(nil, "ARTWORK") icon:SetAllPoints(icon_frame) designelemente:create_border_overlay( icon_frame, icon_frame, "casticon" ) frame.cast_bar = cast_bar frame.cast_icon_frame = icon_frame frame.cast_icon = icon end local function create_unit_frame(unit, definition, palette, designelemente) local frame_name = "samui_unitframe_" .. unit local frame = CreateFrame("Button", frame_name, UIParent, "SecureUnitButtonTemplate") frame:SetSize(definition.width, definition.height) frame:SetAttribute("unit", unit) frame:SetAttribute("*type1", "target") frame:SetAttribute("*type2", "togglemenu") frame:RegisterForClicks("AnyUp") local transparent_background = frame:CreateTexture(nil, "BACKGROUND") transparent_background:SetAllPoints(frame) transparent_background:SetColorTexture(0, 0, 0, 0) local available_bar_height = definition.height - unit_bar_gap local health_height = available_bar_height * 0.75 local power_height = available_bar_height * 0.25 local health_bar = create_statusbar(frame, designelemente, palette) health_bar:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) health_bar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0) health_bar:SetHeight(health_height) local power_bar = create_statusbar(frame, designelemente, palette) power_bar:SetPoint("TOPLEFT", health_bar, "BOTTOMLEFT", 0, -unit_bar_gap) power_bar:SetPoint("TOPRIGHT", health_bar, "BOTTOMRIGHT", 0, -unit_bar_gap) power_bar:SetHeight(power_height) local border = designelemente:create_border_overlay(frame, frame, "frame") frame.unit = unit frame.health_bar = health_bar frame.power_bar = power_bar frame.border = border create_name_region(frame, definition, designelemente) frame:SetScript("OnEnter", on_unitframe_enter) frame:SetScript("OnLeave", on_unitframe_leave) if definition.has_castbar then create_cast_elements(frame, designelemente, palette) end RegisterUnitWatch(frame) return frame end local function get_anchor_relative_frame(anchor_definition) if anchor_definition.relative_unit then return frames[anchor_definition.relative_unit] end return UIParent end local function apply_unitframe_size(unit, width, height) local frame = frames[unit] if not frame then return false end frame:SetSize(width, height) local available_bar_height = height - unit_bar_gap frame.health_bar:SetHeight(available_bar_height * 0.75) frame.power_bar:SetHeight(available_bar_height * 0.25) if frame.name_source_text then update_name_text_layout(frame, frame.name_source_text) end return true end local function apply_unitframe_offset(unit, offset_x, offset_y) local frame = frames[unit] local anchor_definition = unit_anchors[unit] if not frame or not anchor_definition then return false end local relative_frame = get_anchor_relative_frame(anchor_definition) if not relative_frame then return false end frame:ClearAllPoints() frame:SetPoint( anchor_definition.point, relative_frame, anchor_definition.relative_point, offset_x, offset_y ) return true end local function apply_unitframe_enabled(unit, enabled) local frame = frames[unit] if not frame then return false end if unit == "player" then unit_enabled.player = true RegisterUnitWatch(frame) return enabled ~= false end if InCombatLockdown() then pending_visibility[unit] = enabled == true return false end unit_enabled[unit] = enabled == true if unit_enabled[unit] then RegisterUnitWatch(frame) else UnregisterUnitWatch(frame) frame:Hide() end return true end local function apply_pending_geometry() for index = 1, #unit_order do local unit = unit_order[index] local pending = pending_geometry[unit] if pending then if pending.width and pending.height then apply_unitframe_size(unit, pending.width, pending.height) end if pending.offset_x and pending.offset_y then apply_unitframe_offset(unit, pending.offset_x, pending.offset_y) end pending_geometry[unit] = nil end local pending_enabled = pending_visibility[unit] if pending_enabled ~= nil then pending_visibility[unit] = nil apply_unitframe_enabled(unit, pending_enabled) end end end function ns:set_unitframe_size(unit, width, height) if not frames[unit] then return false end if InCombatLockdown() then local pending = pending_geometry[unit] or {} pending.width = width pending.height = height pending_geometry[unit] = pending return false end return apply_unitframe_size(unit, width, height) end function ns:set_unitframe_offset(unit, offset_x, offset_y) if not frames[unit] or not unit_anchors[unit] then return false end if InCombatLockdown() then local pending = pending_geometry[unit] or {} pending.offset_x = offset_x pending.offset_y = offset_y pending_geometry[unit] = pending return false end return apply_unitframe_offset(unit, offset_x, offset_y) end function ns:get_unitframe_geometry(unit) local frame = frames[unit] if not frame then return nil end local _, _, _, offset_x, offset_y = frame:GetPoint(1) if offset_x == nil or offset_y == nil then return nil end return frame:GetWidth(), frame:GetHeight(), offset_x, offset_y end function ns:set_unitframe_enabled(unit, enabled) if type(enabled) ~= "boolean" or not frames[unit] then return false end if unit == "player" then return enabled == true end return apply_unitframe_enabled(unit, enabled) end function ns:get_unitframe_enabled(unit) if not frames[unit] then return nil end if unit == "player" then return true end return unit_enabled[unit] ~= false end local function set_unitframe_aggro_glow_enabled(unit, enabled) if type(enabled) ~= "boolean" or not frames[unit] then return false end unit_aggro_glow_enabled[unit] = enabled update_threat(frames[unit], ns.palette) return true end local function set_unitframe_health_smoothing_enabled(unit, enabled) if type(enabled) ~= "boolean" or not frames[unit] then return false end unit_health_smoothing_enabled[unit] = enabled if not enabled then update_health(frames[unit], ns.palette, true) update_power(frames[unit], ns.palette, true) end return true end local function set_frame_positions() for index = 1, #unit_order do local unit = unit_order[index] local anchor_definition = unit_anchors[unit] apply_unitframe_offset(unit, anchor_definition.offset_x, anchor_definition.offset_y) end end local function collect_profile_settings() local data = {} for index = 1, #unit_order do local unit = unit_order[index] local width, height, offset_x, offset_y = ns:get_unitframe_geometry(unit) if width then data[unit] = { width = width, height = height, offset_x = offset_x, offset_y = offset_y, enabled = unit == "player" or unit_enabled[unit] ~= false, aggro_glow_enabled = unit_aggro_glow_enabled[unit] ~= false, health_smoothing_enabled = unit_health_smoothing_enabled[unit] ~= false, } end end return data end local function is_profile_number_valid(value, minimum, maximum) return type(value) == "number" and value >= minimum and value <= maximum end local function get_ui_parent_bounds() local left, bottom, width, height = UIParent:GetRect() if type(left) == "number" and type(bottom) == "number" and type(width) == "number" and type(height) == "number" and width > 0 and height > 0 then return { left = left, right = left + width, bottom = bottom, top = bottom + height, } end return nil end local function get_profile_rect(unit, width, height, offset_x, offset_y, rects, parent_bounds) local left local right local bottom local top if unit == "player" then if not parent_bounds then return nil end local center_x = ((parent_bounds.left + parent_bounds.right) / 2) + offset_x left = center_x - (width / 2) right = center_x + (width / 2) bottom = parent_bounds.bottom + offset_y top = bottom + height elseif unit == "target" then local player_rect = rects.player if not player_rect then return nil end local center_x = ((player_rect.left + player_rect.right) / 2) + offset_x local center_y = ((player_rect.bottom + player_rect.top) / 2) + offset_y left = center_x - (width / 2) right = center_x + (width / 2) bottom = center_y - (height / 2) top = center_y + (height / 2) elseif unit == "pet" then local player_rect = rects.player if not player_rect then return nil end left = player_rect.right + offset_x right = left + width local center_y = ((player_rect.bottom + player_rect.top) / 2) + offset_y bottom = center_y - (height / 2) top = center_y + (height / 2) elseif unit == "targettarget" then local target_rect = rects.target if not target_rect then return nil end right = target_rect.left + offset_x left = right - width local center_y = ((target_rect.bottom + target_rect.top) / 2) + offset_y bottom = center_y - (height / 2) top = center_y + (height / 2) end if not left or not right or not bottom or not top then return nil end return { left = left, right = right, bottom = bottom, top = top, } end local function is_profile_rect_visible(rect, parent_bounds) if not rect or not parent_bounds then return false end local border_offset = ns.designelemente:get_border_offset("frame") or 0 return rect.left - border_offset >= parent_bounds.left and rect.right + border_offset <= parent_bounds.right and rect.bottom - border_offset >= parent_bounds.bottom and rect.top + border_offset <= parent_bounds.top end local function get_valid_profile_geometry(unit, settings, rects, parent_bounds) local limits = unit_profile_limits[unit] local defaults = unit_definitions[unit] local anchor_defaults = unit_anchors[unit] local valid = type(settings) == "table" if valid then valid = is_profile_number_valid(settings.width, limits.width_min, limits.width_max) and is_profile_number_valid(settings.height, limits.height_min, limits.height_max) and is_profile_number_valid( settings.offset_x, unit_profile_offset_x_minimum, unit_profile_offset_x_maximum ) and is_profile_number_valid( settings.offset_y, unit_profile_offset_y_minimum, unit_profile_offset_y_maximum ) end local width = valid and settings.width or defaults.width local height = valid and settings.height or defaults.height local offset_x = valid and settings.offset_x or anchor_defaults.offset_x local offset_y = valid and settings.offset_y or anchor_defaults.offset_y local rect = parent_bounds and get_profile_rect(unit, width, height, offset_x, offset_y, rects, parent_bounds) or nil if valid and parent_bounds and not is_profile_rect_visible(rect, parent_bounds) then valid = false width = defaults.width height = defaults.height offset_x = anchor_defaults.offset_x offset_y = anchor_defaults.offset_y rect = get_profile_rect(unit, width, height, offset_x, offset_y, rects, parent_bounds) end if not valid then ns:chat_message( "Ungültige Profilwerte für '" .. unit .. "' wurden auf Standard zurückgesetzt.", true ) end if type(settings) == "table" then settings.width = width settings.height = height settings.offset_x = offset_x settings.offset_y = offset_y end if parent_bounds then rects[unit] = rect end return width, height, offset_x, offset_y end local function get_valid_profile_enabled(unit, settings) if unit == "player" then if type(settings) == "table" then settings.enabled = true end return true end if type(settings) == "table" and type(settings.enabled) == "boolean" then return settings.enabled end if type(settings) == "table" and settings.enabled ~= nil then ns:chat_message( "Ungültiger Aktivierungswert für '" .. unit .. "' wurde auf Standard zurückgesetzt.", true ) end if type(settings) == "table" then settings.enabled = true end return true end local function get_valid_profile_aggro_glow_enabled(unit, settings) if type(settings) == "table" and type(settings.aggro_glow_enabled) == "boolean" then return settings.aggro_glow_enabled end if type(settings) == "table" and settings.aggro_glow_enabled ~= nil then ns:chat_message( "Ungültiger Aggro-Glow-Wert für '" .. unit .. "' wurde auf Standard zurückgesetzt.", true ) end if type(settings) == "table" then settings.aggro_glow_enabled = true end return true end local function get_valid_profile_health_smoothing_enabled(unit, settings) if type(settings) == "table" and type(settings.health_smoothing_enabled) == "boolean" then return settings.health_smoothing_enabled end if type(settings) == "table" and settings.health_smoothing_enabled ~= nil then ns:chat_message( "Ungültiger Smooth-Health-Wert für '" .. unit .. "' wurde auf Standard zurückgesetzt.", true ) end if type(settings) == "table" then settings.health_smoothing_enabled = true end return true end local function apply_profile_settings(data) if type(data) ~= "table" then return false end local rects = {} local parent_bounds = get_ui_parent_bounds() if not parent_bounds then pending_profile_validation_data = data else pending_profile_validation_data = nil end for index = 1, #unit_order do local unit = unit_order[index] local settings = data[unit] if type(settings) ~= "table" then settings = {} data[unit] = settings end local width, height, offset_x, offset_y = get_valid_profile_geometry( unit, settings, rects, parent_bounds ) ns:set_unitframe_size(unit, width, height) ns:set_unitframe_offset(unit, offset_x, offset_y) set_unitframe_aggro_glow_enabled( unit, get_valid_profile_aggro_glow_enabled(unit, settings) ) set_unitframe_health_smoothing_enabled( unit, get_valid_profile_health_smoothing_enabled(unit, settings) ) if unit ~= "player" then ns:set_unitframe_enabled(unit, get_valid_profile_enabled(unit, settings)) else settings.enabled = true end end return true end local function update_frame(frame, palette, force_bar_immediate) update_health(frame, palette, force_bar_immediate) update_power(frame, palette, force_bar_immediate) update_threat(frame, palette) if frame.cast_bar then refresh_cast(frame, palette) end end local function update_all_frames(palette, force_bar_immediate) for index = 1, #unit_order do local frame = frames[unit_order[index]] update_frame(frame, palette, force_bar_immediate) end end local function update_unit_frame(unit, palette, force_bar_immediate) local frame = frames[unit] if frame then update_frame(frame, palette, force_bar_immediate) end end local function update_all_threat(palette) for index = 1, #unit_order do update_threat(frames[unit_order[index]], palette) end end local function handle_cast_event(event, unit, palette) local frame = frames[unit] if not frame or not frame.cast_bar then return end if event == "UNIT_SPELLCAST_FAILED" then set_terminal_cast_state(frame, palette, "failed") elseif event == "UNIT_SPELLCAST_INTERRUPTED" then set_terminal_cast_state(frame, palette, "interrupted") elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then stop_cast(frame, palette) elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then refresh_cast(frame, palette) elseif frame.cast_state ~= "failed" and frame.cast_state ~= "interrupted" then refresh_cast(frame, palette) end end local function on_event(_, event, unit) local palette = ns.palette if event == "PLAYER_ENTERING_WORLD" then if pending_profile_validation_data then local pending_data = pending_profile_validation_data pending_profile_validation_data = nil apply_profile_settings(pending_data) end combat_values_visible = UnitAffectingCombat("player") == true update_all_frames(palette, true) update_all_unitframe_value_visibility() return end if event == "PLAYER_REGEN_DISABLED" then combat_values_visible = true update_all_unitframe_value_visibility() return end if event == "PLAYER_REGEN_ENABLED" then combat_values_visible = false update_all_unitframe_value_visibility() apply_pending_geometry() return end if event == "PLAYER_TARGET_CHANGED" then if frames.target and frames.target.cast_bar then hide_cast(frames.target) end update_unit_frame("target", palette, true) update_unit_frame("targettarget", palette, true) update_all_threat(palette) return end if event == "UNIT_PET" then if unit == "player" then update_unit_frame("pet", palette, true) update_all_threat(palette) end return end if event == "PLAYER_LEVEL_UP" then update_unit_frame("player", palette) if type(unit) == "number" and unit > 0 then frames.player.level_text:SetText(" " .. tostring(unit)) frames.player.level_text:SetTextColor(palette:get_rgb("text")) update_name_text_layout( frames.player, frames.player.name_source_text or string.upper(UnitName("player") or "") ) end return end if event == "UNIT_TARGET" then if unit == "target" then update_unit_frame("targettarget", palette, true) end update_all_threat(palette) return end if event == "UNIT_THREAT_LIST_UPDATE" or event == "UNIT_THREAT_SITUATION_UPDATE" then update_all_threat(palette) return end if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then handle_cast_event(event, unit, palette) return end if not unit or not frames[unit] then return end if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then update_health(frames[unit], palette) elseif event == "UNIT_POWER_UPDATE" or event == "UNIT_POWER_FREQUENT" or event == "UNIT_MAXPOWER" then update_power(frames[unit], palette) elseif event == "UNIT_DISPLAYPOWER" then update_power(frames[unit], palette, true) elseif event == "UNIT_NAME_UPDATE" or event == "UNIT_LEVEL" or event == "UNIT_FACTION" or event == "UNIT_FLAGS" then update_health(frames[unit], palette) update_power(frames[unit], palette) update_threat(frames[unit], palette) end end function module:initialize() local palette = ns.palette local designelemente = ns.designelemente for index = 1, #unit_order do local unit = unit_order[index] frames[unit] = create_unit_frame( unit, unit_definitions[unit], palette, designelemente ) end health_animation_driver = CreateFrame("Frame") health_animation_driver:SetScript("OnUpdate", update_health_animation) health_animation_driver:Hide() power_animation_driver = CreateFrame("Frame") power_animation_driver:SetScript("OnUpdate", update_power_animation) power_animation_driver:Hide() set_frame_positions() update_all_frames(palette, true) ns:register_profile_provider("unitframes", collect_profile_settings, apply_profile_settings) ns:register_event("UNIT_HEALTH", self, on_event) ns:register_event("UNIT_MAXHEALTH", self, on_event) ns:register_event("UNIT_POWER_UPDATE", self, on_event) ns:register_event("UNIT_POWER_FREQUENT", self, on_event) ns:register_event("UNIT_MAXPOWER", self, on_event) ns:register_event("UNIT_DISPLAYPOWER", self, on_event) ns:register_event("UNIT_NAME_UPDATE", self, on_event) ns:register_event("UNIT_LEVEL", self, on_event) ns:register_event("UNIT_FACTION", self, on_event) ns:register_event("UNIT_FLAGS", self, on_event) ns:register_event("UNIT_TARGET", self, on_event) ns:register_event("PLAYER_LEVEL_UP", self, on_event) ns:register_event("PLAYER_TARGET_CHANGED", self, on_event) ns:register_event("UNIT_PET", self, on_event) ns:register_event("PLAYER_ENTERING_WORLD", self, on_event) ns:register_event("PLAYER_REGEN_DISABLED", self, on_event) ns:register_event("PLAYER_REGEN_ENABLED", self, on_event) ns:register_event("UNIT_THREAT_LIST_UPDATE", self, on_event) ns:register_event("UNIT_THREAT_SITUATION_UPDATE", self, on_event) ns:register_event("UNIT_SPELLCAST_START", self, on_event) ns:register_event("UNIT_SPELLCAST_DELAYED", self, on_event) ns:register_event("UNIT_SPELLCAST_STOP", self, on_event) ns:register_event("UNIT_SPELLCAST_FAILED", self, on_event) ns:register_event("UNIT_SPELLCAST_INTERRUPTED", self, on_event) ns:register_event("UNIT_SPELLCAST_CHANNEL_START", self, on_event) ns:register_event("UNIT_SPELLCAST_CHANNEL_UPDATE", self, on_event) ns:register_event("UNIT_SPELLCAST_CHANNEL_STOP", self, on_event) end ns:register_module("unitframes")