forked from jsaucier/Perspective
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathPerspective.lua
2375 lines (1967 loc) · 86.2 KB
/
Perspective.lua
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
require "Window"
require "GameLib"
require "Apollo"
require "Quest"
require "QuestLib"
local os, type, pairs = os, type, pairs
local table = table
local math = math
local string = string
local MAX_QUEUE_SIZE = 10
local GeminiAddon = Apollo.GetPackage("Gemini:Addon-1.1").tPackage
local Perspective = GeminiAddon:NewAddon("Perspective", false, {})
local lfrp = nil
local Options
local L = {}
local activationStates = {
{ state = "Public Event", category = "eventInteractable" },
{ state = "QuestReward", category = "questReward" },
{ state = "QuestNewMain", category = "questNewMain" },
{ state = "QuestNew", category = "questNew" },
{ state = "QuestNewRepeatable", category = "questNewRepeatable" },
{ state = "QuestNewDaily", category = "questNewRepeatable" },
{ state = "QuestNewWeekly", category = "questNewRepeatable" },
{ state = "QuestNewTradeskill", category = "questNewTradeskill" },
--{ state = "QuestTarget", category = "questInteractable" },
{ state = "TalkTo", category = "questTalkTo" },
{ state = "Datacube", category = "lore" },
{ state = "ExplorerInterest", category = "explorer" },
{ state = "ExplorerActivate", category = "explorer" },
{ state = "ExplorerDoor", category = "explorer" },
{ state = "SettlerActivate", category = "settler" },
{ state = "SettlerMinfrastructure", category = "settler" },
{ state = "SoldierActivate", category = "soldier" },
{ state = "SoldierKill", category = "soldier" },
{ state = "ScientistScannable", category = "scientist" },
{ state = "ScientistActivate", category = "scientist" },
{ state = "FlightPath", category = "flightPath" },
{ state = "InstancePortal", category = "instancePortal" },
{ state = "BindPoint", category = "bindPoint" },
{ state = "CommodityMarketplace", category = "marketplace" },
{ state = "ItemAuctionhouse", category = "auctionHouse" },
{ state = "Mail", category = "mailBox" },
{ state = "TradeskillTrainer", category = "tradeskillTrainer" },
{ state = "Vendor", category = "vendor" },
{ state = "CraftingStation", category = "craftingStation" },
{ state = "EngravingStation", category = "engravingStation" },
{ state = "GuildRegistrar", category = "guildRegistrar"},
{ state = "CityDirections", category = "cityDirections"},
{ state = "Dye", category = "dye" },
{ state = "Bank", category = "bank" },
{ state = "GuildBank", category = "guildBank" },
{ state = "Dungeon", category = "dungeon" },
{ state = "Collect", category = "interactable" },
{ state = "Interact", category = "interactable" },
}
-- Used to fix units that do not show up as challenges
local challengeUnits = {}
-- Lookup tables to save ourselves a lot of work and fake an oval dead zone around character
local DeadzoneAnglesLookup = {
{ Deg = -90, Rad = -1.5707963267949, NextRad = -1.48352986419518, Length = 250, WideLength = 250, DeltaRad = 0.0872664625997164, DeltaLength = -5, DeltaWideLength = -3 },
{ Deg = -85, Rad = -1.48352986419518, NextRad = -1.39626340159546, Length = 245, WideLength = 247, DeltaRad = 0.0872664625997166, DeltaLength = -13, DeltaWideLength = -2 },
{ Deg = -80, Rad = -1.39626340159546, NextRad = -1.30899693899575, Length = 232, WideLength = 245, DeltaRad = 0.0872664625997164, DeltaLength = -17, DeltaWideLength = -6 },
{ Deg = -75, Rad = -1.30899693899575, NextRad = -1.13446401379631, Length = 215, WideLength = 239, DeltaRad = 0.174532925199433, DeltaLength = -45, DeltaWideLength = -17 },
{ Deg = -65, Rad = -1.13446401379631, NextRad = -0.959931088596881, Length = 170, WideLength = 222, DeltaRad = 0.174532925199433, DeltaLength = -35, DeltaWideLength = -22 },
{ Deg = -55, Rad = -0.959931088596881, NextRad = -0.785398163397448, Length = 135, WideLength = 200, DeltaRad = 0.174532925199433, DeltaLength = -30, DeltaWideLength = -26 },
{ Deg = -45, Rad = -0.785398163397448, NextRad = -0.523598775598299, Length = 105, WideLength = 174, DeltaRad = 0.261799387799149, DeltaLength = -30, DeltaWideLength = -39 },
{ Deg = -30, Rad = -0.523598775598299, NextRad = 0, Length = 75, WideLength = 135, DeltaRad = 0.523598775598299, DeltaLength = -30, DeltaWideLength = -62 },
{ Deg = 0, Rad = 0, NextRad = 0.785398163397448, Length = 45, WideLength = 73, DeltaRad = 0.785398163397448, DeltaLength = -10, DeltaWideLength = -40 },
{ Deg = 45, Rad = 0.785398163397448, NextRad = 1.5707963267949, Length = 35, WideLength = 33, DeltaRad = 0.785398163397448, DeltaLength = 0, DeltaWideLength = -6 },
{ Deg = 90, Rad = 1.5707963267949, NextRad = 2.35619449019234, Length = 35, WideLength = 27, DeltaRad = 0.785398163397448, DeltaLength = 0, DeltaWideLength = 0 }
}
local DeadzoneRaceLookup = {
[1] = { Race = "Exile Human", Scale = 1.1, Wide = 0 },
[3] = { Race = "Granok", Scale = 1.0, Wide = 1 },
[4] = { Race = "Aurin", Scale = 0.9, Wide = 0 },
[13] = { Race = "Chua", Scale = 0.70, Wide = 0 },
[16] = { Race = "Mordesh", Scale = 1.05, Wide = 0 }
}
local unitTypes = {}
local tick = 0
local elapsed = 0
function Perspective:OnInitialize()
-- Load our localization
L = GeminiAddon:GetAddon("PerspectiveLocale"):LoadLocalization()
Options = GeminiAddon:GetAddon("PerspectiveOptions")
Apollo.LoadSprites("PerspectiveSprites.xml")
local xmlDoc = XmlDoc.CreateFromFile("Perspective.xml")
self.Overlay = Apollo.LoadForm(xmlDoc, "Overlay", "InWorldHudStratum", self)
self.Overlay:Show(true, true)
-- Table of all units we know about.
self.units = {
all = {},
prioritized = {},
categorized = {},
queue = {} }
-- Table of the sorted units, used to sort and draw by distance
self.sorted = {
prioritized = {},
categorized = {} }
-- Table of the current active challenges
self.challenges = {}
-- Table of our update timers
self.timers = {
draw = { elapsed = 0, divisor = 1000, func = "OnTimerDraw" },
fast = { elapsed = 0, divisor = 1000, func = "OnTimerFast" },
slow = { elapsed = 0, divisor = 1000, func = "OnTimerSlow" },
queue = { elapsed = 0, divisor = 1000, func = "OnTimerQueue", time = 10 } }
for index, state in pairs(activationStates) do
unitTypes[state.state] = true
end
-- Path marker windows
self.markers = {}
self.markersInitialized = false
self.inRaid = false
self.arAccountFriends = {}
self:PrepareAccountFriends()
-- Register our addon events
Apollo.RegisterEventHandler("ResolutionChanged", "OnResolutionChanged", self)
Apollo.RegisterEventHandler("UnitCreated", "OnUnitCreated", self)
Apollo.RegisterEventHandler("UnitDestroyed", "OnUnitDestroyed", self)
Apollo.RegisterEventHandler("ChangeWorld", "OnWorldChanged", self)
Apollo.RegisterEventHandler("QuestInit", "OnQuestInit", self)
Apollo.RegisterEventHandler("QuestObjectiveUpdated", "OnQuestObjectiveUpdated", self)
Apollo.RegisterEventHandler("QuestStateChanged", "OnQuestStateChanged", self)
Apollo.RegisterEventHandler("QuestTrackedChanged", "OnQuestTrackedChanged", self)
Apollo.RegisterEventHandler("ChallengeActivate", "OnChallengeActivated", self)
Apollo.RegisterEventHandler("ChallengeAbandon", "OnChallengeRemoved", self)
Apollo.RegisterEventHandler("ChallengeCompleted", "OnChallengeRemoved", self)
Apollo.RegisterEventHandler("ChallengeFailArea", "OnChallengeRemoved", self)
Apollo.RegisterEventHandler("ChallengeFailTime", "OnChallengeRemoved", self)
Apollo.RegisterEventHandler("ChallengeFailGeneric", "OnChallengeRemoved", self)
Apollo.RegisterEventHandler("PlayerPathMissionActivate", "OnPlayerPathMissionActivate", self)
Apollo.RegisterEventHandler("PlayerPathMissionAdvanced", "OnPlayerPathMissionAdvanced", self)
Apollo.RegisterEventHandler("PlayerPathMissionComplete", "OnPlayerPathMissionComplete", self)
Apollo.RegisterEventHandler("PlayerPathMissionDeactivate", "OnPlayerPathMissionDeactivate", self)
Apollo.RegisterEventHandler("PlayerPathMissionUnlocked", "OnPlayerPathMissionUnlocked", self)
Apollo.RegisterEventHandler("PlayerPathMissionUpdate", "OnPlayerPathMissionUpdate", self)
Apollo.RegisterEventHandler("TargetUnitChanged", "OnTargetUnitChanged", self)
Apollo.RegisterEventHandler("AlternateTargetUnitChanged", "OnAlternateTargetUnitChanged", self)
Apollo.RegisterEventHandler("PublicEventStart", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventObjectiveUpdate", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventLocationAdded", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventLocationRemoved", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventObjectiveLocationAdded", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventObjectiveLocationRemoved", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventCleared", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventEnd", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("PublicEventLeave", "OnPublicEventUpdate", self)
Apollo.RegisterEventHandler("UnitActivationTypeChanged", "OnUnitActivationTypeChanged", self)
Apollo.RegisterEventHandler("UnitNameChanged", "OnUnitNameChanged", self)
Apollo.RegisterEventHandler("UnitGroupChanged", "OnUnitGroupChanged", self)
Apollo.RegisterEventHandler("Group_MemberFlagsChanged", "OnGroup_MemberFlagsChanged", self)
Apollo.RegisterEventHandler("Group_Left", "OnGroup_Left", self)
Apollo.RegisterEventHandler("Group_Join", "OnGroup_Updated", self)
Apollo.RegisterEventHandler("Group_Updated", "OnGroup_Updated", self)
Apollo.RegisterEventHandler("ChatZoneChange", "OnChatZoneChange", self)
Apollo.RegisterEventHandler("FriendshipAdd", "OnFriendshipChanged", self)
Apollo.RegisterEventHandler("FriendshipPostRemove", "OnFriendshipChanged", self)
Apollo.RegisterEventHandler("FriendshipUpdate", "OnFriendshipChanged", self)
lfrp = Apollo.GetAddon("LFRP")
-- Challenge specific fixes
challengeUnits = {
[L.Unit_Roan_Skull] = { challenge = 576 },
[L.Unit_Shipwrecked_Victim] = { challenge = 603 }
}
end
function Perspective:OnResolutionChanged()
self.DisplaySize = Apollo.GetDisplaySize()
self.MaxDottedLineDelta = self.DisplaySize.nWidth / 6
end
function Perspective:OnEnable()
-- Make sure the addon isn't disabled before starting.
if not Options.db.profile[Options.profile].settings.disabled then
-- Start the timers
Perspective:Start()
end
self.loaded = true
-- if Apollo.GetAddon("Rover") then
-- SendVarToRover("Perspective", self)
-- end
self:OnResolutionChanged()
end
function Perspective:Restart()
self:Stop()
self:Start()
end
function Perspective:Start()
self.offsetLines = Options.db.profile[Options.profile].settings.offsetLines
self.dottedLines = Options.db.profile[Options.profile].settings.dottedLines
self.opacity = Options.db.profile[Options.profile].settings.convertedOpacity
-- Check to see if we are in a raid
self.inRaid = GroupLib.InRaid()
-- Remove the event handler for next frame, again as a precaution
Apollo.RemoveEventHandler("NextFrame", self)
if not Options.db.profile[Options.profile].settings.disabled then
-- Recreate all the units
for id, unit in pairs(self.units.all) do
self:OnUnitCreated(unit)
end
self:MarkersInit()
-- Load the timers
self:SetTimers()
end
self:UpdateZoneSpellEffects()
Apollo.RegisterEventHandler("NextFrame", "OnNextFrame", self)
end
function Perspective:Stop()
-- Disable all timers
for name, timer in pairs(self.timers) do
timer.enabled = nil
timer.elapsed = 0
end
self.units.prioritized = {}
self.units.categorized = {}
self.buffs = nil
self.debuffs = nil
self.markers = {}
self.markersInitialized = false
-- Destroy all our pixies
self.Overlay:DestroyAllPixies()
end
function Perspective:SetTimers()
for name, timer in pairs(self.timers) do
timer.elapsed = 0
timer.time = Options.db.profile[Options.profile].settings[name] / timer.divisor
timer.enabled = true
end
end
function Perspective:GetUnitById(id)
local unit = GameLib.GetUnitById(id)
return unit
end
function Perspective:GetUnitInfo(unit)
if self.units.prioritized[unit:GetId()] then
return self.units.prioritized[unit:GetId()]
elseif self.units.categorized[unit:GetId()] then
return self.units.categorized[unit:GetId()]
else
return { id = unit:GetId() }
end
end
function Perspective:DestroyUnitInfo(unit)
if unit and unit:IsValid() then
self.units.prioritized[unit:GetId()] = nil
self.units.categorized[unit:GetId()] = nil
end
end
function Perspective:OnNextFrame()
if not Options.db.profile[Options.profile].settings.disabled then
-- Save player unit & Race Id
self:UpdateTimers()
end
end
function Perspective:UpdateTimers()
-- Get the amount of time since last update
elapsed = (os.clock() - tick)
for name, timer in pairs(self.timers) do
-- Only update the timer if it's enabled.
if timer.enabled then
-- Update the elapsed time for the timer.
timer.elapsed = timer.elapsed + elapsed
-- Check if its time to fire our timer
if timer.elapsed >= timer.time then
-- Make sure our func exists before attempting to fire it
if self[timer.func] and type(self[timer.func] == "function") then
-- Fire the timers func
self[timer.func](self, elapsed)
end
-- Reset the timers elapsed time
timer.elapsed = 0
end
end
end
-- Save the last tick.
tick = os.clock()
end
function Perspective:OnTimerQueue(elapsed)
if table.getn(self.units.queue) > 0 then
local count = 1
-- Iterrate backwards so we can remove them from the table as we go
for i = table.getn(self.units.queue), 1, -1 do
local update = self.units.queue[i]
if update.recategorize then
-- Update the rewards for this unit.
local canHaveReward = self:UpdateRewards(update.ui, update.unit)
self:UpdateUnitCategory(update.ui, update.unit)
else
-- Update the rewards for this unit.
local canHaveReward = self:UpdateRewards(update.ui, update.unit)
if canHaveReward then
-- If this is a new quest or challenge, first make sure the
-- unit has the quest/challenge
if update.new and update.ui[update.table][update.id] then
-- Recategorize the unit.
self:UpdateUnitCategory(update.ui, update.unit)
elseif not update.new then
-- Recategorize the unit.
self:UpdateUnitCategory(update.ui, update.unit)
end
end
end
table.remove(self.units.queue, i)
count = count + 1
-- Limit to only twenty at a time.
if count >= 5 then
break
end
end
end
end
function Perspective:AddPixie(ui, pPos, pixies, items, lines)
local unit = self:GetUnitById(ui.id)
if unit then -- check for pvp visiblity?then
local isOccluded = unit:IsOccluded()
if isOccluded and unit:IsPvpFlagged() and unit:GetDispositionTo(GameLib.GetPlayerUnit()) == 0 then
-- pvp hostile player, this will cause errors on drawing because we only know its last
-- known location cause the line to go crazy off the screen
return
end
if not ui.disabled and
ui.inRange and
not (ui.disableInCombat and GameLib.GetPlayerUnit():IsInCombat()) and
table.getn(pixies) < Options.db.profile[Options.profile].settings.max
and (not isOccluded or (isOccluded and not ui.disableOccluded)) then
-- Update the units position
local uPos = GameLib.GetUnitScreenPosition(unit)
if uPos then
local showItem = true
local showLine = true
-- Determine if we can show the line
if not ui.showLines or (not uPos.bOnScreen and not ui.showLinesOffscreen) then
showLine = false
end
-- Determine if we can show the icon
if not uPos.bOnScreen then
showItem = false
end
-- We've determined either the lines or icons can be show, now
-- we need to see if we hit our display limit.
if (showItem or showLine) and ui.limitBy and ui.limitId then
for i, id in pairs(ui.limitId) do
-- Determine if our item is within limit.
if showItem and (items[ui.limitBy][id] or 0) >= ui.max then
showItem = false
end
-- Determine if our line is within limit.
if showLine and (lines[ui.limitBy][id] or 0) >= ui.maxLines then
showLine = false
end
end
end
-- Either the item or line are able to be shown.
if showItem or showLine then
-- Add the unit to the draw list.
table.insert(pixies, {
ui = ui,
unit = unit,
uPos = uPos,
pPos = pPos,
showItem = showItem,
showLine = showLine
})
-- Increase our limits.
if ui.limitBy and ui.limitId then
for i, id in pairs(ui.limitId) do
-- Increase the item limit count
if showItem then
items[ui.limitBy][id] = (items[ui.limitBy][id] or 0) + 1
end
-- Increase the line limit count
if showLine then
lines[ui.limitBy][id] = (lines[ui.limitBy][id] or 0) + 1
end
end
end
end
end
end
end
end
function Perspective:GetLineOffsetFromCenter (yDist, vectorLength)
-- Avoid divide by 0
if (vectorLength == 0) then return 0 end
-- Get angle in radians: arcsin of opposite(yDist) / hypothenuse(vectorLength)
local angle = math.asin(yDist / vectorLength)
local Wide = 0
if self.PlayerRaceIsWide ~= nil then Wide = self.PlayerRaceIsWide end
for index, item in pairs(DeadzoneAnglesLookup) do
if (angle >= item.Rad and angle < item.NextRad) then
local DeltaRatio = (angle - item.Rad) / item.DeltaRad
if (Wide == 1) then
return item.WideLength + (item.DeltaWideLength * DeltaRatio)
else
return item.Length + (item.DeltaLength * DeltaRatio)
end
end
end
return 0
end
function Perspective:DrawPixie(ui, unit, uPos, pPos, showItem, showLine, dottedLine, deadzone)
local pixieLocPoints = { 0, 0, 0, 0 }
-- Draw the line first, if it needs to be drawn
if showLine then
-- Get the unit's position and vector
local pos = unit:GetPosition()
local vec = Vector3.New(pos.x, pos.y, pos.z)
-- Get the screen position of the unit by it's vector
local lPos = GameLib.WorldLocToScreenPoint(vec)
local xOffset = 0
local yOffset = 0
local drawLine = 1
if self.offsetLines then
-- Get the length of the vector
local xDist = lPos.x - pPos.nX
local yDist = lPos.y - pPos.nY
local xyDist = xDist * xDist + yDist * yDist
local vectorLength = (xyDist^(-0.5)) * xyDist
-- Get line distance offset based on angle, scale for camera position
local lineOffsetFromCenter = self:GetLineOffsetFromCenter(yDist, vectorLength)
if (deadzone ~= nil) then lineOffsetFromCenter = lineOffsetFromCenter * deadzone.scale end
-- Don't draw "outside-in" lines or if the result will be less than 10 pixels long
if (lineOffsetFromCenter + 25 < vectorLength) then
-- Get the ratio of the line distance from the center of the screen to the vector length
local lengthRatio = lineOffsetFromCenter / vectorLength
-- Get the x and y offsets for the line starting point
xOffset = lengthRatio * xDist
yOffset = lengthRatio * yDist
else
drawLine = 0
end
end
if drawLine == 1 then
if self.dottedLines == true then
-- if true then
-- Draw Dots!
-- First dumb approach, and it seems to work OK:
-- draw dot at start,
-- then one extra dot at ~half (configurable) remaining distance (maybe with a max jump length)
-- until 20 pixels remain on either X or Y axis (really do NOT want to spam SQRT)
-- Remove this from here once this is a config entry.
-- MUST be between 0.1 and 1.0. Really. If it's 0 or less, or >1, it will crash.
-- (don't wanna validate it here 100 times, config UI should guarantee value! let config file hackers crash!)
-- SHOULD be between 0.33 and 0.66, sweet spot is 0.5
-- self.lineStep = 0.5
-- Leaving the comments above for reference later
-- This approach to dots is a simplified process and needs to be improved
-- The goal of this simplified process is to get performance of dots better and then improve
local drawX = pPos.nX + xOffset
local drawY = pPos.nY + yOffset
local targetX = lPos.x
local targetY = lPos.y
local deltaX, deltaY
for i = 1, 6 do
local ratio = i/10
-- Draw Dot
self.Overlay:AddPixie( {
strSprite = "PerspectiveSprites:small-circle", cr = self:HandleGlobalAlpha(ui.cLineColor),
loc = { fPoints = pixieLocPoints, nOffsets = { drawX - 5, drawY - 5, drawX + 5, drawY + 5 } }
} )
-- How far do we still have to go? Stop if close enough
deltaX = (targetX - drawX)
deltaY = (targetY - drawY)
-- -- Step up to the next dot
drawX = drawX + deltaX * ratio
drawY = drawY + deltaY * ratio
end
-- Only draw final dot if not showing item name/icon
if not showItem then
self.Overlay:AddPixie( {
strSprite = "PerspectiveSprites:small-circle", cr = self:HandleGlobalAlpha(ui.cLineColor),
loc = { fPoints = pixieLocPoints, nOffsets = { targetX - 5, targetY - 5, targetX + 5, targetY + 5 } }
} )
end
else
-- Draw lines!
-- Draw the background line to give the outline if required
if ui.showLineOutline then
local lineAlpha = string.sub(ui.cLineColor, 1, 2)
self.Overlay:AddPixie( {
bLine = true, fWidth = ui.lineWidth + 2, cr = self:HandleGlobalAlpha(lineAlpha .. "000000"),
loc = { fPoints = pixieLocPoints, nOffsets = { lPos.x, lPos.y, pPos.nX + xOffset, pPos.nY + yOffset } }
})
end
-- Draw the actual line to the unit's vector
self.Overlay:AddPixie( {
bLine = true, fWidth = ui.lineWidth, cr = self:HandleGlobalAlpha(ui.cLineColor),
loc = { fPoints = pixieLocPoints, nOffsets = { lPos.x, lPos.y, pPos.nX + xOffset, pPos.nY + yOffset } }
} )
end
end
end
-- Draw the icon and text if it needs to be drawn.
if showItem then
-- Draw the icon first
if ui.showIcon then
self.Overlay:AddPixie( {
strSprite = ui.icon, cr = self:HandleGlobalAlpha(ui.cIconColor),
loc = { fPoints = pixieLocPoints, nOffsets = { uPos.nX - (ui.scaledWidth / 2), uPos.nY - (ui.scaledHeight / 2), uPos.nX + (ui.scaledWidth / 2), uPos.nY + (ui.scaledHeight / 2) } }
} )
end
-- Draw the text
if ui.showName or ui.showDistance then
local text = ""
if ui.showName then
text = ui.nameOverride or ui.display or ui.name or ""
end
text = (ui.showDistance and ui.distance >= ui.rangeLimit) and text .. "\n(" .. math.ceil(ui.distance) .. "m)" or text
self.Overlay:AddPixie( {
strText = text, strFont = ui.font, crText = self:HandleGlobalAlpha(ui.cFontColor),
loc = { fPoints = pixieLocPoints, nOffsets = { uPos.nX - 100, uPos.nY + (ui.scaledHeight / 2) + 0, uPos.nX + 100, uPos.nY + (ui.scaledHeight / 2) + 100 } },
flagsText = { DT_CENTER = true, DT_WORDBREAK = true }
} )
end
end
end
function Perspective:OnTimerDraw()
-- Perspective is disabled
if Options.db.profile[Options.profile].settings.disabled then return end
if (self.Player == nil) then
local p = GameLib.GetPlayerUnit()
if (p:IsValid()) then self.Player = p end
end
-- This list will contain all the pixies we we'll need to draw.
local pixies = {}
if (self.PlayerRaceId == nil) then
if (self.Player ~= nil) then
self.PlayerRaceId = self.Player:GetRaceId()
local deadzoneRaceEntry = DeadzoneRaceLookup[self.PlayerRaceId]
if (deadzoneRaceEntry ~= nil) then
self.PlayerRaceIsWide = deadzoneRaceEntry.Wide
else
self.PlayerRaceIsWide = 0
end
end
end
-- Get the player's current screen position
local pPos = GameLib.GetUnitScreenPosition(self.Player)
-- We want to make sure we can get the unit's screen position
if pPos then
-- The limits tables
local items = { unit = {}, category = {}, quest = {}, challenge = {} }
local lines = { unit = {}, category = {}, quest = {}, challenge = {} }
-- Check our prioritized units first, they are the closest to our player
for index, ui in pairs(self.sorted.prioritized) do
self:AddPixie(ui, pPos, pixies, items, lines)
end
-- Finally check our categorized units.
for index, ui in pairs(self.sorted.categorized) do
self:AddPixie(ui, pPos, pixies, items, lines)
end
-- Destroy all our pixies
self.Overlay:DestroyAllPixies()
-- Finally, lets draw some pixies!
-- Draw the markers, they are most likely going to be the farthest pixies from the player
-- so we want them "behind" our other units.
self:MarkersDraw()
-- Drawing pixies want to measure a deadzone. This will be based on character size onscreen
-- (i.e. camera distance/angle). Get the proper information
local deadzone = nil
if (self.Player ~= nil) then
deadzone = {
["nameplateY"] = self.Player:GetOverheadAnchor().y,
["feetY"] = pPos.nY,
["raceScale"] = 1.0,
["scale"] = nil
}
if (self.PlayerRaceId ~= nil) then
local deadzoneRaceEntry = DeadzoneRaceLookup[self.PlayerRaceId]
if (deadzoneRaceEntry ~= nil) then
deadzone.raceScale = deadzoneRaceEntry.Scale
end
end
deadzone.scale = (deadzone.feetY - deadzone.nameplateY) / 300 * deadzone.raceScale
end
-- if deadzone == nil then Print("dz nil") end
-- Now, for the pixies, we'll draw them in reverse, because the lists were sorted by
-- distance, closest to farthest. This will ensure the farthest are drawn first and
-- "behind" our closer pixies.
local pixie
for i = #pixies, 1, -1 do
-- Get our next pixie
pixie = pixies[i]
-- Drw the pixie
self:DrawPixie(
pixie.ui,
pixie.unit,
pixie.uPos,
pixie.pPos,
pixie.showItem,
pixie.showLine,
false, -- replace this by the DottedLine param - "pixie.dottedLine" or some such
deadzone)
end
end
end
-- Updates all the units we know about as well as loading options if its needed.
-- Categorizes and prioritizes our units.
function Perspective:OnTimerSlow()
-- Perspective is disabled
if Options.db.profile[Options.profile].settings.disabled then return end
local player = GameLib.GetPlayerUnit()
if player then
local pos = player:GetPosition()
if pos then
-- Get the player's current vector from position.
local vector = Vector3.New(pos.x, pos.y, pos.z)
self.sorted.categorized = {}
-- Update the categorized units.
for id, ui in pairs(self.units.categorized) do
if ui then
local unit = GameLib.GetUnitById(id)
if unit and self:UpdateUnit(ui, unit) then
table.insert(self.sorted.categorized, ui)
end
end
end
table.sort(self.sorted.categorized, function(a, b) return (a.distance or 0) < (b.distance or 0) end)
if self.markersInitialized then
self:MarkersUpdate(vector)
else
self:MarkersInit()
end
end
-- Check for new spell effects, this is gonna suck :(
for id, unit in pairs(self.units.all) do
-- Limit buffs to players for now
if unit:IsValid() and unit:GetType() == "Player" then
local ui = self:GetUnitInfo(unit)
local category = self:UpdateSpellEffects(ui, unit)
if category ~= ui.category then
-- Need to recategorize this unit
table.insert(self.units.queue, { ui = ui, unit = unit, recategorize = true })
end
end
end
end
end
-- Updates our prioritized (close) units faster than the farther ones.
-- We'll keep this as light weight as possible, only updating the distance and relevant info.
function Perspective:OnTimerFast(forced)
-- Perspective is disabled
if Options.db.profile[Options.profile].settings.disabled then return end
local player = GameLib.GetPlayerUnit()
if player then
local pos = player:GetPosition()
if pos then
-- Get the player's current vector from position.
local vector = Vector3.New(pos.x, pos.y, pos.z)
self.sorted.prioritized = {}
-- Update the prioritized units.
for id, ui in pairs(self.units.prioritized) do
if ui then
local unit = GameLib.GetUnitById(id)
if unit and self:UpdateUnit(ui, unit) then
table.insert(self.sorted.prioritized, ui)
end
end
end
-- Sort the units by distance.
table.sort(self.sorted.prioritized, function(a, b) return (a.distance or 0) < (b.distance or 0) end)
end
end
end
function Perspective:UpdateUnitCategory(ui, unit)
-- Reset the ui category
ui.category = nil
if unit and unit:IsValid() then
-- Get the unit name
ui.name = unit:GetName()
-- Determines if the unit is busy
if not self:IsUnitBusy(unit) then
-- Targetted unit
if unit == GameLib.GetTargetUnit() and
not Options.db.profile[Options.profile].categories.target.disabled then
ui.category = "target"
elseif self.focus and unit == self.focus and
not Options.db.profile[Options.profile].categories.focus.disabled then
ui.category = "focus"
elseif Options.db.profile[Options.profile].categories[ui.name] and
not Options.db.profile[Options.profile].categories[ui.name].disabled then
-- This is a custom category, it has priority over all other category types except
-- target and focus.
ui.category = ui.name
elseif Options.db.profile[Options.profile].names[ui.name] then
local category = Options.db.profile[Options.profile].names[ui.name].category
if not Options.db.profile[Options.profile].categories[category].disabled then
ui.category = category
ui.named = ui.name
end
else
-- Updates the activation state for the unit and determines if it is busy, if it is
-- busy then we do not care for this unit at this time.
self:UpdateActivationState(ui, unit)
end
-- Only continue looking for a category if it has not be found by now, unless its a
-- scientist item, then we'll further check the rewards to see if its an active scan
-- mission target, it will then be reclassified as such.
if not ui.category or ui.category == "scientist" then
-- Determines if any rewards for this unit exist, such as quest objectvies,
-- challenge objectives or scientist scan target.
if ui.hasQuest then
local t = unit:GetType()
if ui.hasActivation and
not Options:GetOptionValue(nil, "disabled", "questInteractable") then
ui.category = "questInteractable"
elseif not ui.hasActivation and (t == "Simple" or t == "SimpleCollidable") then
-- do nothing
--Print("Do nothing: " .. unit:GetName())
else
if ui.hasSpell and
not Options:GetOptionValue(nil, "disabled", "questSpell") then
ui.category = "questSpell"
elseif not Options:GetOptionValue(nil, "disabled", "questObjective") then
ui.category = "questObjective"
end
end
elseif ui.hasEvent then
local t = unit:GetType()
if ui.hasActivation and
not Options:GetOptionValue(nil, "disabled", "eventInteractable") then
ui.category = "eventInteractable"
elseif not ui.hasActivation and (t == "Simple" or t == "SimpleCollidable") then
-- do nothing
--Print("Do nothing: " .. unit:GetName())
elseif not ui.hasActivation then
if not Options:GetOptionValue(nil, "disabled", "eventObjective") then
ui.category = "eventObjective"
end
end
elseif ui.hasChallenge and
not Options:GetOptionValue(nil, "disabled", "challenge") then
ui.category = "challenge"
elseif ui.hasScan and
not Options:GetOptionValue(nil, "disabled", "scientistScans") then
ui.category = "scientistScans"
end
if not ui.category then
-- Attempt to categorize the unit by type.
local type = unit:GetType()
self:UpdateDiscovery(ui, unit)
if type == "Player" then
self:UpdatePlayer(ui, unit)
elseif type == "NonPlayer" then
self:UpdateNonPlayer(ui, unit)
elseif type == "Harvest" then
self:UpdateHarvest(ui, unit)
elseif type == "Simple" then
self:UpdateSimple(ui, unit)
elseif type == "Pickup" then
self:UpdatePickup(ui, unit)
elseif type == "Collectible" then
self:UpdateCollectible(ui, unit)
elseif unit:GetLoot() then
self:UpdateLoot(ui, unit)
end
end
end
-- If a category has still not been found for the unit, then determine its disposition
-- and difficulty and categorize it as such.
if not ui.category and unit:GetType() == "NonPlayer" and not unit:IsDead() and
not (unit:GetMouseOverType() == "Simple" or unit:GetMouseOverType() == "SimpleCollidable") then
local disposition = "friendly"
local difficulty = ""
if unit:GetDispositionTo(GameLib.GetPlayerUnit()) == 0 then
disposition = "hostile"
elseif unit:GetDispositionTo(GameLib.GetPlayerUnit()) == 1 then
disposition = "neutral"
end
-- Rank:
-- Rank 0: Fodder (1st Skull)
-- Rank 1: Minion (1st Skull)
-- Rank 2: Standard (2nd Skull)
-- Rank 3: Champion (2nd Skull)
-- Rank 4: Superior (3rd Skull)
-- Rank 5: Prime (3rd Skull)
-- Eliteness / GroupValue:
-- Eliteness 0, GroupValue 0: 1 Player
-- Eliteness 1, GroupValue 5: 5 Player
-- Eliteness 2, GroupValue 20: 20 Player
-- Rare Mobs:
-- AffiliationName: Elite Champion
-- Not sure how accurate this is
-- Difficulty 1: Minion, Grunt, Challenger
-- Difficulty 3: Prime
-- Difficulty 4: 5 Man? - XT Destroyer (Galeras)
-- Difficulty 5: 10 Man?
-- Difficulty 6: 20 Man? - Doomthorn the Ancient (Galeras)
-- Eliteness 1: 5 Man + (Dungeons?)
-- Eliteness 2: 20 Man? - Doomthorn the Ancient (Galeras)
if unit:GetAffiliationName() == L.Unit_AffiliationName_EliteChampion and disposition == "hostile"then
difficulty = "Elite"
elseif unit:GetGroupValue() > 1 then
difficulty = "Group"
elseif unit:GetRank() >= 4 then
difficulty = "Prime"
end
local npcType = disposition .. difficulty
if not Options.db.profile[Options.profile].categories[npcType].disabled then
ui.category = npcType
end
end
if unit:IsDead() then
-- Clear dead npc quest / challenge objectives and harvests, but not scientist scans
if unit:GetType() == "Harvest" or
ui.category == "questObjective" or
ui.category == "challenge" then
ui.category = nil
end
end
end
end
-- Finally determine that our category has been successfully set
if ui.category then
-- Update the unit's information.
self:UpdateUnitInfo(ui, unit)
else
-- Destroy the unit
self:DestroyUnitInfo(unit)
end
end
-- This gets called when the unit category is updated
function Perspective:UpdateUnitInfo(ui, unit)