-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCore.lua
More file actions
3942 lines (3750 loc) · 190 KB
/
Copy pathCore.lua
File metadata and controls
3942 lines (3750 loc) · 190 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
local ADDON, ns = ...
ns.NAME = "Frame Gambit"
ns.VERSION = "1.1.0"
ns.ICON_TEXTURE = "Interface\\AddOns\\FrameGambit\\Assets\\FrameGambitIcon"
-- Increment only when the player-facing Help topics materially change.
-- Missing state adopts the current revision, so a dot never appears merely
-- because this notification system was added or the addon was freshly installed.
ns.HELP_CONTENT_VERSION = 2
BINDING_HEADER_FRAMEGAMBIT = "Frame Gambit"
BINDING_NAME_FRAMEGAMBIT_TOGGLE_CINEMATIC = "Toggle Frame Gambit Cinematic Mode"
BINDING_NAME_FRAMEGAMBIT_TOGGLE_CINEMATIC_CAMERA = "Toggle Cinematic Camera Offset"
local CINEMATIC_BINDING = "FRAMEGAMBIT_TOGGLE_CINEMATIC"
local CINEMATIC_CAMERA_BINDING = "FRAMEGAMBIT_TOGGLE_CINEMATIC_CAMERA"
local LEGACY_CINEMATIC_BINDING = "PRIORITYFADER_TOGGLE_CINEMATIC"
ns.MAX_REACTIONS_PER_TARGET = 32
ns.COLORS = {
-- BindCards runtime tokens. Keep the window shell distinct from the
-- inner panel so nested dialogs share the same surface layering.
window = { 0.035, 0.041, 0.052, 0.985 },
panel = { 0.055, 0.063, 0.078, 1 },
card = { 0.073, 0.082, 0.098, 1 },
cardAlt = { 0.060, 0.069, 0.084, 1 },
raised = { 0.095, 0.108, 0.129, 1 },
border = { 0.20, 0.235, 0.28, 0.72 },
faint = { 0.18, 0.21, 0.25, 0.25 },
blue = { 0.27, 0.58, 0.82, 1 },
blueSoft = { 0.18, 0.36, 0.52, 1 },
accent = { 0.27, 0.58, 0.82, 1 },
teal = { 0.18, 0.76, 0.70, 1 },
text = { 0.91, 0.93, 0.96, 1 },
muted = { 0.59, 0.64, 0.71, 1 },
red = { 0.88, 0.31, 0.34, 1 },
-- Amber is reserved for warnings and Cinematic's supporting notices;
-- destructive and invalid states use red.
amber = { 0.93, 0.66, 0.25, 1 },
cinematic = { 0.90, 0.52, 0.24, 1 },
}
local DEFAULTS = {
version = 13,
profile = "Default",
help = {
seenVersion = ns.HELP_CONTENT_VERSION,
},
tutorial = {
completed = false,
lastStep = 1,
},
cinematic = {
letterboxEnabled = false,
letterboxHeight = 0.04,
cameraFOV = 90,
cameraOffsetDefault = false,
},
profiles = {
Default = {
targets = {},
groups = {},
links = {},
visibilityLinks = {},
nextReactionID = 1,
nextGroupID = 1,
},
},
}
local DEFAULT_REACTIONS = {
{ id = "mouseover", condition = "mouseover", opacity = 1.00 },
{ id = "combat", condition = "combat", opacity = 1.00 },
}
local CONDITION_INFO = {
mouseover = { label = "Mouseover", category = "system", kind = "state", pickerOrder = 1 },
linked_parent_hover = { label = "Linked parent hover", category = "legacy", kind = "state", internal = true },
combat = { label = "In combat", category = "character", kind = "state", pickerOrder = 1 },
out_of_combat = { label = "Out of combat", category = "character", kind = "state", pickerOrder = 2 },
movement = { label = "Movement", category = "character", kind = "state", restricted = true, pickerOrder = 3 },
-- Retained solely for existing saved profiles. New rules use the single
-- Movement Boolean card: Yes = moving, No = stationary.
moving = { label = "Moving", category = "character", kind = "state", restricted = true, deprecated = true },
stationary = { label = "Stationary", category = "character", kind = "state", restricted = true, deprecated = true },
casting = { label = "Casting", category = "character", kind = "state", pickerOrder = 4 },
falling = { label = "Falling", category = "character", kind = "state", pickerOrder = 5 },
shift = { label = "Shift held", category = "system", kind = "state", pickerOrder = 3 },
control = { label = "Ctrl held", category = "system", kind = "state", pickerOrder = 4 },
alt = { label = "Alt held", category = "system", kind = "state", pickerOrder = 2 },
dead = { label = "Dead or ghost", category = "character", kind = "state", pickerOrder = 6 },
stealth = { label = "Stealthed / invisible", category = "character", kind = "state", pickerOrder = 7 },
form = { label = "Form", category = "character", kind = "state", pickerOrder = 8 },
spec = { label = "Specialization", category = "character", kind = "state", pickerOrder = 9 },
target_any = { label = "Has any target", category = "target", kind = "state" },
target_hostile = { label = "Has hostile target", category = "target", kind = "state" },
target_friendly = { label = "Has friendly target", category = "target", kind = "state" },
target_dead = { label = "Target is dead", category = "target", kind = "state" },
no_target = { label = "No target", category = "target", kind = "state" },
mounted = { label = "Mounted", category = "travel", kind = "state" },
flying = { label = "Flying", category = "travel", kind = "state" },
dragonriding = { label = "Dragonriding / skyriding", category = "travel", kind = "state", restricted = true },
swimming = { label = "Swimming", category = "travel", kind = "state" },
underwater = { label = "Underwater", category = "travel", kind = "state" },
vehicle = { label = "In vehicle", category = "travel", kind = "state" },
taxi = { label = "On flight path", category = "travel", kind = "state" },
pet_battle = { label = "Pet battle", category = "travel", kind = "state" },
fishing = { label = "Fishing", category = "travel", kind = "state" },
class_pet = { label = "Class pet active", category = "pets", kind = "state" },
cosmetic_pet = { label = "Cosmetic companion active", category = "pets", kind = "state" },
group = { label = "In party", category = "social", kind = "state" },
raid = { label = "In raid", category = "social", kind = "state" },
solo = { label = "Solo", category = "social", kind = "state" },
instance = { label = "In an instance", category = "social", kind = "state" },
open_world = { label = "In open world", category = "social", kind = "state" },
dungeon = { label = "In dungeon instance", category = "social", kind = "state" },
raid_instance = { label = "In raid instance", category = "social", kind = "state" },
battleground = { label = "In battleground", category = "social", kind = "state" },
arena = { label = "In arena", category = "social", kind = "state" },
scenario = { label = "In scenario", category = "social", kind = "state" },
delve = { label = "In Delve", category = "social", kind = "state" },
resting = { label = "Resting", category = "social", kind = "state" },
pvp_flagged = { label = "PvP flagged", category = "social", kind = "state", restricted = true },
war_mode = { label = "War Mode enabled", category = "social", kind = "state" },
indoors = { label = "Indoors", category = "social", kind = "state" },
outdoors = { label = "Outdoors", category = "social", kind = "state" },
quest_update = { label = "Quest updated", category = "moment", kind = "moment", duration = 3 },
quest_accepted = { label = "Quest accepted", category = "moment", kind = "moment", duration = 3 },
quest_turned_in = { label = "Quest turned in", category = "moment", kind = "moment", duration = 3 },
quest_objective = { label = "Quest objective updated", category = "moment", kind = "moment", duration = 3 },
loot = { label = "Looted an item", category = "moment", kind = "moment", duration = 4 },
loot_opened = { label = "Loot window opened", category = "moment", kind = "moment", duration = 5 },
cinematic_chat_input = { label = "Typing in chat", category = "legacy", kind = "state", internal = true },
-- Kept only to make existing early saved profiles migrate invisibly.
target = { label = "Has any target", category = "legacy", kind = "state", internal = true },
hostile_target = { label = "Has hostile target", category = "legacy", kind = "state", internal = true },
pvp = { label = "PvP flagged", category = "legacy", kind = "state", internal = true },
}
ns.CONDITION_INFO = CONDITION_INFO
-- AND requirements should describe a state that can actually happen. Keep
-- this deliberately conservative: only encode pairs Retail's own state APIs
-- guarantee are mutually exclusive, while preserving valid combinations such
-- as Flying + Dragonriding and Swimming + Underwater.
local EXCLUSIVE_REQUIREMENT_GROUPS = {
{ "combat", "out_of_combat" },
{ "target_hostile", "target_friendly" },
{ "group", "raid", "solo" },
{ "open_world", "dungeon", "raid_instance", "battleground", "arena", "scenario", "delve" },
{ "vehicle", "taxi", "pet_battle", "fishing" },
-- Specific quest moments can share a brief timestamp window, but an AND
-- of two different quest events is not a useful player-facing filter.
-- Use separate ordered reactions when they need different responses.
{ "quest_update", "quest_accepted", "quest_turned_in", "quest_objective" },
{ "indoors", "outdoors" },
}
local EXCLUSIVE_REQUIREMENT_PAIRS = {
{ "instance", "open_world" },
{ "no_target", "target_any" }, { "no_target", "target_hostile" },
{ "no_target", "target_friendly" }, { "no_target", "target_dead" },
{ "pet_battle", "mounted" }, { "pet_battle", "flying" },
{ "pet_battle", "dragonriding" }, { "pet_battle", "swimming" },
{ "pet_battle", "underwater" },
{ "fishing", "flying" }, { "fishing", "dragonriding" },
{ "fishing", "swimming" }, { "fishing", "underwater" },
{ "taxi", "swimming" }, { "taxi", "underwater" },
}
local REQUIREMENT_CONFLICTS = {}
local function MarkRequirementConflict(first, second)
REQUIREMENT_CONFLICTS[first] = REQUIREMENT_CONFLICTS[first] or {}
REQUIREMENT_CONFLICTS[second] = REQUIREMENT_CONFLICTS[second] or {}
REQUIREMENT_CONFLICTS[first][second], REQUIREMENT_CONFLICTS[second][first] = true, true
end
for _, group in ipairs(EXCLUSIVE_REQUIREMENT_GROUPS) do
for first = 1, #group - 1 do
for second = first + 1, #group do MarkRequirementConflict(group[first], group[second]) end
end
end
for _, pair in ipairs(EXCLUSIVE_REQUIREMENT_PAIRS) do MarkRequirementConflict(pair[1], pair[2]) end
function ns:GetRequirementConflict(reaction, candidate, skipExisting)
if type(reaction) ~= "table" or type(candidate) ~= "string" then return nil end
local function Conflicts(condition)
return condition ~= candidate and REQUIREMENT_CONFLICTS[candidate] and REQUIREMENT_CONFLICTS[candidate][condition]
end
if Conflicts(reaction.condition) then return reaction.condition end
for _, condition in ipairs(reaction.requirements or {}) do
if condition ~= skipExisting and Conflicts(condition) then return condition end
end
return nil
end
-- Form entries deliberately describe only gameplay forms that WoW exposes in
-- the player's shapeshift bar. A reaction is skipped when its form is not
-- available to the current class/spec, which makes one profile safe to share
-- across specs without a negative Form row accidentally matching everywhere.
local FORM_OPTIONS = {
{ id = "druid_bear", label = "Bear Form", class = "DRUID", spellID = 5487 },
{ id = "druid_cat", label = "Cat Form", class = "DRUID", spellID = 768 },
{ id = "druid_travel", label = "Travel Form", class = "DRUID", spellID = 783 },
{ id = "druid_moonkin", label = "Moonkin Form", class = "DRUID", spellID = 24858 },
{ id = "priest_shadow", label = "Shadowform", class = "PRIEST", spellID = 232698 },
{ id = "shaman_ghost_wolf", label = "Ghost Wolf", class = "SHAMAN", spellID = 2645 },
{ id = "demonhunter_havoc_meta", label = "Metamorphosis (Havoc)", class = "DEMONHUNTER", spellID = 191427 },
{ id = "demonhunter_vengeance_meta", label = "Metamorphosis (Vengeance)", class = "DEMONHUNTER", spellID = 187827 },
{ id = "demonhunter_devourer_void_meta", label = "Void Metamorphosis (Devourer)", class = "DEMONHUNTER", spellID = 1225789 },
}
local FORM_BY_ID = {}
for _, option in ipairs(FORM_OPTIONS) do FORM_BY_ID[option.id] = option end
ns.FORM_OPTIONS, ns.FORM_BY_ID = FORM_OPTIONS, FORM_BY_ID
local SPEC_OPTIONS = {
{ class = "WARRIOR", classLabel = "Warrior", id = 71, label = "Arms" }, { class = "WARRIOR", classLabel = "Warrior", id = 72, label = "Fury" }, { class = "WARRIOR", classLabel = "Warrior", id = 73, label = "Protection" },
{ class = "PALADIN", classLabel = "Paladin", id = 65, label = "Holy" }, { class = "PALADIN", classLabel = "Paladin", id = 66, label = "Protection" }, { class = "PALADIN", classLabel = "Paladin", id = 70, label = "Retribution" },
{ class = "HUNTER", classLabel = "Hunter", id = 253, label = "Beast Mastery" }, { class = "HUNTER", classLabel = "Hunter", id = 254, label = "Marksmanship" }, { class = "HUNTER", classLabel = "Hunter", id = 255, label = "Survival" },
{ class = "ROGUE", classLabel = "Rogue", id = 259, label = "Assassination" }, { class = "ROGUE", classLabel = "Rogue", id = 260, label = "Outlaw" }, { class = "ROGUE", classLabel = "Rogue", id = 261, label = "Subtlety" },
{ class = "PRIEST", classLabel = "Priest", id = 256, label = "Discipline" }, { class = "PRIEST", classLabel = "Priest", id = 257, label = "Holy" }, { class = "PRIEST", classLabel = "Priest", id = 258, label = "Shadow" },
{ class = "DEATHKNIGHT", classLabel = "Death Knight", id = 250, label = "Blood" }, { class = "DEATHKNIGHT", classLabel = "Death Knight", id = 251, label = "Frost" }, { class = "DEATHKNIGHT", classLabel = "Death Knight", id = 252, label = "Unholy" },
{ class = "SHAMAN", classLabel = "Shaman", id = 262, label = "Elemental" }, { class = "SHAMAN", classLabel = "Shaman", id = 263, label = "Enhancement" }, { class = "SHAMAN", classLabel = "Shaman", id = 264, label = "Restoration" },
{ class = "MAGE", classLabel = "Mage", id = 62, label = "Arcane" }, { class = "MAGE", classLabel = "Mage", id = 63, label = "Fire" }, { class = "MAGE", classLabel = "Mage", id = 64, label = "Frost" },
{ class = "WARLOCK", classLabel = "Warlock", id = 265, label = "Affliction" }, { class = "WARLOCK", classLabel = "Warlock", id = 266, label = "Demonology" }, { class = "WARLOCK", classLabel = "Warlock", id = 267, label = "Destruction" },
{ class = "MONK", classLabel = "Monk", id = 268, label = "Brewmaster" }, { class = "MONK", classLabel = "Monk", id = 269, label = "Windwalker" }, { class = "MONK", classLabel = "Monk", id = 270, label = "Mistweaver" },
{ class = "DRUID", classLabel = "Druid", id = 102, label = "Balance" }, { class = "DRUID", classLabel = "Druid", id = 103, label = "Feral" }, { class = "DRUID", classLabel = "Druid", id = 104, label = "Guardian" }, { class = "DRUID", classLabel = "Druid", id = 105, label = "Restoration" },
{ class = "DEMONHUNTER", classLabel = "Demon Hunter", id = 577, label = "Havoc" }, { class = "DEMONHUNTER", classLabel = "Demon Hunter", id = 581, label = "Vengeance" }, { class = "DEMONHUNTER", classLabel = "Demon Hunter", id = 1480, label = "Devourer" },
{ class = "EVOKER", classLabel = "Evoker", id = 1467, label = "Devastation" }, { class = "EVOKER", classLabel = "Evoker", id = 1468, label = "Preservation" }, { class = "EVOKER", classLabel = "Evoker", id = 1473, label = "Augmentation" },
}
local SPEC_BY_ID, SPECS_BY_CLASS, CLASS_OPTIONS = {}, {}, {}
for _, option in ipairs(SPEC_OPTIONS) do
SPEC_BY_ID[option.id] = option
SPECS_BY_CLASS[option.class] = SPECS_BY_CLASS[option.class] or {}
SPECS_BY_CLASS[option.class][#SPECS_BY_CLASS[option.class] + 1] = option
end
for classID, specs in pairs(SPECS_BY_CLASS) do CLASS_OPTIONS[#CLASS_OPTIONS + 1] = { id = classID, label = specs[1].classLabel } end
table.sort(CLASS_OPTIONS, function(a, b) return a.label < b.label end)
ns.SPEC_OPTIONS, ns.SPEC_BY_ID, ns.SPECS_BY_CLASS, ns.CLASS_OPTIONS = SPEC_OPTIONS, SPEC_BY_ID, SPECS_BY_CLASS, CLASS_OPTIONS
ns.CONDITION_CATEGORY_ORDER = {
{ id = "system", label = "System" },
{ id = "character", label = "Character" },
{ id = "target", label = "Target" },
{ id = "travel", label = "Travel" },
{ id = "pets", label = "Pets" },
{ id = "social", label = "Group & instances" },
{ id = "moment", label = "Events" },
}
local EVENT_TO_MOMENT = {
QUEST_ACCEPTED = { "quest_update", "quest_accepted" },
QUEST_TURNED_IN = { "quest_update", "quest_turned_in" },
QUEST_WATCH_UPDATE = { "quest_update", "quest_objective" },
QUEST_LOG_UPDATE = "quest_update",
LOOT_OPENED = "loot_opened",
}
local runtime = {
baseAlpha = setmetatable({}, { __mode = "k" }),
currentAlpha = setmetatable({}, { __mode = "k" }),
frameByID = {},
hovered = {},
active = {},
moments = {},
context = {},
neededConditions = {},
hoverNeeded = {},
fadeOutStarted = {},
revealGoal = {},
transitions = {},
pendingRestore = setmetatable({}, { __mode = "k" }),
pendingRelease = setmetatable({}, { __mode = "k" }),
normalized = setmetatable({}, { __mode = "k" }),
managedIDByFrame = setmetatable({}, { __mode = "k" }),
managedAlphaHooks = setmetatable({}, { __mode = "k" }),
managedAlphaAuditAt = setmetatable({}, { __mode = "k" }),
managedAlphaGuard = setmetatable({}, { __mode = "k" }),
immediateApply = {},
cinematicBlackout = setmetatable({}, { __mode = "k" }),
cinematicBlackoutHooks = setmetatable({}, { __mode = "k" }),
cinematicAlphaGuard = setmetatable({}, { __mode = "k" }),
cinematicExemptFrames = setmetatable({}, { __mode = "k" }),
cinematicOpenWindows = setmetatable({}, { __mode = "k" }),
cinematicPanelHooks = setmetatable({}, { __mode = "k" }),
cinematicRootScanAt = 0,
cinematicAuditAt = 0,
cinematicRevealActive = nil,
cinematicRescanToken = 0,
-- Relationship lookups are on the shared evaluator path. Keep a small
-- reverse index so linked/visibility ancestry does not rescan every
-- profile relationship for every target on every tick.
relationshipCache = {
valid = false,
profile = nil,
links = nil,
visibilityLinks = nil,
groups = nil,
linkParents = {},
visibilityParents = {},
hoverMembers = {},
},
lastTick = 0,
lastMouseTick = 0,
playerCasting = false,
}
ns.runtime = runtime
-- Declared here so the evaluator can deliberately detach its shared OnUpdate
-- below, while the actual event driver remains created near the event list.
-- Keeping one driver (rather than per-target frame handlers) is important for
-- both performance and safe late-provider wakeups.
local driver
local SafeFrameAlpha
local QueuePendingRestore
local function IsSecret(value)
return issecretvalue and issecretvalue(value) or false
end
local function SafeBoolean(func, ...)
if type(func) ~= "function" then return nil end
local ok, value = pcall(func, ...)
if not ok or IsSecret(value) then return nil end
return value and true or false
end
local function SafeValue(func, ...)
if type(func) ~= "function" then return nil end
local ok, value = pcall(func, ...)
if not ok or value == nil or IsSecret(value) then return nil end
return value
end
local function CallFrameIsShown(frame)
return frame:IsShown()
end
local function CallFrameGetRect(frame)
return frame:GetRect()
end
local function CallFrameGetAlpha(frame)
return frame:GetAlpha()
end
local function CallFrameSetAlpha(frame, alpha)
frame:SetAlpha(alpha)
end
local function SafeSetFrameAlpha(frame, alpha)
return pcall(CallFrameSetAlpha, frame, alpha)
end
local function ClearTable(values)
for key in pairs(values) do values[key] = nil end
return values
end
local function IsLocalLootMessage(senderName, senderGUID)
if senderGUID ~= nil then
if IsSecret(senderGUID) then return false end
local playerGUID = SafeValue(UnitGUID, "player")
return playerGUID ~= nil and senderGUID == playerGUID
end
-- Older/nonstandard payloads may omit GUID. Use only an exact, guarded
-- local-name fallback; realm-normalization or chat-message parsing would
-- risk claiming another player's loot.
if senderName == nil or IsSecret(senderName) then return false end
local playerName = SafeValue(UnitName, "player")
return playerName ~= nil and senderName == playerName
end
local function CopyDefaults(defaults, value)
if type(defaults) ~= "table" then return value == nil and defaults or value end
local result = type(value) == "table" and value or {}
for k, v in pairs(defaults) do
if result[k] == nil then
result[k] = CopyDefaults(v)
elseif type(v) == "table" then
result[k] = CopyDefaults(v, result[k])
end
end
return result
end
local function DeepCopy(value, seen)
if type(value) ~= "table" then return value end
seen = seen or {}
if seen[value] then return seen[value] end
local copy = {}
seen[value] = copy
for key, child in pairs(value) do copy[DeepCopy(key, seen)] = DeepCopy(child, seen) end
return copy
end
function ns:Profile()
local db = FrameGambitDB
db.profiles = db.profiles or {}
db.profile = db.profile or "Default"
db.profiles[db.profile] = db.profiles[db.profile] or { targets = {}, groups = {}, links = {}, visibilityLinks = {}, nextReactionID = 1, nextGroupID = 1 }
local profile = db.profiles[db.profile]
profile.targets = type(profile.targets) == "table" and profile.targets or {}
profile.groups = type(profile.groups) == "table" and profile.groups or {}
profile.links = type(profile.links) == "table" and profile.links or {}
profile.visibilityLinks = type(profile.visibilityLinks) == "table" and profile.visibilityLinks or {}
profile.nextReactionID = tonumber(profile.nextReactionID) or 1
profile.nextGroupID = tonumber(profile.nextGroupID) or 1
return profile
end
local function InvalidateRelationshipCache()
runtime.relationshipCache.valid = false
end
local function GetRelationshipIndices(owner)
local profile = owner:Profile()
local links = profile.links
local visibilityLinks = profile.visibilityLinks
local groups = profile.groups
local cache = runtime.relationshipCache
if cache.valid and cache.profile == profile and cache.links == links
and cache.visibilityLinks == visibilityLinks and cache.groups == groups then
return cache
end
local linkParents, visibilityParents, hoverMembers = {}, {}, {}
for parentID, children in pairs(links or {}) do
if type(children) == "table" then
for childID, enabled in pairs(children) do
-- Link relationships historically treated any truthy value as
-- enabled, so retain that compatibility while indexing.
if enabled then
local parents = linkParents[childID]
if not parents then parents = {}; linkParents[childID] = parents end
parents[#parents + 1] = parentID
end
end
end
end
for parentID, children in pairs(visibilityLinks or {}) do
if type(children) == "table" then
for childID, enabled in pairs(children) do
if enabled == true and visibilityParents[childID] == nil then
visibilityParents[childID] = parentID
end
end
end
end
for _, group in pairs(groups or {}) do
if type(group) == "table" and type(group.members) == "table" then
local members = {}
for memberID, enabled in pairs(group.members) do
if enabled then members[#members + 1] = memberID end
end
for _, memberID in ipairs(members) do
local peers = hoverMembers[memberID]
if not peers then peers = {}; hoverMembers[memberID] = peers end
for _, peerID in ipairs(members) do peers[#peers + 1] = peerID end
end
end
end
cache.profile = profile
cache.links = links
cache.visibilityLinks = visibilityLinks
cache.groups = groups
cache.linkParents = linkParents
cache.visibilityParents = visibilityParents
cache.hoverMembers = hoverMembers
cache.valid = true
return cache
end
function ns:NextReactionID()
local profile = self:Profile()
local id = profile.nextReactionID
profile.nextReactionID = id + 1
return id
end
function ns:NextGroupID()
local profile = self:Profile()
local id = profile.nextGroupID
profile.nextGroupID = id + 1
return id
end
function ns:MigrateDatabase()
local db = FrameGambitDB
local oldVersion = tonumber(db.version) or 1
local aliases = { target = "target_any", hostile_target = "target_hostile", pvp = "pvp_flagged" }
local function RemoveProfileTargets(profile, matches)
if type(profile) ~= "table" then return end
profile.targets = type(profile.targets) == "table" and profile.targets or {}
profile.groups = type(profile.groups) == "table" and profile.groups or {}
profile.links = type(profile.links) == "table" and profile.links or {}
profile.visibilityLinks = type(profile.visibilityLinks) == "table" and profile.visibilityLinks or {}
local retired = {}
for id in pairs(profile.targets) do if matches(id) then retired[#retired + 1] = id end end
for _, id in ipairs(retired) do
profile.targets[id] = nil
for groupID, group in pairs(profile.groups) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[groupID] = nil
else
group.members[id] = nil
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled == true and type(memberID) == "string" then count = count + 1 else group.members[memberID] = nil end
end
if count < 2 then profile.groups[groupID] = nil end
end
end
profile.links[id] = nil
for parentID, children in pairs(profile.links) do
if type(children) ~= "table" then
profile.links[parentID] = nil
else
children[id] = nil
if not next(children) then profile.links[parentID] = nil end
end
end
profile.visibilityLinks[id] = nil
for parentID, children in pairs(profile.visibilityLinks) do
if type(children) ~= "table" then
profile.visibilityLinks[parentID] = nil
else
children[id] = nil
if not next(children) then profile.visibilityLinks[parentID] = nil end
end
end
end
end
local function RemapProfileTarget(profile, fromID, toID)
if type(profile) ~= "table" or type(fromID) ~= "string" or fromID == toID then return end
profile.targets = type(profile.targets) == "table" and profile.targets or {}
profile.groups = type(profile.groups) == "table" and profile.groups or {}
profile.links = type(profile.links) == "table" and profile.links or {}
profile.visibilityLinks = type(profile.visibilityLinks) == "table" and profile.visibilityLinks or {}
if profile.targets[fromID] ~= nil then
if profile.targets[toID] == nil then profile.targets[toID] = profile.targets[fromID] end
profile.targets[fromID] = nil
end
for groupID, group in pairs(profile.groups) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[groupID] = nil
else
if group.members[fromID] == true then
group.members[fromID] = nil
group.members[toID] = true
end
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled == true and type(memberID) == "string" then count = count + 1 else group.members[memberID] = nil end
end
if count < 2 then profile.groups[groupID] = nil end
end
end
local oldChildren = profile.links[fromID]
if type(oldChildren) == "table" then
local children = type(profile.links[toID]) == "table" and profile.links[toID] or {}
for childID, enabled in pairs(oldChildren) do
local mapped = childID == fromID and toID or childID
if enabled == true and mapped ~= toID then children[mapped] = true end
end
profile.links[toID] = next(children) and children or nil
end
profile.links[fromID] = nil
for parentID, children in pairs(profile.links) do
if type(children) ~= "table" then
profile.links[parentID] = nil
else
if children[fromID] == true then
children[fromID] = nil
if parentID ~= toID then children[toID] = true end
end
children[parentID] = nil
if not next(children) then profile.links[parentID] = nil end
end
end
local inherited = profile.visibilityLinks[fromID]
if type(inherited) == "table" then
local children = type(profile.visibilityLinks[toID]) == "table" and profile.visibilityLinks[toID] or {}
for childID, enabled in pairs(inherited) do
local mapped = childID == fromID and toID or childID
if enabled == true and mapped ~= toID then children[mapped] = true end
end
profile.visibilityLinks[toID] = next(children) and children or nil
end
profile.visibilityLinks[fromID] = nil
for parentID, children in pairs(profile.visibilityLinks) do
if type(children) ~= "table" then
profile.visibilityLinks[parentID] = nil
else
if children[fromID] == true then
children[fromID] = nil
if parentID ~= toID then children[toID] = true end
end
children[parentID] = nil
if not next(children) then profile.visibilityLinks[parentID] = nil end
end
end
end
if oldVersion < 2 then
for _, profile in pairs(db.profiles or {}) do
profile.targets = type(profile.targets) == "table" and profile.targets or {}
profile.groups = type(profile.groups) == "table" and profile.groups or {}
profile.links = type(profile.links) == "table" and profile.links or {}
local nextID, nextGroupID = tonumber(profile.nextReactionID) or 1, tonumber(profile.nextGroupID) or 1
for _, settings in pairs(profile.targets) do
local reactions = type(settings) == "table" and settings.reactions or {}
if type(settings) == "table" then settings.reactions = reactions end
for _, reaction in ipairs(reactions) do
if type(reaction) == "table" then
reaction.condition = aliases[reaction.condition] or reaction.condition
if type(reaction.id) ~= "number" then reaction.id, nextID = nextID, nextID + 1 end
end
end
end
for key, group in pairs(profile.groups) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[key] = nil
else
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled ~= true or type(memberID) ~= "string" then group.members[memberID] = nil else count = count + 1 end
end
if count < 2 then profile.groups[key] = nil end
end
nextGroupID = nextGroupID + 1
end
profile.nextReactionID, profile.nextGroupID = nextID, nextGroupID
end
end
if oldVersion < 3 then
for _, profile in pairs(db.profiles or {}) do
for _, settings in pairs(type(profile.targets) == "table" and profile.targets or {}) do
for _, reaction in ipairs(type(settings) == "table" and settings.reactions or {}) do
if type(reaction) == "table" then
local requirements, seen = {}, {}
for _, condition in ipairs(type(reaction.requirements) == "table" and reaction.requirements or {}) do
if type(condition) == "string" and condition ~= reaction.condition and not seen[condition] then
requirements[#requirements + 1], seen[condition] = condition, true
end
end
reaction.requirements = requirements
end
end
end
end
end
if oldVersion < 4 then
-- This v1.1 target was an invisible 1x1 Resource Bars anchor. It
-- cannot provide a useful hover region and would compound child alpha.
for _, profile in pairs(db.profiles or {}) do
if type(profile.targets) == "table" then profile.targets.eui_resources = nil end
if type(profile.groups) == "table" then
for key, group in pairs(profile.groups) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[key] = nil
else
group.members.eui_resources = nil
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled == true and type(memberID) == "string" then count = count + 1 else group.members[memberID] = nil end
end
if count < 2 then profile.groups[key] = nil end
end
end
end
if type(profile.links) == "table" then
profile.links.eui_resources = nil
for parentID, children in pairs(profile.links) do
if type(children) ~= "table" then
profile.links[parentID] = nil
else
children.eui_resources = nil
if not next(children) then profile.links[parentID] = nil end
end
end
end
end
end
if oldVersion < 5 then
db.cinematic = type(db.cinematic) == "table" and db.cinematic or {}
end
if oldVersion < 6 then
-- v1.9 temporarily exposed CDM adapters through a source-level EUI
-- bridge. Remove every saved reference now that CDM is deliberately
-- outside Priority Fader's standalone compatibility contract.
for _, profile in pairs(db.profiles or {}) do
local retired = {}
for id in pairs(type(profile.targets) == "table" and profile.targets or {}) do
if type(id) == "string" and id:match("^eui_cdm_") then retired[#retired + 1] = id end
end
for _, id in ipairs(retired) do
profile.targets[id] = nil
for groupID, group in pairs(type(profile.groups) == "table" and profile.groups or {}) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[groupID] = nil
else
group.members[id] = nil
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled == true and type(memberID) == "string" then count = count + 1 else group.members[memberID] = nil end
end
if count < 2 then profile.groups[groupID] = nil end
end
end
if type(profile.links) == "table" then
profile.links[id] = nil
for parentID, children in pairs(profile.links) do
if type(children) ~= "table" then
profile.links[parentID] = nil
else
children[id] = nil
if not next(children) then profile.links[parentID] = nil end
end
end
end
end
end
end
if oldVersion < 7 then
db.customTargets = type(db.customTargets) == "table" and db.customTargets or {}
-- Anonymous frame-picker targets intentionally last only for the UI
-- session in which they were chosen. They cannot resolve safely after
-- reload, so discard their saved rule graph instead of leaving ghosts.
for _, profile in pairs(db.profiles or {}) do
local retired = {}
for id in pairs(type(profile.targets) == "table" and profile.targets or {}) do
if type(id) == "string" and id:match("^session_frame_") then retired[#retired + 1] = id end
end
for _, id in ipairs(retired) do
profile.targets[id] = nil
for groupID, group in pairs(type(profile.groups) == "table" and profile.groups or {}) do
if type(group) ~= "table" or type(group.members) ~= "table" then
profile.groups[groupID] = nil
else
group.members[id] = nil
local count = 0
for memberID, enabled in pairs(group.members) do
if enabled == true and type(memberID) == "string" then count = count + 1 else group.members[memberID] = nil end
end
if count < 2 then profile.groups[groupID] = nil end
end
end
if type(profile.links) == "table" then
profile.links[id] = nil
for parentID, children in pairs(profile.links) do
if type(children) ~= "table" then profile.links[parentID] = nil
else children[id] = nil; if not next(children) then profile.links[parentID] = nil end end
end
end
end
end
end
if oldVersion < 8 then
-- Cinematic Mode is a screen-clearing scene. Chat, objectives, and the
-- action bar now fall under its blackout layer rather than being quiet
-- default scene components. Keep only the requested essentials.
for _, profile in pairs(db.profiles or {}) do
if type(profile) == "table" and profile.cinematicSystem then
RemoveProfileTargets(profile, function(id)
return id == "chat" or id == "objectives" or id == "eui_main"
end)
end
end
end
if oldVersion < 9 then
-- Replace the experimental per-EUI-bar bridge with stable Blizzard
-- viewer targets. Existing picker-created viewer rules retain their
-- settings and graph relationships under the canonical target IDs.
local viewerTargets = {
EssentialCooldownViewer = "cdm_cooldowns",
UtilityCooldownViewer = "cdm_utility",
BuffIconCooldownViewer = "cdm_buffs",
}
db.customTargets = type(db.customTargets) == "table" and db.customTargets or {}
local remapped = {}
for customID, definition in pairs(db.customTargets) do
local toID = type(definition) == "table" and viewerTargets[definition.name] or nil
if type(customID) == "string" and toID then remapped[#remapped + 1] = { customID, toID } end
end
for _, mapping in ipairs(remapped) do
for _, profile in pairs(db.profiles or {}) do RemapProfileTarget(profile, mapping[1], mapping[2]) end
db.customTargets[mapping[1]] = nil
end
for _, profile in pairs(db.profiles or {}) do
RemoveProfileTargets(profile, function(id)
return type(id) == "string" and id:match("^experimental_cdm_") ~= nil
end)
end
end
if oldVersion < 10 then
for _, profile in pairs(db.profiles or {}) do
if type(profile) == "table" then profile.visibilityLinks = {} end
end
end
if oldVersion < 11 then
db.tutorial = type(db.tutorial) == "table" and db.tutorial or {}
end
if oldVersion < 13 then
-- Promote stable Blizzard HUD frames that players previously had to
-- discover by hand. Preserve their rules and graph relationships under
-- one portable semantic id, then remove the redundant picker entry.
local builtinTargets = {}
for _, target in ipairs(ns.Targets or {}) do
if type(target) == "table" and type(target.id) == "string" then
for _, name in ipairs(type(target.names) == "table" and target.names or {}) do
if type(name) == "string" then builtinTargets[name] = target.id end
end
end
end
db.customTargets = type(db.customTargets) == "table" and db.customTargets or {}
local remapped = {}
for customID, definition in pairs(db.customTargets) do
local toID = type(definition) == "table" and builtinTargets[definition.name] or nil
if type(customID) == "string" and toID then remapped[#remapped + 1] = { customID, toID } end
end
table.sort(remapped, function(a, b) return a[1] < b[1] end)
for _, mapping in ipairs(remapped) do
for _, profile in pairs(db.profiles or {}) do RemapProfileTarget(profile, mapping[1], mapping[2]) end
db.customTargets[mapping[1]] = nil
end
end
-- Session-only roots cannot survive a reload. Do this every login rather
-- than only during the schema migration that introduced them.
for _, profile in pairs(db.profiles or {}) do
RemoveProfileTargets(profile, function(id) return type(id) == "string" and id:match("^session_frame_") end)
end
db.version = 13
-- Migrations normalize relationship tables in place. Their identities do
-- not change, so invalidate the evaluator index explicitly in case a
-- startup hook inspected it before migration completed.
InvalidateRelationshipCache()
end
function ns:GetTutorialState()
FrameGambitDB = type(FrameGambitDB) == "table" and FrameGambitDB or {}
FrameGambitDB.tutorial = type(FrameGambitDB.tutorial) == "table" and FrameGambitDB.tutorial or {}
local state = FrameGambitDB.tutorial
state.completed = state.completed == true
state.lastStep = math.max(1, math.min(9, math.floor(tonumber(state.lastStep) or 1)))
return state.lastStep, state.completed
end
function ns:SetTutorialState(step, completed)
self:GetTutorialState()
local state = FrameGambitDB.tutorial
if step ~= nil then state.lastStep = math.max(1, math.min(9, math.floor(tonumber(step) or 1))) end
if completed ~= nil then state.completed = completed == true end
return state
end
function ns:HasUnreadHelp()
FrameGambitDB = type(FrameGambitDB) == "table" and FrameGambitDB or {}
FrameGambitDB.help = type(FrameGambitDB.help) == "table" and FrameGambitDB.help or {}
local current = math.max(1, math.floor(tonumber(self.HELP_CONTENT_VERSION) or 1))
local seen = tonumber(FrameGambitDB.help.seenVersion)
if not seen then
seen = current
FrameGambitDB.help.seenVersion = seen
else
seen = math.max(0, math.floor(seen))
end
return seen < current
end
function ns:MarkHelpRead()
self:HasUnreadHelp()
local current = math.max(1, math.floor(tonumber(self.HELP_CONTENT_VERSION) or 1))
local seen = math.max(0, math.floor(tonumber(FrameGambitDB.help.seenVersion) or 0))
FrameGambitDB.help.seenVersion = math.max(seen, current)
local dot = self.Options and self.Options.helpButton and self.Options.helpButton.helpDot
if dot then dot:Hide() end
end
local CINEMATIC_PROFILE_LABEL = "Cinematic Mode"
local CINEMATIC_TEMPLATE_VERSION = 8
local CINEMATIC_COMPONENTS = {
{ id = "eui_player", label = "Player frame", default = "context_hover", rest = 0 },
{ id = "eui_target", label = "Target frame", default = "context_hover", rest = 0 },
{ id = "eui_castbar", label = "Cast bar", default = "casting", rest = 0 },
{ id = "eui_resourcebars", label = "Resource bars", default = "combat", rest = 0 },
{ id = "minimap", label = "Minimap", default = "hover", rest = 0 },
{ id = "chat", label = "Chat", default = "typing_only", rest = 0 },
}
ns.CINEMATIC_COMPONENTS = CINEMATIC_COMPONENTS
local CINEMATIC_MODE_CONDITIONS = {
typing_only = {},
context_hover = { "alt", "mouseover", "combat", "target_any" },
target_hover = { "alt", "mouseover", "target_any" },
combat_hover = { "alt", "mouseover", "combat" },
combat = { "alt", "combat" },
casting = { "alt", "casting" },
hover = { "alt", "mouseover" },
quest_hover = { "alt", "mouseover", "quest_update", "quest_accepted", "quest_turned_in", "quest_objective" },
loot_hover = { "alt", "mouseover", "loot", "loot_opened" },
}
ns.CINEMATIC_MODE_LABELS = {
untouched = "Untouched",
typing_only = "While typing",
context_hover = "Combat, target + hover",
target_hover = "Target + hover",
combat_hover = "Combat + hover",
combat = "Combat only",
casting = "Casting only",
hover = "Hover only",
quest_hover = "Quest + hover",
loot_hover = "Loot + hover",
custom = "Custom rules",
}
local function NewCinematicSettings(profile, mode, rest, nativeMarkerMode)
local conditions = CINEMATIC_MODE_CONDITIONS[mode]
if not conditions then return nil end
local settings = { enabled = true, atRest = rest or 0.05, fadeDuration = 0.25, fadeDelay = 0.35, reactions = {}, cinematicMode = mode }
if nativeMarkerMode ~= nil then settings.nativeMarkerMode = nativeMarkerMode end
for _, condition in ipairs(conditions) do
settings.reactions[#settings.reactions + 1] = { id = profile.nextReactionID, condition = condition, opacity = 1 }
profile.nextReactionID = profile.nextReactionID + 1
end
return settings
end
local function CinematicSettingsMatch(settings, mode, rest)
local conditions = CINEMATIC_MODE_CONDITIONS[mode]
if type(settings) ~= "table" or not conditions
or math.abs((settings.atRest or 0) - rest) > 0.0001
or math.abs((settings.fadeDuration or 0) - 0.25) > 0.0001
or math.abs((settings.fadeDelay or 0) - 0.35) > 0.0001
or type(settings.reactions) ~= "table" or #settings.reactions ~= #conditions then return false end
for index, condition in ipairs(conditions) do
local reaction = settings.reactions[index]
if not reaction or reaction.condition ~= condition or reaction.opacity ~= 1 or #(reaction.requirements or {}) > 0 then return false end
local info = CONDITION_INFO[condition]
if info and info.kind == "moment" and math.abs((reaction.duration or info.duration or 3) - (info.duration or 3)) > 0.0001 then return false end
end
return true
end
function ns:GetCinematicProfileName()
local cinematic = FrameGambitDB and FrameGambitDB.cinematic
return cinematic and cinematic.profileName
end
function ns:IsCinematicProfileName(name)
return type(name) == "string" and name == self:GetCinematicProfileName()
end
function ns:EnsureCinematicProfile()
local db = FrameGambitDB
db.cinematic = type(db.cinematic) == "table" and db.cinematic or {}
local cinematic = db.cinematic
cinematic.actions = type(cinematic.actions) == "table" and cinematic.actions or {}
cinematic.letterboxEnabled = cinematic.letterboxEnabled == true
cinematic.letterboxHeight = math.max(0, math.min(0.25, tonumber(cinematic.letterboxHeight) or 0.04))
cinematic.cameraFOV = math.max(40, math.min(100, math.floor((tonumber(cinematic.cameraFOV) or 90) + 0.5)))
cinematic.cameraOffsetDefault = cinematic.cameraOffsetDefault == true
cinematic.templateVersion = tonumber(cinematic.templateVersion) or 1
-- The old Keep a frame shortcut duplicated profile editing and made a
-- saved exception invisible to the player. Retire those legacy exceptions
-- once; any intended visible UI now belongs in the Cinematic profile.
if cinematic.templateVersion < 6 then cinematic.keepNames = {} end
local profileName = cinematic.profileName
local created = false
if type(profileName) ~= "string" or not db.profiles[profileName] or not db.profiles[profileName].cinematicSystem then
profileName = CINEMATIC_PROFILE_LABEL
local suffix = 2
while db.profiles[profileName] and not db.profiles[profileName].cinematicSystem do
profileName = CINEMATIC_PROFILE_LABEL .. " " .. suffix
suffix = suffix + 1
end
cinematic.profileName = profileName
db.profiles[profileName] = db.profiles[profileName] or { targets = {}, groups = {}, links = {}, visibilityLinks = {}, nextReactionID = 1, nextGroupID = 1 }
local profile = db.profiles[profileName]
created = true
profile.cinematicSystem = true
profile.targets, profile.groups, profile.links, profile.visibilityLinks = {}, {}, {}, {}
profile.nextReactionID, profile.nextGroupID = 1, 1
for _, component in ipairs(CINEMATIC_COMPONENTS) do
profile.targets[component.id] = NewCinematicSettings(profile, component.default, component.rest,
component.id == "minimap" and "hide_zero" or nil)
end
end
local profile = db.profiles[cinematic.profileName]
if type(profile) == "table" then
profile.nextReactionID = tonumber(profile.nextReactionID) or 1
profile.nextGroupID = tonumber(profile.nextGroupID) or 1
end
if not created and cinematic.templateVersion < CINEMATIC_TEMPLATE_VERSION and type(profile) == "table" then
profile.targets = type(profile.targets) == "table" and profile.targets or {}
-- Upgrade only untouched v1 defaults. Advanced edits remain exactly as
-- the user made them and simply show as Custom in the dedicated page.
if CinematicSettingsMatch(profile.targets.eui_player, "combat_hover", 0.05) then
profile.targets.eui_player = NewCinematicSettings(profile, "target_hover", 0)
end