-- TalkingHeadFrame Revamp: Less intrudingIsWorldQuest local _, addon = ... local API = addon.API; local L = addon.L; local FADE_OUT_AFTER_FINISH = 0.5; local QUEST_DESCRIPTION_GRADIENT_LENGTH = 30; local TEXT_WIDTH = 512; local LINE_SPACING = 2; local DEFAULT_POSITION_Y = 336; local SPEECH_FORMAT_GENRIC = "|cffffd100%s: |r%s"; local SPEECH_FORMAT_PVP = "|cffff2020%s: |r%s"; local FadeFrame = API.UIFrameFade; local Lerp = API.Lerp; local Round = API.Round; local strlenutf8 = strlenutf8; local ceil = math.ceil; local max = math.max; local format = string.format; local PlaySound = PlaySound; local StopSound = StopSound; local GetCurrentLineInfo = C_TalkingHead.GetCurrentLineInfo; local IsWorldQuest = C_QuestLog.IsWorldQuest; local IsQuestTask = C_QuestLog.IsQuestTask; local IsInInstance = IsInInstance; --local SetPortraitTextureFromCreatureDisplayID = SetPortraitTextureFromCreatureDisplayID; local DB; local PVP_CREATURE_DISPLAYID = { [108418] = true, }; local NewTalkingHead = CreateFrame("Frame", nil, UIParent); NewTalkingHead:Hide(); NewTalkingHead:SetFrameStrata("FULLSCREEN"); --LOW NewTalkingHead:SetFrameLevel(980); NewTalkingHead:SetSize(TEXT_WIDTH, 32); --[[cause taint "UIParentBottomManagedFrameTemplate" NewTalkingHead.layoutIndex = 1208; NewTalkingHead.hideWhenActionBarIsOverriden = false; NewTalkingHead.align = "center"; --]] addon.TalkingHead = NewTalkingHead; local function SetFontShadow(fontString) --Game Bug: Shadow is removed upon SetAlphaGradient fontString:SetShadowColor(0, 0, 0, 1); fontString:SetShadowOffset(1, -1); end local function SetupFontString(fontString) fontString:SetJustifyH("CENTER"); fontString:SetJustifyV("TOP"); fontString:SetSpacing(LINE_SPACING); SetFontShadow(fontString); end local function FadeOutAfter_OnUpdate(self, elapsed) self.t = self.t + elapsed; if self.t >= self.fadeOutDelay then self:FadeOutText(true); end end local function FadeIn_TypeWriter_OnUpdate(self, elapsed) self.t = self.t + elapsed; self.fadingProgress = self.fadingProgress + (elapsed * self.gradientCPS); self.gradientLength = self.gradientLength + 10 * elapsed; if self.gradientLength > QUEST_DESCRIPTION_GRADIENT_LENGTH then self.gradientLength = QUEST_DESCRIPTION_GRADIENT_LENGTH; end if not self.LineText:SetAlphaGradient(self.fadingProgress, self.gradientLength) then self:SetScript("OnUpdate", FadeOutAfter_OnUpdate); return true end if self.t < self.backgroundFadeDuration then self.Background:SetAlpha(Lerp(0, 1, self.t / self.backgroundFadeDuration)); end end local function FadeIn_InstantText_OnUpdate(self, elapsed) self.t = self.t + elapsed; self.fadingProgress = self.fadingProgress + 4*elapsed; if self.fadingProgress >= 1 then self:SetScript("OnUpdate", FadeOutAfter_OnUpdate); self.fadingProgress = 1; self.LineText:SetAlpha(1); self.Background:SetAlpha(1); return true end self.LineText:SetAlpha(self.fadingProgress); self.Background:SetAlpha(self.fadingProgress); end local function FadeOut_OnUpdate(self, elapsed) self.t = self.t + elapsed; if self.t >= 0 then self.textAlpha = self.textAlpha - 2 * elapsed; if self.textAlpha <= 0 then self.textAlpha = 0; self:Hide(); self.isFadingOut = false; self:SetScript("OnUpdate", nil); end self.LineText:SetAlpha(self.textAlpha); self.Background:SetAlpha(self.textAlpha); end end function NewTalkingHead:SetText(text) self.LineText:SetText(text); self.LineText:Show(); local textWidth = self.LineText:GetWrappedWidth(); local textHeight = self.LineText:GetHeight() - (self.fontHeight + LINE_SPACING); local numLines = self.LineText:GetNumLines() + 1; self.Background:ClearAllPoints(); self.Background:SetPoint("CENTER", self, "TOP", 0, -0.5*( numLines*(self.fontHeight + LINE_SPACING) - LINE_SPACING)); self.Background:SetSize(textWidth + 32, textHeight + 32); self:UpdateFrameSize(); end function NewTalkingHead:FadeInText(speakerName, text, voiceoverDuration, hideName, isFinalLine, displayInfo) if not (text and speakerName and voiceoverDuration) then return end; if not hideName then local speechFormat; if displayInfo and PVP_CREATURE_DISPLAYID[displayInfo] then speechFormat = SPEECH_FORMAT_PVP; else speechFormat = SPEECH_FORMAT_GENRIC; end text = format(speechFormat, speakerName, text); end local numChars = strlenutf8(text); if numChars == 0 or voiceoverDuration == 0 then return end; numChars = numChars + 15; text = " \n"..text; self:SetText(text); self.gradientCPS = ceil(numChars / voiceoverDuration) + 10; self.fadingProgress = 0; self.backgroundFadeDuration = max(voiceoverDuration - 1, 1); self.t = 0; self.gradientLength = QUEST_DESCRIPTION_GRADIENT_LENGTH; if isFinalLine then self.fadeOutDelay = voiceoverDuration + 1; else self.fadeOutDelay = voiceoverDuration; end self.isFadingOut = false; if self.instantText then self.LineText:SetAlpha(0); self.Background:SetAlpha(0); self:SetScript("OnUpdate", FadeIn_InstantText_OnUpdate); else self.LineText:SetAlpha(1); self.LineText:SetAlphaGradient(0, QUEST_DESCRIPTION_GRADIENT_LENGTH); self.Background:SetAlpha(0); self:SetScript("OnUpdate", FadeIn_TypeWriter_OnUpdate); end FadeFrame(self, 0, 1); end function NewTalkingHead:FadeOutText(noDelay) if self:IsShown() then if not self.isFadingOut then self.textAlpha = self.LineText:GetAlpha(); self.t = (noDelay and 0) or -1; self:SetScript("OnUpdate", FadeOut_OnUpdate); end else self.isFadingOut = false; self.LineText:Hide(); self.LineText:SetAlpha(0); self.textAlpha = 0; self:SetScript("OnUpdate", nil); end end function NewTalkingHead:SetFontHeightByPercentage(percentage) if not percentage then percentage = 100; end if percentage < 100 then percentage = 100; elseif percentage > 120 then percentage = 120; end local font, baseFontHeight = QuestFont:GetFont(); local fontHeight = Round(percentage * 0.01 * baseFontHeight); self.fontHeight = fontHeight; local style, gray; if DB.TalkingHead_TextOutline then style = "OUTLINE"; gray = 0.898; --VERY_LIGHT_GRAY_COLOR else style = ""; gray = 1; end self.LineText:SetFont(font, fontHeight, style); self.LineText:SetTextColor(gray, gray, gray); SetupFontString(self.LineText); DB.TalkingHead_FontSize = percentage; end function NewTalkingHead:LoadPosition() self:ClearAllPoints(); if DB.TalkingHead_PositionX and DB.TalkingHead_PositionY then if DB.TalkingHead_PositionX > 0 then self:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", DB.TalkingHead_PositionX, DB.TalkingHead_PositionY); else self:SetPoint("TOP", UIParent, "BOTTOM", 0, DB.TalkingHead_PositionY); end else self:SetPoint("TOP", UIParent, "BOTTOM", 0, DEFAULT_POSITION_Y); end end function NewTalkingHead:Init() DB = PlumberDB; self:LoadPosition(); local _, fontHeight = QuestFont:GetFont(); self.baseFontHeight = Round(fontHeight + 0.5); self.fontHeight = self.baseFontHeight; self.LineText = NewTalkingHead:CreateFontString(nil, "OVERLAY", "QuestFont"); self.LineText:SetPoint("TOP", self, "TOP", 0, 0); self.LineText:SetWidth(TEXT_WIDTH); self.LineText:SetAlpha(0); self:SetFontHeightByPercentage(DB.TalkingHead_FontSize); self.Background = self:CreateTexture(nil, "BACKGROUND"); self.Background:SetTexture("Interface/AddOns/Plumber/Art/Frame/SubtitleShadow_NineSlice"); self.Background:SetTextureSliceMargins(30, 30, 30, 30); self.Background:SetTextureSliceMode(0); self.Background:SetSize(128, 32); self.Background:SetPoint("CENTER", self, "TOP", 0, 0); --[[ self.Portrait = self:CreateTexture(nil, "BACKGROUND", nil, -1); self.Portrait:SetSize(115, 115); self.Portrait:SetPoint("CENTER", self, "CENTER", 0, -6); --]] self:OnSettingsChanged(); self.Init = function() end; end function NewTalkingHead:EnableTalkingHead() if self.enabled then return end; if self.blizzardTalkingHeadEnabled == nil then self.blizzardTalkingHeadEnabled = TalkingHeadFrame:IsEventRegistered("TALKINGHEAD_REQUESTED"); end TalkingHeadFrame:UnregisterEvent("TALKINGHEAD_REQUESTED"); self.enabled = true; self:Init(); self:RegisterEvent("TALKINGHEAD_REQUESTED"); self:RegisterEvent("TALKINGHEAD_CLOSE"); self:RegisterEvent("LOADING_SCREEN_ENABLED"); self:RegisterEvent("PLAYER_ENTERING_WORLD"); self:SetScript("OnEvent", self.OnEvent); if not self.editmodeHooked then self.editmodeHooked = true; EventRegistry:RegisterCallback("EditMode.Enter", self.EnterEditMode, self); EventRegistry:RegisterCallback("EditMode.Exit", self.ExitEditMode, self); end end function NewTalkingHead:DisableTalkingHead() if self.enabled then if self.blizzardTalkingHeadEnabled then TalkingHeadFrame:RegisterEvent("TALKINGHEAD_REQUESTED"); end self.blizzardTalkingHeadEnabled = nil; self.enabled = false; self:UnregisterEvent("TALKINGHEAD_REQUESTED"); self:UnregisterEvent("TALKINGHEAD_CLOSE"); self:UnregisterEvent("SOUNDKIT_FINISHED"); self:UnregisterEvent("LOADING_SCREEN_ENABLED"); self:UnregisterEvent("PLAYER_ENTERING_WORLD"); self:UnregisterEvent("QUEST_ACCEPTED"); self:UnregisterEvent("QUEST_TURNED_IN"); self:OnTalkingHeadClose(); self:SetScript("OnEvent", nil); end end function NewTalkingHead:TryDisable() --Disable if not used in any other modules if not DB then DB = PlumberDB end; if DB.TalkingHead_MasterSwitch then return end; self:DisableTalkingHead(); end function NewTalkingHead:OnTalkingHeadRequested() if self.voHandle then StopSound(self.voHandle); self.voHandle = nil; end local displayInfo, cameraID, vo, duration, lineNumber, numLines, name, text, isNewTalkingHead, textureKit = GetCurrentLineInfo(); --print(name, displayInfo, vo, duration) --Debug if displayInfo and displayInfo ~= 0 and duration then local success, voHandle = PlaySound(vo, "Talking Head", true, true); if success then self:RegisterEvent("SOUNDKIT_FINISHED"); self.voHandle = voHandle; end --lineNumber start from 0 local isFinalLine = lineNumber and numLines and (lineNumber + 1 == numLines); if self.shouldMuteLine then if isFinalLine then self.shouldMuteLine = nil; else self:MuteNextLine(duration); end return end if self.hideInInstance and self.inInstance then return end duration = duration - FADE_OUT_AFTER_FINISH; --reserved for fade-out between line local hideName = (lineNumber ~= 0) and (name and name == self.lastName); self.lastName = name; self:FadeInText(name, text, duration, hideName, isFinalLine, displayInfo); --SetPortraitTextureFromCreatureDisplayID(self.Portrait, displayInfo); end end function NewTalkingHead:FadeOutFrame() FadeFrame(self, 0.5, 0); end function NewTalkingHead:OnTalkingHeadClose() self:FadeOutFrame(); self.lastName = nil; end function NewTalkingHead:CloseImmediately() if self.voHandle then StopSound(self.voHandle); end FadeFrame(self, 0, 0); self.lastName = nil; end local function ShouldMuteText(questID) --Sometimes TALKINGHEAD_REQUESTED fires before QUEST_ACCEPTED, and we won't be able to mute it --Sira Moonwarden's WQ returns wrong type? return questID and (IsWorldQuest(questID) or IsQuestTask(questID)) end function NewTalkingHead:OnEvent(event, ...) if self.isEditing then return end; if event == "TALKINGHEAD_REQUESTED" then self:OnTalkingHeadRequested(); elseif event == "TALKINGHEAD_CLOSE" then --This event will triggered automatically, not closed by the user self:OnTalkingHeadClose(); elseif event == "SOUNDKIT_FINISHED" then self:UnregisterEvent(event); local voHandle = ... if self.voHandle == voHandle then self.voHandle = nil; end elseif event == "LOADING_SCREEN_ENABLED" then self:CloseImmediately(); elseif event == "PLAYER_ENTERING_WORLD" then self.inInstance = IsInInstance(); elseif event == "QUEST_ACCEPTED" then local questID = ... if ShouldMuteText(questID) then self:MuteNextLine(); end elseif event == "QUEST_TURNED_IN" then local questID = ... if ShouldMuteText(questID) then self:MuteNextLine(); end end end function NewTalkingHead:OnSettingsChanged() if not DB then DB = PlumberDB; end; if not DB then return end; if DB.TalkingHead_InstantText then self.instantText = true; else self.instantText = false; end if DB.TalkingHead_HideInInstance then self.hideInInstance = true; else self.hideInInstance = false; end if DB.TalkingHead_HideWorldQuest then self:RegisterEvent("QUEST_ACCEPTED"); self:RegisterEvent("QUEST_TURNED_IN"); else self:UnregisterEvent("QUEST_ACCEPTED"); self:UnregisterEvent("QUEST_TURNED_IN"); self.shouldMuteLine = nil; end end ---- Edit Mode function NewTalkingHead:EnterEditMode() if not self.enabled then return end; self:Init(); if not self.Selection then local uiName = "Simple Talking Head"; local hideLabel = true; self.Selection = addon.CreateEditModeSelection(self, uiName, hideLabel); end self.isEditing = true; self:SetScript("OnUpdate", nil); FadeFrame(self, 0, 1); self.Selection:ShowHighlighted(); self:ShowExampleText(); end function NewTalkingHead:ExitEditMode() if self.Selection then self.Selection:Hide(); end self:ShowOptions(false); self.isEditing = false; self:CloseImmediately(); end local function FadeIn_TypeWriter_NoAutoHide_OnUpdate(self, elapsed) if FadeIn_TypeWriter_OnUpdate(self, elapsed) then self:SetScript("OnUpdate", nil); end end local function FadeIn_InstantText_NoAutoHide_OnUpdate(self, elapsed) if FadeIn_InstantText_OnUpdate(self, elapsed) then self:SetScript("OnUpdate", nil); end end function NewTalkingHead:ShowExampleText(animate) local exampleText = format(SPEECH_FORMAT_GENRIC, "The Speaker", "Time is an illusion that helps things make sense. So we are always living in the present tense."); self:SetText(" \n"..exampleText); self.LineText:SetAlpha(1); if animate then self.fadingProgress = 0; self.t = 0; if self.instantText then self:SetScript("OnUpdate", FadeIn_InstantText_NoAutoHide_OnUpdate); else local numChars = strlenutf8(exampleText); local voiceoverDuration = 3; self.gradientCPS = ceil(numChars / voiceoverDuration) + 10; self.fadingProgress = 0; self.backgroundFadeDuration = max(voiceoverDuration - 1, 1); self.gradientLength = QUEST_DESCRIPTION_GRADIENT_LENGTH; self:SetScript("OnUpdate", FadeIn_TypeWriter_NoAutoHide_OnUpdate); end end end function NewTalkingHead:IsFocused() return (self:IsShown() and self:IsMouseOver()) or (self.OptionFrame and self.OptionFrame:IsShown() and self.OptionFrame:IsMouseOver()) end local function Options_FontSizeSlider_OnValueChanged(value) NewTalkingHead:SetFontHeightByPercentage(value); NewTalkingHead:UpdateFrameSize(); end local function Options_FontSizeSlider_FormatValue(value) return format("%.0f%%", value); end local function Options_InstantText_OnClick(self, state) NewTalkingHead:OnSettingsChanged(); NewTalkingHead:ShowExampleText(true); end local function Options_TextOutline_OnClick(self, state) NewTalkingHead:SetFontHeightByPercentage(DB.TalkingHead_FontSize); end local function Options_HideInInstance_OnClick(self, state) NewTalkingHead:OnSettingsChanged(); end local function Options_HideWorldQuest_OnClick(self, state) NewTalkingHead:OnSettingsChanged(); end local function Options_ResetPosition_ShouldEnable(self) if DB.TalkingHead_PositionX and DB.TalkingHead_PositionY then return true else return false end end local function Options_ResetPosition_OnClick(self) self:Disable(); DB.TalkingHead_PositionX = nil; DB.TalkingHead_PositionY = nil; NewTalkingHead:LoadPosition(); end local OPTIONS_SCHEMATIC = { title = L["EditMode TalkingHead"], widgets = { {type = "Checkbox", label = L["TalkingHead Option InstantText"], onClickFunc = Options_InstantText_OnClick, dbKey = "TalkingHead_InstantText"}, {type = "Checkbox", label = L["TalkingHead Option TextOutline"], onClickFunc = Options_TextOutline_OnClick, dbKey = "TalkingHead_TextOutline"}, {type = "Slider", label = L["Font Size"], minValue = 100, maxValue = 120, valueStep = 10, onValueChangedFunc = Options_FontSizeSlider_OnValueChanged, formatValueFunc = Options_FontSizeSlider_FormatValue, dbKey = "TalkingHead_FontSize"}, {type = "Divider"}, {type = "Header", label = L["TalkingHead Option Condition Header"]}; {type = "Checkbox", label = L["TalkingHead Option Condition Instance"] , onClickFunc = Options_HideInInstance_OnClick, dbKey = "TalkingHead_HideInInstance", tooltip = L["TalkingHead Option Condition Instance Tooltip"]}, {type = "Checkbox", label = L["TalkingHead Option Condition WorldQuest"], onClickFunc = Options_HideWorldQuest_OnClick, dbKey = "TalkingHead_HideWorldQuest", tooltip = L["TalkingHead Option Condition WorldQuest Tooltip"]}, {type = "Divider"}, {type = "UIPanelButton", label = L["Reset To Default Position"], onClickFunc = Options_ResetPosition_OnClick, stateCheckFunc = Options_ResetPosition_ShouldEnable, widgetKey = "ResetButton"}, } }; function NewTalkingHead:CreateOptions() self.OptionFrame = addon.SetupSettingsDialog(self, OPTIONS_SCHEMATIC); end function NewTalkingHead:ShowOptions(state) if state then self:CreateOptions(); self.OptionFrame:Show(); if self.OptionFrame.requireResetPosition then self.OptionFrame.requireResetPosition = false; self.OptionFrame:ClearAllPoints(); self.OptionFrame:SetPoint("LEFT", UIParent, "CENTER", TEXT_WIDTH * 0.5, 0); end else if self.OptionFrame then self.OptionFrame:Hide(); end if not API.IsInEditMode() then self:CloseImmediately(); end end end function NewTalkingHead:ToggleOptions() self:ShowOptions( self.OptionFrame and self.OptionFrame:IsShown() ); NewTalkingHead:EnterEditMode() end function NewTalkingHead:OnDragStart() self:SetMovable(true); self:SetDontSavePosition(true); self:SetClampedToScreen(true); self:StartMoving(); end function NewTalkingHead:OnDragStop() self:StopMovingOrSizing(); local centerX = self:GetCenter(); local uiCenter = UIParent:GetCenter(); local left = self:GetLeft(); local top = self:GetTop(); left = Round(left); top = Round(top); self:ClearAllPoints(); --Convert anchor and save position if math.abs(uiCenter - centerX) <= 48 then --Snap to centeral line self:SetPoint("TOP", UIParent, "BOTTOM", 0, top); DB.TalkingHead_PositionX = -1; else self:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top); DB.TalkingHead_PositionX = left; end DB.TalkingHead_PositionY = top; if self.OptionFrame then local button = self.OptionFrame:FindWidget("ResetButton"); if button then button:Enable(); end end end function NewTalkingHead:UpdateFrameSize() local height = (self.LineText:GetHeight() or 0) + self.fontHeight; self:SetHeight(height); end local MuteDelay; local function MuteDelay_OnUpdate(self, elapsed) self.t = self.t + elapsed; if self.t > self.duration then self.t = nil; self:SetScript("OnUpdate", nil); NewTalkingHead.shouldMuteLine = nil; end end function NewTalkingHead:MuteNextLine(duration) --Sound is still played, just no text --Doesn't always work when you accept multiple WQ at the same location if not MuteDelay then MuteDelay = CreateFrame("Frame"); end if (not duration) or duration <= 0.5 then duration = 0.5; end if MuteDelay.t then MuteDelay.duration = (MuteDelay.duration or 0) + duration + 0.5; else MuteDelay.t = 0; MuteDelay.duration = duration; end MuteDelay:SetScript("OnUpdate", MuteDelay_OnUpdate); self.shouldMuteLine = true; end do local function EnableModule(state) if state then NewTalkingHead:EnableTalkingHead(); else NewTalkingHead:DisableTalkingHead(); end end local function OptionToggle_OnClick(self, button) if NewTalkingHead.OptionFrame and NewTalkingHead.OptionFrame:IsShown() then NewTalkingHead:ShowOptions(false); NewTalkingHead:ExitEditMode(); else NewTalkingHead:EnterEditMode(); NewTalkingHead:ShowOptions(true); end end local moduleData = { name = addon.L["ModuleName TalkingHead"], dbKey = "TalkingHead_MasterSwitch", description = addon.L["ModuleDescription TalkingHead"], toggleFunc = EnableModule, categoryID = 2, uiOrder = 3, moduleAddedTime = 1704423300, optionToggleFunc = OptionToggle_OnClick, }; addon.ControlCenter:AddModule(moduleData); end