diff --git a/README.md b/README.md
index 87755c4d..00c8f3f4 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,8 @@ Provides:
- Click-casting
- Loot Roll History (/loothistory)
- New item highlighting
+- Loss of control alert - Stuns, fears, silences and school lockouts from C_LossOfControl
+- Dressing room zoom, pan and per-slot undress (PlayerModel / DressUpModel methods)
- Plenty other functions
### [Nampower](https://github.com/brues-code/nampower)
diff --git a/env/locales_deDE.lua b/env/locales_deDE.lua
index b30380bd..b003060b 100644
--- a/env/locales_deDE.lua
+++ b/env/locales_deDE.lua
@@ -30,4 +30,20 @@ pfUI_locale["deDE"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (DE) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["deDE"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "Betört",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "Verwirrt",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "Entwaffnet",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "Verängstigt",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s gesperrt",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "Befriedet",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "Kampfunfähig",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "Besessen",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "Bewegungsunfähig",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "Unterbrochen",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "Stille",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "Betäubt",
+ LOSS_OF_CONTROL_SECONDS = "Sek.",
+}
diff --git a/env/locales_enUS.lua b/env/locales_enUS.lua
index 261f08ca..59aec467 100644
--- a/env/locales_enUS.lua
+++ b/env/locales_enUS.lua
@@ -30,4 +30,20 @@ pfUI_locale["enUS"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (GB) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["enUS"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "Charmed",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "Confused",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "Disarmed",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "Feared",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s Locked",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "Pacified",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "Disabled",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "Possessed",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "Rooted",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "Interrupted",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "Silenced",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "Stunned",
+ LOSS_OF_CONTROL_SECONDS = "seconds",
+}
diff --git a/env/locales_esES.lua b/env/locales_esES.lua
index 2a9bdc08..2bd50411 100644
--- a/env/locales_esES.lua
+++ b/env/locales_esES.lua
@@ -30,4 +30,20 @@ pfUI_locale["esES"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (ES) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["esES"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "Embelesado",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "Confundido",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "Desarmado",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "Asustado",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "Bloqueo: %s",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "Pacificado",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "Inutilizado",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "Poseído",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "Enraizado",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "Interrumpido",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "Silenciado",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "Aturdido",
+ LOSS_OF_CONTROL_SECONDS = "segundos",
+}
diff --git a/env/locales_frFR.lua b/env/locales_frFR.lua
index b2cf186c..6b7544c1 100644
--- a/env/locales_frFR.lua
+++ b/env/locales_frFR.lua
@@ -30,4 +30,20 @@ pfUI_locale["frFR"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (FR) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["frFR"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "Charme",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "Confusion",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "Désarmement",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "Peur",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s verrouillé",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "Pacification",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "Neutralisation",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "$gPossédé:Possédée;",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "Immobilisation",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "Interruption",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "Silence",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "Étourdissement",
+ LOSS_OF_CONTROL_SECONDS = "secondes",
+}
diff --git a/env/locales_koKR.lua b/env/locales_koKR.lua
index b724661e..ca0d5508 100644
--- a/env/locales_koKR.lua
+++ b/env/locales_koKR.lua
@@ -30,4 +30,20 @@ pfUI_locale["koKR"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (KR) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["koKR"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "현혹",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "혼란",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "무장 해제",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "공포",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s 계열 차단됨",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "평정",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "무력화",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "빙의",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "이동 불가",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "차단",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "침묵",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "기절",
+ LOSS_OF_CONTROL_SECONDS = "초",
+}
diff --git a/env/locales_ruRU.lua b/env/locales_ruRU.lua
index 9cac15df..79a5dba6 100644
--- a/env/locales_ruRU.lua
+++ b/env/locales_ruRU.lua
@@ -30,4 +30,20 @@ pfUI_locale["ruRU"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (RU) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["ruRU"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "Подчинение",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "Растерянный",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "Без оружия",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "Страх",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s: способности недоступны",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "Покой",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "Отключено",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "Подчинен",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "Обездвиженность",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "Прерывание",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "Немота.",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "Оглушение",
+ LOSS_OF_CONTROL_SECONDS = "сек",
+}
diff --git a/env/locales_zhCN.lua b/env/locales_zhCN.lua
index 851d6504..3ab45124 100644
--- a/env/locales_zhCN.lua
+++ b/env/locales_zhCN.lua
@@ -30,4 +30,20 @@ pfUI_locale["zhCN"]["resurrections"] = {
-- custom entries not detected by DBC extractor
-
+-- Classic Era GlobalStrings (CN) that the loss of control alert
+-- reads and the 1.12 client lacks.
+pfUI_locale["zhCN"]["lossofcontrol"] = {
+ LOSS_OF_CONTROL_DISPLAY_CHARM = "魅惑",
+ LOSS_OF_CONTROL_DISPLAY_CONFUSE = "迷惑",
+ LOSS_OF_CONTROL_DISPLAY_DISARM = "缴械",
+ LOSS_OF_CONTROL_DISPLAY_FEAR = "恐惧",
+ LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL = "%s系法术被锁定",
+ LOSS_OF_CONTROL_DISPLAY_PACIFY = "平静",
+ LOSS_OF_CONTROL_DISPLAY_PACIFYSILENCE = "禁用",
+ LOSS_OF_CONTROL_DISPLAY_POSSESS = "被占据",
+ LOSS_OF_CONTROL_DISPLAY_ROOT = "被定身",
+ LOSS_OF_CONTROL_DISPLAY_SCHOOL_INTERRUPT = "打断",
+ LOSS_OF_CONTROL_DISPLAY_SILENCE = "沉默",
+ LOSS_OF_CONTROL_DISPLAY_STUN = "昏迷",
+ LOSS_OF_CONTROL_SECONDS = "秒",
+}
diff --git a/init/modules.xml b/init/modules.xml
index 50e353c9..5ae46479 100644
--- a/init/modules.xml
+++ b/init/modules.xml
@@ -82,4 +82,5 @@
+
\ No newline at end of file
diff --git a/modules/lossofcontrol.lua b/modules/lossofcontrol.lua
new file mode 100644
index 00000000..d369fbe2
--- /dev/null
+++ b/modules/lossofcontrol.lua
@@ -0,0 +1,94 @@
+pfUI:RegisterModule("lossofcontrol", function ()
+ -- 5.x's loss of control alert. ClassicAPI's C_LossOfControl builds the
+ -- effects from the player's debuffs and from the school lockout of an
+ -- interrupt.
+ local strings = L["lossofcontrol"]
+
+ -- how much each effect takes away; the alert shows the strongest one
+ local PRIORITY = {
+ STUN = 3, FEAR = 3, CONFUSE = 3, CHARM = 3, POSSESS = 3, PACIFYSILENCE = 3,
+ SILENCE = 2, SCHOOL_INTERRUPT = 2, PACIFY = 2,
+ DISARM = 1, ROOT = 1,
+ }
+
+ -- the lockout packet does not name the interrupting spell
+ local INTERRUPT_ICON = "Interface\\Icons\\Ability_Kick"
+
+ local function IsStronger(data, best)
+ local priority, bestPriority = PRIORITY[data.locType], PRIORITY[best.locType]
+ if priority ~= bestPriority then return priority > bestPriority end
+ return (data.timeRemaining or math.huge) > (best.timeRemaining or math.huge)
+ end
+
+ local frame = CreateFrame("Frame", "pfLossOfControl", UIParent)
+ frame:SetSize(220, 48)
+ frame:SetPoint("CENTER", 0, 120)
+ frame:SetFrameStrata("HIGH")
+ frame:Hide()
+
+ frame.icon = CreateFrame("Frame", nil, frame)
+ frame.icon:SetSize(48, 48)
+ frame.icon:SetPoint("LEFT", 0, 0)
+ CreateBackdrop(frame.icon, nil, true)
+
+ frame.icon.tex = frame.icon:CreateTexture(nil, "ARTWORK")
+ HandleIcon(frame.icon, frame.icon.tex)
+
+ frame.cd = CreateFrame("Cooldown", nil, frame.icon, "CooldownFrameTemplate")
+ frame.cd:SetAllPoints(frame.icon.tex)
+ frame.cd.pfCooldownStyleAnimation = 1
+ -- the alert prints the time itself
+ frame.cd.noCooldownCount = true
+
+ frame.label = frame:CreateFontString(nil, "OVERLAY")
+ frame.label:SetFont(pfUI.font_default, 20, "OUTLINE")
+ frame.label:SetTextColor(NORMAL_FONT_COLOR:GetRGB())
+ frame.label:SetPoint("TOPLEFT", frame.icon, "TOPRIGHT", 8, -4)
+
+ frame.time = frame:CreateFontString(nil, "OVERLAY")
+ frame.time:SetFont(pfUI.font_default, 14, "OUTLINE")
+ frame.time:SetPoint("BOTTOMLEFT", frame.icon, "BOTTOMRIGHT", 8, 4)
+
+ frame.Refresh = function(self)
+ local best
+ for i = 1, C_LossOfControl.GetActiveLossOfControlDataCount() do
+ local data = C_LossOfControl.GetActiveLossOfControlData(i)
+ if not best or IsStronger(data, best) then best = data end
+ end
+
+ if not best then
+ if pfUI.unlock and pfUI.unlock:IsShown() then return end
+ self:Hide()
+ return
+ end
+
+ if best.locType == "SCHOOL_INTERRUPT" then
+ self.label:SetText(format(strings.LOSS_OF_CONTROL_DISPLAY_INTERRUPT_SCHOOL, C_Spell.GetSchoolString(best.lockoutSchool)))
+ self.icon.tex:SetTexture(INTERRUPT_ICON)
+ else
+ self.label:SetText(strings["LOSS_OF_CONTROL_DISPLAY_" .. best.locType])
+ self.icon.tex:SetTexture(best.iconTexture)
+ end
+
+ -- a refreshed effect or a second kick on the same school changes the
+ -- timer without an event, so this runs again while the alert shows
+ if self.start ~= best.startTime or self.duration ~= best.duration then
+ self.start, self.duration = best.startTime, best.duration
+ CooldownFrame_SetTimer(self.cd, best.startTime or 0, best.duration or 0, best.startTime and 1 or 0)
+ end
+
+ self.time:SetText(best.timeRemaining and format("%.1f %s", best.timeRemaining, strings.LOSS_OF_CONTROL_SECONDS) or "")
+ self:Show()
+ end
+
+ frame:SetScript("OnUpdate", function()
+ if ( this.tick or 0) > GetTime() then return else this.tick = GetTime() + .1 end
+ this:Refresh()
+ end)
+
+ pfUI.events:RegisterFrameEventAndCallback("LOSS_OF_CONTROL_ADDED", function() frame:Refresh() end)
+ pfUI.events:RegisterFrameEventAndCallback("LOSS_OF_CONTROL_UPDATE", function() frame:Refresh() end)
+
+ UpdateMovable(frame)
+ pfUI.lossofcontrol = frame
+end)