-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathImmersion.lua
More file actions
1050 lines (925 loc) · 34.6 KB
/
Copy pathImmersion.lua
File metadata and controls
1050 lines (925 loc) · 34.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Immersion — targeted fix so buffs don't pop back after fade-out
-- Idea: keep FADE_ONLY during fades, but when finishing a fade-out (alpha=0),
-- call :Hide() *only* on BuffFrame and TemporaryEnchantFrame. On the way back (fade-in),
-- show them at alpha=0 and animate normally.
-- Added: defers BuffFrame/TemporaryEnchantFrame fade-in if a fade-out is mid-flight to avoid flicker.
-- Added: fixes rogue stealth/main bar swap by restoring main action buttons after stance/page changes.
-- The rest of the logic stays aligned with the stable version + timeouts; we don't touch chat/minimap.
-- safe match helper (handles environments where string.match or strmatch may be nil/overridden)
--[[-----------------------------------------------------------------------------
Immersion core controller
- Fades main UI elements in/out depending on:
* Combat state
* Having a living target
* Mouse over the action bars
* Being in a resting zone (inn/city)
- This section is safe to tweak if you want different timings/behavior.
High-level knobs you can customize:
- TARGET_GRACE : seconds to keep UI visible after losing a living target.
- MOUSEOVER_GRACE : seconds to keep UI visible after leaving the action bars.
- CLOSE_WINDOWS_ON_FADE : if true, CloseAllWindows() is called when UI fades out.
- ImmersionDB.fadeTime : base fade duration (seconds), see FreshDB() / InitDB().
- Post-combat grace : configured in PLAYER_REGEN_ENABLED block (search for 'grace = 8.0').
- ZONE_DEBOUNCE : debounce between zone changes before we react to resting.
You can also change FRAME_NAMES / DO_NOT_FORCE_SHOW / FADE_ONLY to include or
exclude extra frames from Immersion control.
-----------------------------------------------------------------------------]]
-- Small safe wrapper around string.match / strmatch.
-- Some UIs override or nil-out one of these, so we try both.
local function s_match(s, p)
local sm = (_G and _G.string and _G.string.match) and _G.string.match or _G.strmatch
if sm then return sm(s, p) end
return nil
end
local ADDON = "Immersion"
local prefix = "|cFF66C2FF[Immersion]|r "
local f = CreateFrame("Frame")
-- Always keep Controllers defined to avoid pairs(nil) before PEW
local Controllers = {}
-- ====== Delay tweaks ======
-- These values control how long the UI stays visible after certain actions.
-- You can freely tweak them to taste without breaking the rest of the addon.
-- TARGET_GRACE controls the grace window AFTER you lose a LIVING target.
local TARGET_GRACE = 5.0 -- seconds
-- MOUSEOVER_GRACE controls the grace window AFTER leaving the action bars with the mouse.
local MOUSEOVER_GRACE = 12.0 -- seconds
-- Only close game windows on fade if explicitly enabled
local CLOSE_WINDOWS_ON_FADE = false
local function CloseWindowsIfAllowed()
if CLOSE_WINDOWS_ON_FADE and CloseAllWindows then
CloseAllWindows()
end
end
-- ===================== Config / DB =====================
-- Returns a brand-new default DB table.
-- You can change default fadeTime or behavior flags here if you like.
local function FreshDB()
return { enabled=true, debug=false, showOnTarget=true, fadeTime=3.0 }
end
-- Normalizes a value into a boolean, accepting numbers and strings
-- like "1", "true", "on", "yes" / "0", "false", etc.
local function asBool(v,d)
if v==nil then return d end
local t=type(v)
if t=="boolean" then return v end
if t=="number" then return v~=0 end
if t=="string" then
local s=string.lower(v)
if s=="1" or s=="true" or s=="on" or s=="yes" then return true end
if s=="0" or s=="false" or s=="off" or s=="no" then return false end
return d
end
return d
end
-- Initializes ImmersionDB, filling defaults and normalizing types.
-- Customization tip: ImmersionDB.fadeTime is the global fade duration used by controllers.
local function InitDB()
if type(ImmersionDB)~="table" then ImmersionDB = FreshDB() end
ImmersionDB.enabled = asBool(ImmersionDB.enabled, true)
ImmersionDB.debug = asBool(ImmersionDB.debug, false)
ImmersionDB.showOnTarget = asBool(ImmersionDB.showOnTarget, true)
ImmersionDB.fadeTime = tonumber(ImmersionDB.fadeTime or 3.0)
end
-- Global lookup helper that works even if getglobal is nil.
local function G(n)
if getglobal then return getglobal(n) end
if _G then return _G[n] end
end
-- Debug print helper (respects ImmersionDB.debug flag).
-- Enable ImmersionDB.debug = true in SavedVariables to see verbose logs.
local function dprint(msg)
if ImmersionDB and ImmersionDB.debug then
DEFAULT_CHAT_FRAME:AddMessage(prefix.."|cFFBBBBBB"..msg.."|r")
end
end
-- ===================== FIXED LIST OF FRAMES =====================
-- List of frames controlled by Immersion.
-- To add/remove frames from being faded, edit this list.
-- Make sure each frame supports :Show(), :Hide() and :SetAlpha().
local FRAME_NAMES = {
-- Action bars
"MainMenuBar",
"MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight",
"PetActionBarFrame", "DFRL_PetBar",
-- MicroMenu
"CharacterMicroButton","SpellbookMicroButton","TalentMicroButton",
"QuestLogMicroButton","SocialsMicroButton","WorldMapMicroButton",
"MainMenuMicroButton","HelpMicroButton",
-- Bags
"MainMenuBarBackpackButton","CharacterBag0Slot","CharacterBag1Slot",
"CharacterBag2Slot","CharacterBag3Slot","KeyRingButton",
-- Unit frames
"PlayerFrame","PetFrame","TargetFrameToT",
-- ChatFrame
"ChatFrameMenuButton","ChatFrame1UpButton","ChatFrame1DownButton","ChatFrame1BottomButton",
-- ShaguDPS
"ShaguDPSWindow","ShaguDPSReset",
-- pfQuest
"pfQuestMapTracker",
-- DragonflightUI: Reforged
"DFRL_GryphonContainer","DFRLBagToggleButton","DFRLEBCMicroButton","DFRLLFTMicroButton",
"DFRLPvPMicroButton","DFRL_MainBar","DFRL_RepBar","DFRL_XPBar",
"DFRL_LatencyIndicator","DFRL_PagingContainer","DFRL_ActionBar","DFRLLowLevelTalentsButton","DFRL_ShapeshiftBar",
"DFRL_PWB_Panel",
-- ItemRack
"ItemRack_InvFrame",
-- Class Addons
"COEEarthFrame","COEFireFrame",
-- Quest tracker (Classic/Turtle)
"QuestWatchFrame",
-- Cast/Buffs
-- "CastingBarFrame", -- don't touch player casting bar
"PetCastingBarFrame",
"BuffFrame","TemporaryEnchantFrame",
}
-- Frames we must NOT force :Show() on (conditional frames)
-- Frames we must not force :Show() on.
-- These are conditional frames that should only appear when the game decides
-- (party frames, pet frame, etc.).
local DO_NOT_FORCE_SHOW = {
TargetFrameToT = true,
PetFrame = true,
PetCastingBarFrame = true,
}
-- Bars/frames that should only alpha-fade (we do NOT call :Hide() at the end)
-- NOTE: Removed BuffFrame/TemporaryEnchantFrame here, because we explicitly Hide() them on fade-out.
-- Frames that only alpha-fade; we do NOT call :Hide() at the end of fade-out.
-- Useful for bars that should always logically exist, just be transparent.
local FADE_ONLY = {
MainMenuBar = true,
MultiBarBottomLeft = true, MultiBarBottomRight = true, MultiBarLeft = true, MultiBarRight = true,
PetActionBarFrame = true,
PetFrame = true, -- prevents disappearing for good at the end of fade-out
-- BuffFrame = true, TemporaryEnchantFrame = true, -- <- removed on purpose
}
-- ===================== CONTROLLERS (one per frame) =====================
-- Creates a controller object for a single frame.
-- Each controller manages:
-- * Fade animation state
-- * Whether a frame should "resume" (be shown again) when UI is revealed
-- * Buff-specific anti-flicker logic (deferred fade in/out)
-- You normally do not need to modify this unless changing fade mechanics.
local function NewController(fr)
local function IsBuffLike(name)
return name == "BuffFrame" or name == "TemporaryEnchantFrame"
end
local c = {}
c.frame = fr
c.fadeCtrl = CreateFrame("Frame"); c.fadeCtrl:Hide()
c.Fade = { active=false, target=nil, start=1, elapsed=0, duration=3.0 }
c.resume = fr:IsShown()
c.deferFadeIn = false -- defers fade-in if a fade-out is in progress (buffs only)
c.deferReason = nil -- optional: remember reason
c.deferFadeOut = false
c.deferOutReason = nil
local function GetAlphaSafe()
local a = c.frame and c.frame.GetAlpha and c.frame:GetAlpha() or 1
return a or 1
end
local function ClampDuration(x)
x = tonumber(x or 0) or 0
if x < 0.05 then x = 0.05 end
return x
end
function c:StartFade(targetAlpha, reason)
if not self.frame then return end
local name = self.frame:GetName() or ""
-- Minimal anti-flicker: if Buff-like is mid fade-IN and fade-OUT is requested, defer the OUT
if IsBuffLike(name) and targetAlpha == 0 and self.Fade.active and self.Fade.target == 1 then
self.deferFadeOut = true
self.deferOutReason = reason or "deferred_buff_fadeout"
return
end
local dontForce = DO_NOT_FORCE_SHOW[name]
if targetAlpha == 1 and dontForce and not self.frame:IsShown() then
dprint("["..name.."] skip force-show (conditional)")
self.Fade.active = false
self.Fade.target = nil
return
end
if targetAlpha == 1 and self.frame:IsShown() then
self.resume = true
end
-- default duration
self.Fade.duration = ClampDuration(ImmersionDB.fadeTime or 3.0)
-- priority fade-ins are shorter
if targetAlpha == 1 and reason and (
string.find(reason, "priority:combat", 1, true) or
string.find(reason, "priority:target", 1, true) or
string.find(reason, "priority:mouseover", 1, true)
) then
self.Fade.duration = ClampDuration(0.8)
end
-- If BUFF-like is fading out and a fade-in arrives, defer the fade-in
if IsBuffLike(name) and self.Fade.active and self.Fade.target == 0 and targetAlpha == 1 then
self.deferFadeIn = true
self.deferReason = reason or "deferred_buff_fadein"
return
end
if self.Fade.active and self.Fade.target == targetAlpha then
dprint("["..(name).."] Fade -> "..targetAlpha.." (skip) ["..(reason or "").."]")
return
end
dprint("["..(name).."] StartFade -> "..targetAlpha.." ["..(reason or "").."]")
self.Fade.active = true
self.Fade.target = targetAlpha
self.Fade.elapsed = 0
self.Fade.start = GetAlphaSafe()
if targetAlpha > self.Fade.start then
if self.frame.Show then self.frame:Show() end
if self.frame.SetAlpha then self.frame:SetAlpha(self.Fade.start) end
end
self.fadeCtrl:Show()
end
c.fadeCtrl:SetScript("OnUpdate", function()
if not (c.Fade.active and c.Fade.target and c.frame) then return end
local dt = arg1 or 0
c.Fade.elapsed = c.Fade.elapsed + dt
local t = c.Fade.elapsed / (c.Fade.duration > 0 and c.Fade.duration or 0.05)
local name = c.frame:GetName() or ""
if t >= 1 then
if c.frame.SetAlpha then c.frame:SetAlpha(c.Fade.target) end
-- If we just completed a fade-in and a fade-out was deferred, run it now
if c.Fade.target == 1 and c.deferFadeOut then
local _r = c.deferOutReason or "deferred_buff_fadeout"
c.deferFadeOut, c.deferOutReason = false, nil
c.Fade.active, c.Fade.target = false, nil
c.Fade.elapsed, c.Fade.start = 0, 1
c:StartFade(0, _r)
return
end
if c.Fade.target == 0 then
-- end of fade-out
if name=="BuffFrame" or name=="TemporaryEnchantFrame" then
if c.frame.Hide then c.frame:Hide() end
-- if a fade-in was deferred, trigger it now cleanly
if c.deferFadeIn then
c.deferFadeIn = false
local reason = c.deferReason or "deferred_buff_fadein"
c.deferReason = nil
-- prepare: show at 0 and then fade-in
if c.frame.Show then c.frame:Show() end
if c.frame.SetAlpha then c.frame:SetAlpha(0) end
c.Fade.active = false
c.Fade.target = nil
c.Fade.elapsed = 0
c.Fade.start = 0
c:StartFade(1, reason)
return
end
elseif not FADE_ONLY[name] and c.frame.Hide then
c.frame:Hide()
end
end
c.Fade.active, c.Fade.target = false, nil
c.fadeCtrl:Hide()
return
end
local newAlpha = c.Fade.start + (c.Fade.target - c.Fade.start) * t
if c.frame.SetAlpha then c.frame:SetAlpha(newAlpha) end
end)
return c
end
-- Rebuilds the Controllers table from FRAME_NAMES.
-- Called on load and once again shortly after PEW to catch late-created frames.
-- If you add new frame names, they will be wired up here.
local function ResolveControllers()
Controllers = {}
for _,name in ipairs(FRAME_NAMES) do
local fr = G(name)
if fr and fr.SetAlpha and fr.Show and fr.Hide then
Controllers[fr] = NewController(fr)
dprint("OK: "..name)
else
dprint("Skip: "..name.." (missing/not API-compatible)")
end
end
local count=0; for _ in pairs(Controllers) do count=count+1 end
dprint("Controllers created: "..count)
end
-- ===================== ZoneText guard & failsafes =====================
-- Returns true if the frame is shown and has a visible alpha (> 0.1).
-- Used to detect active ZoneText/SubZoneText.
local function IsFrameAlphaActive(fr)
if not fr or not fr.IsShown or not fr:IsShown() then return false end
if fr.GetAlpha then
local a = fr:GetAlpha() or 1
if a <= 0.1 then return false end
end
return true
end
-- Returns true if the ZoneText or SubZoneText frames are visible.
-- We delay fades while zone text is on screen to avoid harsh pops.
local function IsZoneTextActive()
local Z, S = G("ZoneTextFrame"), G("SubZoneTextFrame")
return IsFrameAlphaActive(Z) or IsFrameAlphaActive(S)
end
-- EXIT (fade-out): wait for ZoneText to clear, with TIMEOUT (3s)
local zoneHideGuard = CreateFrame("Frame"); zoneHideGuard:Hide()
zoneHideGuard.acc, zoneHideGuard.tick = 0, 0.05
zoneHideGuard.waited, zoneHideGuard.maxWait = 0, 3.0
zoneHideGuard.wantHide = false
zoneHideGuard:SetScript("OnUpdate", function()
local dt = arg1 or 0
zoneHideGuard.acc = zoneHideGuard.acc + dt
zoneHideGuard.waited = zoneHideGuard.waited + dt
if zoneHideGuard.acc < zoneHideGuard.tick then return end
zoneHideGuard.acc = 0
if (not IsZoneTextActive()) or (zoneHideGuard.waited >= zoneHideGuard.maxWait) then
zoneHideGuard:Hide()
if zoneHideGuard.wantHide then
zoneHideGuard.wantHide = false
CloseWindowsIfAllowed()
for fr,c in pairs(Controllers) do
if fr:IsShown() then
c.resume = true
end
c:StartFade(0, (zoneHideGuard.waited >= zoneHideGuard.maxWait) and "hide_timeout" or "hide_after_zone")
end
end
end
end)
-- ENTER (fade-in): guard + timeout
local zoneShowGuard = CreateFrame("Frame"); zoneShowGuard:Hide()
zoneShowGuard.acc, zoneShowGuard.tick = 0, 0.05
zoneShowGuard.waited, zoneShowGuard.maxWait = 0, 0
zoneShowGuard.wantShow = false
zoneShowGuard:SetScript("OnUpdate", function()
local dt = arg1 or 0
zoneShowGuard.acc = zoneShowGuard.acc + dt
zoneShowGuard.waited = zoneShowGuard.waited + dt
if zoneShowGuard.acc < zoneShowGuard.tick then return end
zoneShowGuard.acc = 0
if (not IsZoneTextActive()) or (zoneShowGuard.waited >= zoneShowGuard.maxWait) then
zoneShowGuard:Hide()
if zoneShowGuard.wantShow then
zoneShowGuard.wantShow = false
for _,c in pairs(Controllers) do
if c.resume ~= false then
c:StartFade(1, (zoneShowGuard.waited >= zoneShowGuard.maxWait) and "show_timeout" or "show_after_zone")
end
end
end
end
end)
-- Forces a fade-in on all controllers that are allowed to resume.
-- Respects DO_NOT_FORCE_SHOW and the per-frame resume flags.
local function ForceFadeInAll(reason)
for fr,c in pairs(Controllers) do
local name = fr:GetName() or ""
local dontForce = DO_NOT_FORCE_SHOW[name]
if not dontForce and c.resume ~= false then
c:StartFade(1, reason or "force_fade_in")
end
end
end
-- Immediately restores all resumable frames to full alpha and shown state.
-- Mainly used by failsafes; not normally something you need to call manually.
local function ForceRestoreAllInstant()
for fr,c in pairs(Controllers) do
if c.resume ~= false then
local n = fr:GetName() or ""
local unit = fr.unit
if unit and UnitExists and UnitExists(unit) then
c.resume = true
if fr.SetAlpha then fr:SetAlpha(1) end
if fr.Show then fr:Show() end
end
if not DO_NOT_FORCE_SHOW[n] then -- do not force-show conditional frames
if fr.Show then fr:Show() end
if fr.SetAlpha then fr:SetAlpha(1) end
end
end
end
end
local restoreFailsafe = CreateFrame("Frame"); restoreFailsafe:Hide()
restoreFailsafe.t, restoreFailsafe.timeout = 0, 4.0
restoreFailsafe:SetScript("OnUpdate", function()
restoreFailsafe.t = restoreFailsafe.t + (arg1 or 0)
if restoreFailsafe.t >= restoreFailsafe.timeout then
restoreFailsafe:Hide()
dprint("Failsafe: forcing fade-in (respecting resume/conditionals).")
ForceFadeInAll("failsafe_timer")
end
end)
-- ===================== Helpers =====================
local function RestoreMainActionButtons()
for i = 1, 12 do
local btn = G("ActionButton"..i)
if btn then
if btn.Show then btn:Show() end
if btn.SetAlpha then btn:SetAlpha(1) end
end
end
for i = 1, 12 do
local btn = G("BonusActionButton"..i)
if btn then
if btn.Show then btn:Show() end
if btn.SetAlpha then btn:SetAlpha(1) end
end
end
local mm = G("MainMenuBar")
if mm then
if mm.Show then mm:Show() end
if mm.SetAlpha then mm:SetAlpha(1) end
end
end
local function RestoreDragonflightBarArtwork()
local names = {
"DFRL_MainBar",
"DFRL_ActionBar",
"DFRL_PWB_Panel",
}
for _, name in ipairs(names) do
local fr = G(name)
if fr then
if fr.Show then fr:Show() end
if fr.SetAlpha then fr:SetAlpha(1) end
if fr.GetRegions then
local regions = { fr:GetRegions() }
for _, r in ipairs(regions) do
if r then
if r.Show then r:Show() end
if r.SetAlpha then r:SetAlpha(1) end
end
end
end
end
end
end
local function RestoreDragonflightCustomButtonBackgrounds()
for i = 1, 12 do
local bg = G("DFRL_ActionButtonBg"..i)
if bg then
if bg.Show then bg:Show() end
if bg.SetAlpha then bg:SetAlpha(1) end
end
local border = G("DFRL_ActionButtonBorder"..i)
if border then
if border.Show then border:Show() end
if border.SetAlpha then border:SetAlpha(1) end
end
end
end
local function HideBlizzardButtonNormals()
local function HideButtonNormal(btn)
if not btn then return end
local nt = btn.GetNormalTexture and btn:GetNormalTexture() or nil
if nt then
if nt.Hide then nt:Hide() end
if nt.SetAlpha then nt:SetAlpha(0) end
end
local name = btn.GetName and btn:GetName() or nil
if name then
local normal = G(name .. "NormalTexture")
if normal then
if normal.Hide then normal:Hide() end
if normal.SetAlpha then normal:SetAlpha(0) end
end
local floatingBG = G(name .. "FloatingBG")
if floatingBG then
if floatingBG.Hide then floatingBG:Hide() end
if floatingBG.SetAlpha then floatingBG:SetAlpha(0) end
end
end
end
for i = 1, 12 do
HideButtonNormal(G("ActionButton"..i))
HideButtonNormal(G("BonusActionButton"..i))
end
end
local function NeedsActionBarFix()
for i = 1, 12 do
local btn = G("ActionButton"..i)
if btn then
local nt = btn.GetNormalTexture and btn:GetNormalTexture() or nil
if nt and nt.IsShown and nt:IsShown() then
return true
end
end
local bbtn = G("BonusActionButton"..i)
if bbtn then
local nt = bbtn.GetNormalTexture and bbtn:GetNormalTexture() or nil
if nt and nt.IsShown and nt:IsShown() then
return true
end
end
end
return false
end
-- Triggers fade-out on all controlled frames, optionally delayed by ZoneText.
-- Customization tip: toggling CLOSE_WINDOWS_ON_FADE changes whether game windows
-- (spellbook, character sheet, etc.) are auto-closed when the UI fades out.
local function HideAll(reason)
local any=false; for _ in pairs(Controllers) do any=true; break end
if not any then return end
if IsZoneTextActive() then
dprint("ZoneText visible — delaying fade-out (all)")
zoneHideGuard.wantHide = true
zoneHideGuard.waited = 0
zoneHideGuard:Show()
return
end
CloseWindowsIfAllowed()
for fr,c in pairs(Controllers) do
-- Do not overwrite resume to false when the frame is already hidden.
-- Only mark resume as true if the frame is currently visible.
if fr:IsShown() then
c.resume = true
end
c:StartFade(0, reason or "hide")
end
end
-- Triggers fade-in on all frames that have resume ~= false.
-- Also makes sure party backgrounds are visible if you are grouped.
local function ShowAll(reason)
local any=false; for _ in pairs(Controllers) do any=true; break end
if not any then return end
if IsZoneTextActive() then
dprint("ZoneText visible — delaying fade-in (all)")
zoneShowGuard.wantShow = true
zoneShowGuard.waited = 0
zoneShowGuard:Show()
restoreFailsafe.t = 0; restoreFailsafe:Show()
return
end
restoreFailsafe:Hide()
for _,c in pairs(Controllers) do
local fr = c.frame
local n = fr and (fr:GetName() or "") or ""
local unit = fr and fr.unit or nil
if unit and UnitExists and UnitExists(unit) then
c.resume = true
if fr and fr.Show then fr:Show() end
end
if c.resume ~= false then c:StartFade(1, reason or "show") end
end
end
-- ===================== Debounced ENTER logic =====================
local ZONE_DEBOUNCE = 0.6
-- Increase this if you see flicker when changing zones; decrease to react faster.
local lastZoneEvent, pendingRestingCheck = 0, false
local scheduler = CreateFrame("Frame"); scheduler:Hide()
scheduler.timeLeft = 0
scheduler:SetScript("OnUpdate", function()
scheduler.timeLeft = scheduler.timeLeft - (arg1 or 0)
if scheduler.timeLeft <= 0 then
scheduler:Hide()
if pendingRestingCheck then
pendingRestingCheck = false
f:Evaluate("zone_debounced")
end
end
end)
-- Shows UI when you are in a resting zone, with zone-change debounce.
-- This avoids flickering when multiple ZONE_CHANGED events fire quickly.
-- If you want extra delay for resting, this is one of the safe places to tweak.
local function DebouncedShowForResting()
local now = GetTime and GetTime() or 0
if now - lastZoneEvent < ZONE_DEBOUNCE then
pendingRestingCheck = true
scheduler.timeLeft = ZONE_DEBOUNCE - (now - lastZoneEvent)
if scheduler.timeLeft < 0.05 then scheduler.timeLeft = 0.05 end
scheduler:Show()
dprint("Waiting zone debounce: "..string.format("%.2f", scheduler.timeLeft).."s")
return
end
if IsZoneTextActive() then
zoneShowGuard.wantShow = true
zoneShowGuard.waited = 0
zoneShowGuard:Show()
restoreFailsafe.t = 0; restoreFailsafe:Show()
else
restoreFailsafe:Hide()
for _,c in pairs(Controllers) do
local fr = c.frame
local n = fr and (fr:GetName() or "") or ""
local unit = fr and fr.unit or nil
if unit and UnitExists and UnitExists(unit) then
c.resume = true
if fr and fr.Show then fr:Show() end
end
if c.resume ~= false then c:StartFade(1, "resting") end
end
end
end
-- ===================== Action bars mouseover =====================
-- Heuristic: returns true if a frame name looks like an action bar or related
-- button/container. Used by hoverWatch to detect mouse-over on action bars.
local function IsActionBarish(name)
if not name then return false end
-- Common buttons
if string.find(name, "ActionButton", 1, true) then return true end -- ActionButton1..12
if string.find(name, "BonusActionButton", 1, true) then return true end
if string.find(name, "MultiBar", 1, true) then return true end -- MultiBarBottomLeftButton...
if string.find(name, "PetActionButton", 1, true) then return true end
if string.find(name, "ShapeshiftButton", 1, true) then return true end
-- Common containers
if name=="MainMenuBar" or name=="PetActionBarFrame" or name=="ShapeshiftBarFrame" then return true end
-- Compat containers (DragonFlight-like)
if name=="DFRL_ActionBar" or name=="DFRL_MainBar" then return true end
return false
end
local hoverWatch = CreateFrame("Frame"); hoverWatch:Show()
hoverWatch.acc, hoverWatch.tick = 0, 0.05
hoverWatch:SetScript("OnUpdate", function()
local dt = arg1 or 0
hoverWatch.acc = hoverWatch.acc + dt
if hoverWatch.acc < hoverWatch.tick then return end
hoverWatch.acc = 0
local mf = GetMouseFocus and GetMouseFocus() or nil
local name = mf and mf.GetName and mf:GetName() or nil
local onBars = IsActionBarish(name)
if onBars ~= f.mouseOverBars then
f.mouseOverBars = onBars
if onBars then
-- Entered the bars: cancel leave window and show
f.postMouseoverGraceUntil = 0
f:Evaluate("mouseover_bars_enter")
else
-- Left the bars: keep visible for MOUSEOVER_GRACE
local now = GetTime and GetTime() or 0
f.postMouseoverGraceUntil = now + MOUSEOVER_GRACE
if C_TimerAfter then
C_TimerAfter(MOUSEOVER_GRACE, function()
if not f.mouseOverBars and not f.inCombat then
f.postMouseoverGraceUntil = 0
f:Evaluate("mouseover_bars_end_delayed")
end
end)
end
f:Evaluate("mouseover_bars_leave_grace")
end
end
end)
-- ===================== Main logic =====================
f.inCombat = false
f.postCombatGraceUntil = 0 -- post-combat grace window (GetTime)
f.postTargetGraceUntil = 0 -- post-target-loss grace window (GetTime)
f.lastTargetAlive = nil -- last known target alive state
f.mouseOverBars = false
f.postMouseoverGraceUntil = 0 -- post-mouseover grace window (GetTime)
-- Returns true if the player is at full health (HP == MaxHP).
-- Used to prevent UI fade-out while the player is injured.
local function IsPlayerFullHealth()
if not (UnitHealth and UnitHealthMax) then return true end -- safe fallback
local max = UnitHealthMax("player") or 0
if max <= 0 then return false end
local hp = UnitHealth("player") or 0
return hp >= max
end
-- Central brain of Immersion.
-- Decides whether the UI should be visible or hidden based on:
-- * Combat flag
-- * Alive target
-- * Mouse over action bars
-- * Resting state
-- * Grace windows after combat/target/mouseover
-- You can adjust grace durations via:
-- TARGET_GRACE, MOUSEOVER_GRACE and the 'grace' value in PLAYER_REGEN_ENABLED.
function f:Evaluate(reason)
if not ImmersionDB or not ImmersionDB.enabled then
dprint("Disabled; no action.")
return
end
local now = GetTime and GetTime() or 0
-- Grace windows: (post-combat, post-target, post-mouseover)
if (not f.inCombat) and (
(f.postCombatGraceUntil or 0) > now or
(f.postTargetGraceUntil or 0) > now or
(f.postMouseoverGraceUntil or 0) > now
) then
restoreFailsafe:Hide()
ShowAll("grace_window")
return
end
-- NEW: Do not fade out UI while in a raid
local inRaid = (IsInRaid and IsInRaid()) or (GetNumRaidMembers and (GetNumRaidMembers() or 0) > 0)
if inRaid then
restoreFailsafe:Hide()
ShowAll("priority:raid")
return
end
-- Fade-in: combat, living target, or mouseover on bars
if f.inCombat or (
ImmersionDB.showOnTarget
and UnitExists and UnitExists("target")
and not UnitIsDeadOrGhost("target")
) or f.mouseOverBars then
restoreFailsafe:Hide()
local why = f.inCombat and "combat" or (f.mouseOverBars and "mouseover" or "target")
ShowAll("priority:"..why)
return
end
-- NEW: Do not fade out UI if player is not at 100% health
if not IsPlayerFullHealth() then
restoreFailsafe:Hide()
ShowAll("priority:player_not_full_hp")
return
end
-- Remaining logic
if IsResting() then
DebouncedShowForResting()
else
restoreFailsafe:Hide()
HideAll("not_resting")
end
end
-- ===================== Events =====================
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_UPDATE_RESTING")
f:RegisterEvent("PLAYER_REGEN_DISABLED")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:RegisterEvent("PLAYER_TARGET_CHANGED")
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("ZONE_CHANGED")
f:RegisterEvent("ZONE_CHANGED_INDOORS")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:RegisterEvent("UNIT_HEALTH")
f:RegisterEvent("UNIT_MAXHEALTH")
f:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
f:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
f:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
f:RegisterEvent("UPDATE_SHAPESHIFT_USABLE")
-- Master event handler.
-- Handles:
-- PLAYER_ENTERING_WORLD : initialization + startup delay
-- PLAYER_REGEN_DISABLED : entering combat
-- PLAYER_REGEN_ENABLED : leaving combat with grace window
-- PLAYER_TARGET_CHANGED : target logic + grace when target is lost
-- PLAYER_UPDATE_RESTING : entering/leaving resting areas
-- ZONE_* events : debounce + resting check on zone change
-- GROUP_ROSTER_UPDATE : re-evaluate state (raid/party changes)
f:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
InitDB()
C_TimerAfter = function(sec, fn)
local t = CreateFrame("Frame")
t.elapsed = 0
t:SetScript("OnUpdate", function()
t.elapsed = t.elapsed + (arg1 or 0)
if t.elapsed >= sec then
t:SetScript("OnUpdate", nil)
if type(fn) == "function" then fn() end
end
end)
end
-- Create controllers normally
ResolveControllers()
C_TimerAfter(0.3, ResolveControllers)
dprint("Loaded. Applying 5-second startup delay.")
-- Wait 5 seconds before enabling the addon
C_TimerAfter(5, function()
f:Evaluate("entering_world_delayed")
end)
return
end
if event=="PLAYER_REGEN_DISABLED" then
f.inCombat = true
f.postCombatGraceUntil = 0 -- cancel any previous window
f.postTargetGraceUntil = 0 -- combat has priority
f.postMouseoverGraceUntil = 0
f:Evaluate("combat_start")
return
end
if event=="PLAYER_REGEN_ENABLED" then
-- Leaving combat: apply an Xs window BEFORE starting fade-out
f.inCombat = false
-- Customization: this is how long (in seconds) the UI stays after leaving combat.
local grace = 10.0 -- adjust post-combat delay here
f.postCombatGraceUntil = (GetTime and GetTime() or 0) + grace
C_TimerAfter(grace, function()
if not f.inCombat then
f.postCombatGraceUntil = 0
f:Evaluate("combat_end_delayed")
end
end)
return
end
if event=="PLAYER_TARGET_CHANGED" then
local hasTarget = UnitExists and UnitExists("target")
if hasTarget then
-- Update: is the current target alive?
local alive = not UnitIsDeadOrGhost("target")
f.lastTargetAlive = alive
-- Aiming a living target cancels the post-target window (avoid delayed fade-out)
if alive then
f.postTargetGraceUntil = 0
end
f:Evaluate("target_changed")
return
else
-- Now there is no target
-- If the LAST target was dead, do nothing (no window, no Evaluate)
if f.lastTargetAlive == false then
dprint("Target cleared (was dead) — no UI change.")
f.lastTargetAlive = nil
return
end
-- Last target was living (or unknown): start post-target window
f.lastTargetAlive = nil
local now2 = GetTime and GetTime() or 0
f.postTargetGraceUntil = now2 + TARGET_GRACE
C_TimerAfter(TARGET_GRACE, function()
-- Only apply if there is still no target and we didn't re-enter combat
if not (UnitExists and UnitExists("target")) and not f.inCombat then
f.postTargetGraceUntil = 0
f:Evaluate("target_end_delayed")
end
end)
-- Keep UI visible during the window
f:Evaluate("target_lost_grace")
return
end
end
if event=="PLAYER_UPDATE_RESTING" then
if IsResting() then
-- Waits 4 seconds before fading the UI in when entering resting state
C_TimerAfter(4.0, function()
-- If, when the timer fires, we are still in resting state, then show the UI
if IsResting and IsResting() then
DebouncedShowForResting()
C_TimerAfter(1.0, function() ForceFadeInAll("resting_timer_1s") end)
C_TimerAfter(3.5, function() ForceFadeInAll("resting_timer_3_5s") end)
end
end)
return
end
end
if event=="ZONE_CHANGED" or event=="ZONE_CHANGED_INDOORS" or event=="ZONE_CHANGED_NEW_AREA" then
lastZoneEvent = GetTime and GetTime() or 0
dprint("Zone event: "..event)
if IsResting() then
DebouncedShowForResting()
return
end
end
if event=="GROUP_ROSTER_UPDATE" then
f:Evaluate("group_roster_update")
return
end
if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
if arg1 == "player" then
f:Evaluate("player_health_changed")
end
return
end
if event=="UPDATE_BONUS_ACTIONBAR"
or event=="ACTIONBAR_PAGE_CHANGED"
or event=="UPDATE_SHAPESHIFT_FORMS"
or event=="UPDATE_SHAPESHIFT_USABLE" then
local shouldBeVisible = false
local now = GetTime and GetTime() or 0
if f.inCombat
or f.mouseOverBars
or ((f.postCombatGraceUntil or 0) > now)
or ((f.postTargetGraceUntil or 0) > now)
or ((f.postMouseoverGraceUntil or 0) > now)
or (ImmersionDB.showOnTarget and UnitExists and UnitExists("target") and not UnitIsDeadOrGhost("target"))
or IsResting()
or not IsPlayerFullHealth()
or ((IsInRaid and IsInRaid()) or (GetNumRaidMembers and (GetNumRaidMembers() or 0) > 0)) then
shouldBeVisible = true
end
if shouldBeVisible then