---------------------------------------------------- -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("plaketten") local nameplate_width = 150 local nameplate_health_height = 15 local nameplate_cast_height = 5 local nameplate_cast_gap = 5 local nameplate_name_gap = 5 local nameplate_target_scale = 1.08 local health_smoothing_duration = 0.5 local npc_title_retry_delay = 0.05 local npc_title_max_attempts = 3 local friendly_nameplate_cvars = { "nameplateShowFriends", "nameplateShowFriendlyNPCs", "nameplateShowFriendlyPets", "nameplateShowFriendlyGuardians", "nameplateShowFriendlyTotems", "nameplateShowFriendlyMinions", } local plate_data_by_base = setmetatable({}, { __mode = "k" }) local plate_data_by_unit = {} local pending_nameplate_units = {} local npc_title_cache = {} local pending_npc_title_requests = {} local friendly_cvar_state = {} local pending_friendly_cvar_values = {} local friendly_cvar_update_pending = false local health_animation_driver local update_cast_progress local title_tooltip local settings_state = { scale = 100, threat_enabled = true, blizzard_nameplates_shown = false, target_highlight_enabled = true, health_smoothing_enabled = true, } local function get_unit_color(unit, palette) 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") elseif 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 is_friendly_unit(unit) local reaction = UnitReaction(unit, "player") return reaction ~= nil and reaction >= 5 end local function get_cast_info(unit) local name, _, _, start_time_ms, end_time_ms = UnitCastingInfo(unit) if name then return name, start_time_ms, end_time_ms, false end name, _, _, start_time_ms, end_time_ms = UnitChannelInfo(unit) if name then return name, start_time_ms, end_time_ms, true end return nil 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_plate_data(nameplate_base) local palette = ns.palette local designelemente = ns.designelemente local frame = CreateFrame("Frame", nil, nameplate_base) frame:SetSize(nameplate_width, nameplate_health_height) frame:SetPoint("CENTER", nameplate_base, "CENTER", 0, 0) frame:SetIgnoreParentScale(true) frame:SetFrameLevel(nameplate_base:GetFrameLevel() + 20) frame:EnableMouse(false) local health_bar = create_statusbar(frame, designelemente, palette) health_bar:SetSize(nameplate_width, nameplate_health_height) health_bar:SetPoint("CENTER", frame, "CENTER", 0, 0) local health_border = designelemente:create_border_overlay( health_bar, frame, "frame" ) local name_text = frame:CreateFontString(nil, "OVERLAY") name_text:SetFontObject(samui_font_14_outline) name_text:SetPoint("BOTTOM", health_bar, "TOP", 0, nameplate_name_gap) name_text:SetJustifyH("CENTER") name_text:SetWordWrap(false) local secondary_text = frame:CreateFontString(nil, "OVERLAY") secondary_text:SetFontObject(samui_font_12_outline) secondary_text:SetPoint("TOP", name_text, "BOTTOM", 0, -2) secondary_text:SetJustifyH("CENTER") secondary_text:SetWordWrap(false) secondary_text:Hide() local cast_bar = create_statusbar(frame, designelemente, palette) cast_bar:SetSize(nameplate_width, nameplate_cast_height) cast_bar:SetPoint("TOP", health_bar, "BOTTOM", 0, -nameplate_cast_gap) cast_bar:SetStatusBarColor(palette:get_rgb("gold")) cast_bar:Hide() local data = { base = nameplate_base, frame = frame, health_bar = health_bar, health_border = health_border, name_text = name_text, secondary_text = secondary_text, cast_bar = cast_bar, unit = nil, guid = nil, cast_name = nil, cast_active = false, cast_start_time = nil, cast_end_time = nil, cast_is_channel = nil, cast_display_value = nil, friendly = false, health_animation = { initialized = false, current_value = 0, start_value = 0, target_value = 0, elapsed = 0, active = false, }, } cast_bar:SetScript("OnUpdate", function(_, elapsed) if update_cast_progress then update_cast_progress(data, elapsed) end end) frame:Hide() plate_data_by_base[nameplate_base] = data return data end local function get_or_create_plate_data(nameplate_base) local data = plate_data_by_base[nameplate_base] if data then return data end return create_plate_data(nameplate_base) end local function reset_health_animation(data) local state = data.health_animation if not state then return end state.initialized = false state.current_value = 0 state.start_value = 0 state.target_value = 0 state.elapsed = 0 state.active = false end local function set_health_bar_value(data, current, maximum, force_immediate) local state = data.health_animation if not state then data.health_bar:SetValue(current) return 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 settings_state.health_smoothing_enabled == false then state.initialized = true state.current_value = current state.start_value = current state.target_value = current state.elapsed = 0 state.active = false data.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 _, data in pairs(plate_data_by_unit) do local state = data.health_animation if state and state.active then if not data.unit or data.friendly or settings_state.health_smoothing_enabled == 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) data.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 apply_plate_scale(data) local scale = settings_state.scale / 100 if settings_state.target_highlight_enabled and data.unit and UnitIsUnit(data.unit, "target") then scale = scale * nameplate_target_scale end data.frame:SetScale(scale) end local function update_health_border(data) if not data.health_border or not data.unit then return end local token = "border" if settings_state.target_highlight_enabled and UnitIsUnit(data.unit, "target") then token = "text" elseif settings_state.threat_enabled and not data.friendly then local _, status, scaled_percentage = UnitDetailedThreatSituation("player", data.unit) 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("player", data.unit) or status if status == 2 or status == 3 then token = "red" elseif status == 1 then token = "orange" end end end data.health_border:SetBackdropBorderColor( ns.palette:get_rgba(token, ns.palette:get_alpha("opaque")) ) end local function format_secondary_text(value) if type(value) ~= "string" or value == "" then return nil end return "<" .. value .. ">" end local function uppercase_npc_name(value) if type(value) ~= "string" then return "" end local uppercase_value = string.upper(value) uppercase_value = uppercase_value:gsub("ä", "Ä") uppercase_value = uppercase_value:gsub("ö", "Ö") uppercase_value = uppercase_value:gsub("ü", "Ü") uppercase_value = uppercase_value:gsub("ß", "SS") return uppercase_value end local function update_name(data) if not data.unit then return end local red, green, blue = get_unit_color(data.unit, ns.palette) local unit_is_player = UnitIsPlayer(data.unit) local unit_name = UnitName(data.unit) or "" if not unit_is_player then unit_name = uppercase_npc_name(unit_name) end local name = data.cast_name or unit_name data.name_text:SetText(name) if data.cast_name then data.name_text:SetTextColor(ns.palette:get_rgb("text")) else data.name_text:SetTextColor(red, green, blue) end data.secondary_text:Hide() data.secondary_text:SetText("") if not data.friendly then return end local secondary_value if unit_is_player then secondary_value = GetGuildInfo(data.unit) elseif data.guid then local cached_title = npc_title_cache[data.guid] if type(cached_title) == "string" then secondary_value = cached_title end end local secondary_text = format_secondary_text(secondary_value) if secondary_text then data.secondary_text:SetText(secondary_text) data.secondary_text:SetTextColor(ns.palette:get_rgb("text")) data.secondary_text:Show() end end local function reset_cast_progress(data) data.cast_active = false data.cast_start_time = nil data.cast_end_time = nil data.cast_is_channel = nil data.cast_display_value = nil end local function update_layout(data) if not data.unit then return end data.friendly = is_friendly_unit(data.unit) local unit_is_player = UnitIsPlayer(data.unit) data.name_text:ClearAllPoints() data.name_text:SetWidth(0) if data.friendly then data.health_bar:Hide() if data.health_border then data.health_border:Hide() end data.cast_bar:Hide() reset_cast_progress(data) reset_health_animation(data) data.name_text:SetFontObject(samui_font_12_outline) data.name_text:SetPoint("CENTER", data.frame, "CENTER", 0, 6) else data.health_bar:Show() if data.health_border then data.health_border:Show() end if unit_is_player then data.name_text:SetFontObject(samui_font_14_outline) else data.name_text:SetFontObject(samui_font_12_outline) data.name_text:SetWidth(nameplate_width) end data.name_text:SetPoint( "BOTTOM", data.health_bar, "TOP", 0, nameplate_name_gap ) end update_name(data) end local function update_health(data, force_immediate) if not data.unit or data.friendly then return end local maximum = UnitHealthMax(data.unit) or 0 local current = UnitHealth(data.unit) or 0 if maximum <= 0 then maximum = 1 current = 0 elseif current < 0 then current = 0 elseif current > maximum then current = maximum end local red, green, blue = get_unit_color(data.unit, ns.palette) data.health_bar:SetMinMaxValues(0, maximum) set_health_bar_value(data, current, maximum, force_immediate) data.health_bar:SetStatusBarColor(red, green, blue) update_health_border(data) end update_cast_progress = function(data, elapsed) if not data.cast_active or not data.cast_start_time or not data.cast_end_time or data.friendly then return end local now = GetTime() local duration = data.cast_end_time - data.cast_start_time if duration <= 0 or now >= data.cast_end_time then data.cast_name = nil reset_cast_progress(data) data.cast_bar:Hide() update_name(data) return end local value if data.cast_is_channel then value = data.cast_end_time - now else value = now - data.cast_start_time end if value < 0 then value = 0 elseif value > duration then value = duration end data.cast_bar:SetMinMaxValues(0, math.max(duration, 0.001)) local displayed = data.cast_display_value if type(displayed) ~= "number" or settings_state.health_smoothing_enabled == false then displayed = value else local progress = (elapsed or 0) / health_smoothing_duration if progress > 1 then progress = 1 elseif progress < 0 then progress = 0 end displayed = displayed + ((value - displayed) * progress) end data.cast_display_value = displayed data.cast_bar:SetValue(displayed) end local function refresh_cast(data) if not data.unit then return end local name, start_time_ms, end_time_ms, is_channel = get_cast_info(data.unit) local previous_name = data.cast_name data.cast_name = name if not name or not start_time_ms or not end_time_ms then reset_cast_progress(data) data.cast_bar:Hide() update_name(data) return end if not data.friendly then local is_new_cast = not data.cast_active or previous_name ~= name or data.cast_is_channel ~= is_channel data.cast_active = true data.cast_start_time = start_time_ms / 1000 data.cast_end_time = end_time_ms / 1000 data.cast_is_channel = is_channel if is_new_cast then data.cast_display_value = nil end data.cast_bar:SetStatusBarColor(ns.palette:get_rgb("gold")) update_cast_progress(data, 0) data.cast_bar:Show() else reset_cast_progress(data) data.cast_bar:Hide() end update_name(data) end local function get_title_tooltip() if title_tooltip then return title_tooltip end title_tooltip = CreateFrame( "GameTooltip", "samui_nameplate_title_tooltip", UIParent, "GameTooltipTemplate" ) title_tooltip:SetOwner(UIParent, "ANCHOR_NONE") title_tooltip:Hide() return title_tooltip end local function tooltip_line_matches_unit_level(unit, value) if type(value) ~= "string" or value == "" then return false end local level = UnitLevel(unit) if level == -1 then return value:find("??", 1, true) ~= nil end if type(level) ~= "number" or level <= 0 then return false end return value:find(tostring(level), 1, true) ~= nil end local function read_npc_title(unit) local tooltip = get_title_tooltip() tooltip:SetOwner(UIParent, "ANCHOR_NONE") tooltip:ClearLines() tooltip:SetUnit(unit) local title_line = _G["samui_nameplate_title_tooltipTextLeft2"] local level_line = _G["samui_nameplate_title_tooltipTextLeft3"] local title = title_line and title_line:GetText() or nil local level_text = level_line and level_line:GetText() or nil tooltip:Hide() if type(title) ~= "string" or title == "" then return nil end if not tooltip_line_matches_unit_level(unit, level_text) then return nil end return title end local function request_npc_title(data, guid, attempt) if not data.unit or data.guid ~= guid or UnitGUID(data.unit) ~= guid or not data.friendly or UnitIsPlayer(data.unit) then pending_npc_title_requests[guid] = nil return end local title = read_npc_title(data.unit) if title then npc_title_cache[guid] = title pending_npc_title_requests[guid] = nil update_name(data) return end if attempt >= npc_title_max_attempts then npc_title_cache[guid] = false pending_npc_title_requests[guid] = nil return end C_Timer.After(npc_title_retry_delay, function() request_npc_title(data, guid, attempt + 1) end) end local function ensure_npc_title(data) if not data.friendly or UnitIsPlayer(data.unit) or not data.guid then return end if npc_title_cache[data.guid] ~= nil or pending_npc_title_requests[data.guid] then return end pending_npc_title_requests[data.guid] = true request_npc_title(data, data.guid, 1) end local function refresh_plate(data, force_health_immediate) if not data.unit or not UnitExists(data.unit) then return end data.guid = UnitGUID(data.unit) update_layout(data) update_health(data, force_health_immediate) refresh_cast(data) apply_plate_scale(data) ensure_npc_title(data) end local function refresh_all_plates(force_health_immediate) for _, data in pairs(plate_data_by_unit) do refresh_plate(data, force_health_immediate) end end local function update_all_target_states() for _, data in pairs(plate_data_by_unit) do apply_plate_scale(data) update_health_border(data) end end local function handle_nameplate_added(unit) if settings_state.blizzard_nameplates_shown or not UnitExists(unit) then return end local nameplate_base = C_NamePlate.GetNamePlateForUnit(unit) if not nameplate_base then ns:chat_message("Native Plaketten-Basis für '" .. tostring(unit) .. "' fehlt", true) return end if not plate_data_by_base[nameplate_base] and InCombatLockdown() then pending_nameplate_units[unit] = true return end local data = get_or_create_plate_data(nameplate_base) if data.unit and data.unit ~= unit then plate_data_by_unit[data.unit] = nil end reset_health_animation(data) reset_cast_progress(data) data.cast_name = nil data.unit = unit data.guid = UnitGUID(unit) plate_data_by_unit[unit] = data data.frame:SetFrameLevel(nameplate_base:GetFrameLevel() + 20) data.frame:Show() refresh_plate(data, true) if type(ns.refresh_blizzard_nameplate_visibility) == "function" then ns:refresh_blizzard_nameplate_visibility() end end local function handle_nameplate_removed(unit) pending_nameplate_units[unit] = nil local data = plate_data_by_unit[unit] if not data then return end plate_data_by_unit[unit] = nil data.frame:Hide() data.cast_bar:Hide() reset_health_animation(data) reset_cast_progress(data) data.unit = nil data.guid = nil data.cast_name = nil data.friendly = false end local function is_restricted_instance() local _, instance_type = IsInInstance() return instance_type == "party" or instance_type == "raid" end local function set_friendly_cvar(cvar_name, value) pending_friendly_cvar_values[cvar_name] = tostring(value) SetCVar(cvar_name, value) end local function update_friendly_nameplate_cvars(force_restore) local should_override = not force_restore and not settings_state.blizzard_nameplates_shown and is_restricted_instance() if InCombatLockdown() and not force_restore then friendly_cvar_update_pending = true return false end friendly_cvar_update_pending = false for index = 1, #friendly_nameplate_cvars do local cvar_name = friendly_nameplate_cvars[index] local current_value = GetCVar(cvar_name) local state = friendly_cvar_state[cvar_name] if current_value ~= nil then if should_override then if not state then state = { original_value = current_value, overridden = true, } friendly_cvar_state[cvar_name] = state elseif not state.overridden then state.original_value = current_value state.overridden = true end if current_value ~= "0" then set_friendly_cvar(cvar_name, "0") end elseif state and state.overridden then state.overridden = false if state.original_value ~= nil and current_value ~= state.original_value then set_friendly_cvar(cvar_name, state.original_value) end end end end return true end local function handle_cvar_update(cvar_name) local state = friendly_cvar_state[cvar_name] if not state then return end local current_value = GetCVar(cvar_name) if current_value == nil then return end local pending_value = pending_friendly_cvar_values[cvar_name] if pending_value ~= nil and pending_value == current_value then pending_friendly_cvar_values[cvar_name] = nil return end if state.overridden then state.original_value = current_value update_friendly_nameplate_cvars(false) end end local function collect_profile_settings() return { scale = settings_state.scale, threat_enabled = settings_state.threat_enabled, blizzard_nameplates_shown = settings_state.blizzard_nameplates_shown, target_highlight_enabled = settings_state.target_highlight_enabled, health_smoothing_enabled = settings_state.health_smoothing_enabled, } end local function get_valid_scale(data) local value = data.scale if type(value) == "number" and value >= 50 and value <= 150 then local normalized_value = math.floor((value + 5) / 10) * 10 data.scale = normalized_value return normalized_value end if value ~= nil then ns:chat_message("Ungültige Plaketten-Skalierung wurde auf Standard zurückgesetzt", true) end data.scale = 100 return 100 end local function get_valid_boolean(data, key, default_value, label) local value = data[key] if type(value) == "boolean" then return value end if value ~= nil then ns:chat_message("Ungültiger Wert für '" .. label .. "' wurde auf Standard zurückgesetzt", true) end data[key] = default_value return default_value end local function apply_profile_settings(data) if type(data) ~= "table" then return false end settings_state.scale = get_valid_scale(data) settings_state.threat_enabled = get_valid_boolean( data, "threat_enabled", true, "Bedrohungsanzeige" ) settings_state.blizzard_nameplates_shown = get_valid_boolean( data, "blizzard_nameplates_shown", false, "Blizzard-Plaketten" ) settings_state.target_highlight_enabled = get_valid_boolean( data, "target_highlight_enabled", true, "Ziel hervorheben" ) settings_state.health_smoothing_enabled = get_valid_boolean( data, "health_smoothing_enabled", true, "Smooth Health" ) if type(ns.set_blizzard_nameplates_shown) == "function" then ns:set_blizzard_nameplates_shown(settings_state.blizzard_nameplates_shown) end if settings_state.blizzard_nameplates_shown then for unit, plate_data in pairs(plate_data_by_unit) do plate_data.frame:Hide() plate_data.cast_bar:Hide() reset_health_animation(plate_data) reset_cast_progress(plate_data) plate_data_by_unit[unit] = nil plate_data.unit = nil plate_data.guid = nil plate_data.cast_name = nil end else refresh_all_plates(settings_state.health_smoothing_enabled == false) end update_friendly_nameplate_cvars(false) return true end local function on_event(_, event, unit) if event == "NAME_PLATE_UNIT_ADDED" then handle_nameplate_added(unit) return elseif event == "NAME_PLATE_UNIT_REMOVED" then handle_nameplate_removed(unit) return elseif event == "PLAYER_TARGET_CHANGED" then update_all_target_states() return elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" or event == "GROUP_ROSTER_UPDATE" then update_friendly_nameplate_cvars(false) return elseif event == "PLAYER_REGEN_ENABLED" then for pending_unit in pairs(pending_nameplate_units) do pending_nameplate_units[pending_unit] = nil if UnitExists(pending_unit) then handle_nameplate_added(pending_unit) end end if friendly_cvar_update_pending then update_friendly_nameplate_cvars(false) end return elseif event == "PLAYER_LOGOUT" then update_friendly_nameplate_cvars(true) return elseif event == "CVAR_UPDATE" then handle_cvar_update(unit) return end local data = unit and plate_data_by_unit[unit] or nil if event == "UNIT_THREAT_LIST_UPDATE" or event == "UNIT_THREAT_SITUATION_UPDATE" then if data then update_health_border(data) else for _, plate_data in pairs(plate_data_by_unit) do update_health_border(plate_data) end end return end if not data then return end if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then update_health(data) elseif event == "UNIT_NAME_UPDATE" or event == "UNIT_FACTION" or event == "UNIT_FLAGS" then refresh_plate(data) elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then refresh_cast(data) elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then data.cast_name = nil reset_cast_progress(data) data.cast_bar:Hide() update_name(data) end end function module:initialize() get_title_tooltip() health_animation_driver = CreateFrame("Frame") health_animation_driver:SetScript("OnUpdate", update_health_animation) health_animation_driver:Hide() ns:register_profile_provider("plaketten", collect_profile_settings, apply_profile_settings) ns:register_event("NAME_PLATE_UNIT_ADDED", self, on_event) ns:register_event("NAME_PLATE_UNIT_REMOVED", self, on_event) ns:register_event("UNIT_HEALTH", self, on_event) ns:register_event("UNIT_MAXHEALTH", self, on_event) ns:register_event("UNIT_NAME_UPDATE", self, on_event) ns:register_event("UNIT_FACTION", self, on_event) ns:register_event("UNIT_FLAGS", 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) ns:register_event("PLAYER_TARGET_CHANGED", self, on_event) ns:register_event("PLAYER_ENTERING_WORLD", self, on_event) ns:register_event("ZONE_CHANGED_NEW_AREA", self, on_event) ns:register_event("GROUP_ROSTER_UPDATE", self, on_event) ns:register_event("PLAYER_REGEN_ENABLED", self, on_event) ns:register_event("PLAYER_LOGOUT", self, on_event) ns:register_event("CVAR_UPDATE", self, on_event) end ns:register_module("plaketten")