Repository navigation
Expand file tree
/
Copy pathPins.lua
More file actions
500 lines (444 loc) · 18.4 KB
/
Copy pathPins.lua
File metadata and controls
500 lines (444 loc) · 18.4 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
-------------------------------------------------------------------------------
-- BlackwellUI for Azeroth Survivors - Pins.lua
-- Copyright (c) 2026 Pierre (Blackwellcode). All rights reserved. See LICENSE.
-- Cyberpunk Minimap Radar & Pins Engine:
-- - Real-time field radar for Caches (AZS_CHESTS) and Guardian Lairs (AZS_GetLairs)
-- - Square minimap edge clamping with distance and directional calculation
-- - Cyberpunk neon pin styling with breathing aura for flagged target lair
-- - Hover tooltip with status, guardian state, and exact yard telemetry
-- Lua 5.0 / WoW 1.12.1: no '#', no '%', no string methods.
-------------------------------------------------------------------------------
local B = BlackwellUI
local L = B.L
local mod = {}
mod.label = "Radar pins"
mod.hint = "Pins stay on the minimap edge when the target is out of range."
-- Pins sit on the minimap and follow its scale; size and opacity are own options.
mod.noLook = true
local ART_PATH = "Interface\\AddOns\\BlackwellUI\\media\\pins\\"
local PIN_SIZE = 14
local EDGE_SIZE = 10
local NEW_PULSE_TIME = 3
mod.options = {
{ type = "section", label = "Pins" },
{ type = "toggle", db = "pinCaches", label = "Caches", default = true },
{ type = "toggle", db = "pinLairs", label = "Guardian lairs", default = true },
{ type = "toggle", key = "showDormant", label = "Dormant guardians", default = true },
{ type = "choice", db = "pinEmpty", label = "Empty lairs", default = "show",
values = { { value = "show", label = "Show" }, { value = "hide", label = "Hide" } } },
{ type = "section", label = "Look" },
{ type = "range", key = "pinSize", label = "Pin size", min = 8, max = 24, step = 1, default = PIN_SIZE, format = "%d px" },
{ type = "range", key = "pinAlpha", label = "Pin opacity", min = 0.3, max = 1, step = 0.05, default = 1, format = "pct" },
{ type = "color", key = "cacheColor", label = "Cache colour", default = { r = 1.0, g = 0.82, b = 0.15 } },
{ type = "color", key = "activeColor", label = "Active guardian colour", default = { r = 1.0, g = 0.25, b = 0.25 } },
{ type = "color", key = "dormantColor", label = "Dormant guardian colour", default = { r = 0.20, g = 0.70, b = 1.0 } },
{ type = "section", label = "Behaviour" },
{ type = "choice", db = "pinEdge", label = "Out of range", default = "clamp",
values = { { value = "clamp", label = "Keep on the edge" }, { value = "hide", label = "Hide" } } },
{ type = "range", key = "edgeAlpha", label = "Edge pin opacity", min = 0.2, max = 1, step = 0.05, default = 0.75, format = "pct" },
{ type = "toggle", key = "pulseFlagged", label = "Pulse the objective lair", default = true },
{ type = "toggle", key = "pulseNew", label = "Pulse new pins", default = false },
{ type = "toggle", key = "showTooltip", label = "Tooltip on hover", default = true },
}
local function Opt(key, default)
return B:ModOpt("pins", key, default)
end
local function OptColor(key, r, g, b)
local c = Opt(key, nil)
if type(c) == "table" and c.r then
return c.r, c.g, c.b
end
return r, g, b
end
-- When each cache / lair was first seen this run (for "Pulse new pins").
local seen = {}
local MINIMAP_YARDS = { [0] = 466.67, [1] = 400.00, [2] = 333.33, [3] = 266.67, [4] = 200.00, [5] = 133.33 }
local INSIDE_YARDS = { [0] = 300.00, [1] = 240.00, [2] = 180.00, [3] = 120.00, [4] = 80.00, [5] = 50.00 }
local spans = {}
local pins = {}
local flaggedPin = nil
local breathTime = 0
local elapsedTimer = 0
local function Playing()
if B.unlocked or (B.db and B.db.demo) then
return true
end
local s = B:GetState()
if not s or not s.active then
return false
end
local phase = s.phase
return phase == "playing" or phase == "choosing" or phase == "paused"
end
local function Share(value)
local n = tonumber(value)
if not n or n < 0 then
return nil
end
if n > 1.5 then
return n / 100
end
return n
end
local function GetPlaces()
local list = {}
local chests = AZS_CHESTS or (type(AZS_GetChests) == "function" and AZS_GetChests()) or {}
local nChests = (type(chests) == "table") and table.getn(chests) or 0
for i = 1, nChests do
table.insert(list, chests[i])
end
local lairs = (type(AZS_GetLairs) == "function" and AZS_GetLairs()) or (type(AZS_LAIRS) == "table" and AZS_LAIRS) or {}
local nLairs = (type(lairs) == "table") and table.getn(lairs) or 0
for i = 1, nLairs do
table.insert(list, lairs[i])
end
return list
end
local NEAREST = 0.004
local function CalculateSpan()
local zone = (AZS_ZONE and AZS_ZONE.zname) or "?"
local known = spans[zone] or { baseAcross = 0, baseDown = 0 }
local places = GetPlaces()
local nPlaces = table.getn(places)
for a = 1, nPlaces do
for b = a + 1, nPlaces do
local p, q = places[a], places[b]
local pmx, qmx = Share(p.mx), Share(q.mx)
local pmy, qmy = Share(p.my), Share(q.my)
local px, qx = tonumber(p.x), tonumber(q.x)
local py, qy = tonumber(p.y), tonumber(q.y)
if pmx and qmx and pmy and qmy and px and qx and py and qy then
local dmx = math.abs(pmx - qmx)
local dmy = math.abs(pmy - qmy)
if dmx > known.baseAcross and dmx > NEAREST then
known.baseAcross = dmx
known.foundAcross = math.abs(py - qy) / dmx
end
if dmy > known.baseDown and dmy > NEAREST then
known.baseDown = dmy
known.foundDown = math.abs(px - qx) / dmy
end
end
end
end
known.across = known.foundAcross or (known.foundDown and known.foundDown * 3 / 2)
known.down = known.foundDown or (known.foundAcross and known.foundAcross * 2 / 3)
spans[zone] = known
if known.across and known.down then
return known
end
return nil
end
local function GetHeroMapPlace()
if not GetPlayerMapPosition then return nil end
local x, y = GetPlayerMapPosition("player")
if (not x or (x == 0 and y == 0)) and SetMapToCurrentZone then
SetMapToCurrentZone()
x, y = GetPlayerMapPosition("player")
end
if not x or (x == 0 and y == 0) then return nil end
return x, y
end
local function GetHeroWorld()
local run = AZS_RUN
if not run then return nil end
local px, py = tonumber(run.px), tonumber(run.py)
if not (px and py) then return nil end
local facing = tonumber(run.pf)
if facing then facing = -facing end
return px, py, facing
end
local function WorldWay(px, py, x, y)
return py - y, px - x
end
local function Indoors()
if not GetCVar then return false end
local zoom = Minimap:GetZoom() or 0
local outside = tonumber(GetCVar("minimapZoom"))
local inside = tonumber(GetCVar("minimapInsideZoom"))
if outside and inside and outside ~= inside then
return zoom == inside
end
return not GetHeroMapPlace()
end
local function GetMinimapYards()
local zoom = Minimap:GetZoom() or 0
local yards = Indoors() and INSIDE_YARDS or MINIMAP_YARDS
return yards[zoom] or yards[0]
end
local function WayTo(place, px, py, heroX, heroY, span)
local x, y = tonumber(place.x), tonumber(place.y)
if px and x and y then
return WorldWay(px, py, x, y)
end
local mx, my = Share(place.mx), Share(place.my)
if heroX and span and mx and my then
return (mx - heroX) * span.across, (my - heroY) * span.down
end
return nil, nil
end
local function CreateOrGetPin(index)
local pin = pins[index]
if pin then return pin end
-- Parented to the watcher so the pins hide with the module outside a run.
pin = CreateFrame("Frame", nil, mod.watcher or Minimap)
pin:SetFrameStrata("HIGH")
pin:SetFrameLevel(25)
pin:SetWidth(PIN_SIZE)
pin:SetHeight(PIN_SIZE)
-- Background glow for flagged lair
local glow = pin:CreateTexture(nil, "BACKGROUND")
glow:SetTexture(ART_PATH .. "PaceGlow.tga")
glow:SetBlendMode("ADD")
glow:SetPoint("CENTER", pin, "CENTER", 0, 0)
glow:Hide()
pin.glow = glow
-- Main pin art
local art = pin:CreateTexture(nil, "ARTWORK")
art:SetAllPoints(pin)
art:SetTexture(ART_PATH .. "PinCache.tga")
pin.art = art
pin:EnableMouse(true)
pin:SetScript("OnEnter", function()
if not Opt("showTooltip", true) then return end
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:ClearLines()
local title = this.title or L["Target"]
GameTooltip:AddLine(title, 0.0, 0.90, 1.0)
if this.note and this.note ~= "" then
GameTooltip:AddLine(this.note, 0.85, 0.85, 0.85)
end
if this.flagged then
GameTooltip:AddLine(L["Primary Objective"], 1.0, 0.82, 0.10)
end
if this.yards then
local dist = math.floor(this.yards + 0.5)
GameTooltip:AddLine(string.format(L["%d yards away"], dist), 0.60, 0.60, 0.60)
end
GameTooltip:Show()
end)
pin:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
pins[index] = pin
return pin
end
local function PlacePin(pin, east, south, yardsAcross, baseSize)
local half = Minimap:GetWidth() / 2
local ya = (yardsAcross and yardsAcross > 0) and yardsAcross or 300
local perYard = (2 * half) / ya
pin.yards = math.sqrt(east * east + south * south)
local px = east * perYard
local py = -south * perYard
local size = baseSize or PIN_SIZE
local edgeSize = math.floor(size * EDGE_SIZE / PIN_SIZE + 0.5)
local alpha = Opt("pinAlpha", 1)
-- Square minimap Chebyshev boundary
local reach = math.max(math.abs(px), math.abs(py))
local limit = half - (edgeSize / 2) - 1
if reach > limit then
if B.db and B.db.pinEdge == "hide" then
pin:Hide()
return
end
if reach > 0 then
px = px * limit / reach
py = py * limit / reach
end
size = edgeSize
pin:SetAlpha(Opt("edgeAlpha", 0.75) * alpha)
else
pin:SetAlpha(alpha)
end
pin:SetWidth(size)
pin:SetHeight(size)
pin:ClearAllPoints()
pin:SetPoint("CENTER", Minimap, "CENTER", px, py)
pin:Show()
end
-- Marks a place the first time it shows up; returns the end of its pulse.
local function NewUntil(key)
local now = (type(GetTime) == "function" and GetTime()) or 0
if not seen[key] then
seen[key] = now + NEW_PULSE_TIME
end
return seen[key]
end
function mod:UpdateRadar()
local shown = 0
flaggedPin = nil
local baseSize = Opt("pinSize", PIN_SIZE)
local cr, cg, cb = OptColor("cacheColor", 1.0, 0.82, 0.15)
local ar, ag, ab = OptColor("activeColor", 1.0, 0.25, 0.25)
local sr, sg, sb = OptColor("dormantColor", 0.20, 0.70, 1.0)
local pulseFlagged = Opt("pulseFlagged", true)
if not Playing() or not B:IsEnabled("pins") then
local nPins = table.getn(pins)
for i = 1, nPins do
pins[i]:Hide()
end
return
end
local px, py = GetHeroWorld()
local heroX, heroY, span
if not px then
heroX, heroY = GetHeroMapPlace()
span = heroX and CalculateSpan()
end
local isDemo = B.unlocked or (B.db and B.db.demo)
if px or span or isDemo then
local yardsAcross = GetMinimapYards()
-- 1. Field Caches (Chests)
local chests = (B.db and B.db.pinCaches ~= false) and (AZS_CHESTS or (type(AZS_GetChests) == "function" and AZS_GetChests()) or {}) or {}
if isDemo and (type(chests) ~= "table" or table.getn(chests) == 0) then
chests = {
{ id = 1, x = 60, y = -50, mx = 0.52, my = 0.48 },
{ id = 2, x = -80, y = 90, mx = 0.46, my = 0.55 },
}
end
local nChests = (type(chests) == "table") and table.getn(chests) or 0
for i = 1, nChests do
local chest = chests[i]
local east, south
if isDemo and not px and not span then
east, south = tonumber(chest.x) or 0, tonumber(chest.y) or 0
else
east, south = WayTo(chest, px, py, heroX, heroY, span)
end
if east and south then
shown = shown + 1
local pin = CreateOrGetPin(shown)
pin.art:SetTexture(ART_PATH .. "PinCache.tga")
pin.art:SetVertexColor(cr, cg, cb) -- Cyber Amber / Gold by default
pin.glow:Hide()
pin.title = L["Cache of Wealth"]
pin.note = L["Break open for gold, stats & card upgrades."]
pin.flagged = nil
pin.glowR, pin.glowG, pin.glowB = cr, cg, cb
pin.newUntil = NewUntil("c" .. tostring(chest.id or i))
PlacePin(pin, east, south, yardsAcross, baseSize)
end
end
-- 2. Guardian Lairs & Bosses (Shrines)
local lairs = (B.db and B.db.pinLairs ~= false) and ((type(AZS_GetLairs) == "function" and AZS_GetLairs()) or (type(AZS_LAIRS) == "table" and AZS_LAIRS) or {}) or {}
if isDemo and (type(lairs) ~= "table" or table.getn(lairs) == 0) then
lairs = {
{ name = "Garr (Guardian)", id = 101, x = -110, y = -90, mx = 0.44, my = 0.45, state = "awake", flag = 1 },
{ name = "Baron Geddon", id = 102, x = 120, y = 130, mx = 0.56, my = 0.56, state = "empty", left = 45 },
}
end
local nLairs = (type(lairs) == "table") and table.getn(lairs) or 0
for i = 1, nLairs do
local lair = lairs[i]
local east, south
if isDemo and not px and not span then
east, south = tonumber(lair.x) or 0, tonumber(lair.y) or 0
else
east, south = WayTo(lair, px, py, heroX, heroY, span)
end
local sState = lair.state
local isActive = (sState == "awake") or (sState == "asleep") or (lair.active == true) or (tonumber(lair.awake) and tonumber(lair.awake) > 0)
local isAsleep = (sState == "asleep")
local wanted = isActive or (B.db and B.db.pinEmpty ~= "hide")
if isAsleep and not Opt("showDormant", true) then wanted = false end
if east and south and wanted then
shown = shown + 1
local pin = CreateOrGetPin(shown)
pin.art:SetTexture(ART_PATH .. "PinLair.tga")
local markName = lair.name or string.format(L["Guardian Lair %s"], tostring(lair.index or i))
pin.title = markName
if isActive then
if isAsleep then
pin.art:SetVertexColor(sr, sg, sb) -- Soft Ice Cyan by default
pin.glowR, pin.glowG, pin.glowB = sr, sg, sb
pin.note = string.format(L["Dormant Guardian: %s (Approach to awaken)"], markName)
else
pin.art:SetVertexColor(ar, ag, ab) -- Neon Crimson / Red by default (Active Boss!)
pin.glowR, pin.glowG, pin.glowB = ar, ag, ab
pin.note = string.format(L["BOSS / GUARDIAN ACTIVE: %s"], markName)
end
else
pin.art:SetVertexColor(0.40, 0.40, 0.45) -- Dimmed Slate
pin.glowR, pin.glowG, pin.glowB = 0.40, 0.40, 0.45
local respawnLeft = tonumber(lair.left) or tonumber(lair.respawn) or 0
if respawnLeft > 0 then
pin.note = string.format(L["Empty lair (Respawns in %s)"], B:Clock(respawnLeft))
else
pin.note = L["Empty lair"]
end
end
pin.flagged = (tonumber(lair.flag) == 1)
pin.newUntil = NewUntil("l" .. tostring(lair.id or lair.name or i))
if pin.flagged and pulseFlagged then
flaggedPin = pin
pin.glow:SetVertexColor(1.0, 0.82, 0.10)
pin.glow:Show()
else
pin.glow:Hide()
end
PlacePin(pin, east, south, yardsAcross, baseSize + 2)
end
end
end
local totalPins = table.getn(pins)
for i = shown + 1, totalPins do
pins[i]:Hide()
end
end
function mod:Create()
local watcher = CreateFrame("Frame", "BUI_RadarWatcher", Minimap)
watcher:SetAllPoints(Minimap)
watcher:EnableMouse(false)
self.watcher = watcher
watcher:SetScript("OnUpdate", function()
local dt = arg1 or 0.05
elapsedTimer = elapsedTimer + dt
if elapsedTimer >= 0.10 then
elapsedTimer = 0
mod:UpdateRadar()
end
-- Flagged pin pulse animation
breathTime = breathTime + dt
if flaggedPin and flaggedPin:IsShown() then
local pSize = flaggedPin:GetWidth() * (2.2 + 0.35 * math.sin(breathTime * 4))
flaggedPin.glow:SetWidth(pSize)
flaggedPin.glow:SetHeight(pSize)
flaggedPin.glow:SetAlpha(0.60 + 0.30 * math.sin(breathTime * 4))
flaggedPin.glow:Show()
end
-- Short pulse on pins that just appeared
if Opt("pulseNew", false) then
local now = (type(GetTime) == "function" and GetTime()) or 0
local nPins = table.getn(pins)
for i = 1, nPins do
local p = pins[i]
if p ~= flaggedPin and p:IsShown() then
if p.newUntil and p.newUntil > now then
local left = (p.newUntil - now) / NEW_PULSE_TIME
local wave = math.sin(breathTime * 8)
local gSize = p:GetWidth() * (1.8 + 0.4 * wave)
p.glow:SetVertexColor(p.glowR or 1, p.glowG or 1, p.glowB or 1)
p.glow:SetWidth(gSize)
p.glow:SetHeight(gSize)
p.glow:SetAlpha((0.45 + 0.35 * wave) * left)
p.glow:Show()
elseif p.glow:IsShown() then
p.glow:Hide()
end
end
end
end
end)
self.frame = watcher
end
-- A new run starts with every place unseen.
function mod:OnRunStart()
for k in pairs(seen) do seen[k] = nil end
end
function mod:ApplyOptions()
self:UpdateRadar()
end
function mod:Update(state)
mod:UpdateRadar()
end
B:RegisterModule("pins", mod)