Repository navigation
Expand file tree
/
Copy pathCore_Route.lua
More file actions
694 lines (622 loc) · 24.6 KB
/
Copy pathCore_Route.lua
File metadata and controls
694 lines (622 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
--[[---------------------------------------------------------------------------
Solaryn's Expedition — route planner.
Builds an ordered list of stops from the quest log, chains objectives into
a walkable sequence, and can drop map pins / set a waypoint for the next
stop. Stored per character in SolarynCharDB.route.
The router is deliberately simple: nearest-neighbour over objective waypoints
using continent-space distance. With a typical 15-quest log on one or two
maps this is effectively optimal and costs nothing, and unlike a real
travelling-salesman solve it stays instant when the log changes on zone change
or objective completion.
-----------------------------------------------------------------------------]]
local ADDON_NAME = ...
local ns = _G.SolarynExpedition
ns.Route = {}
local Route = ns.Route
---------------------------------------------------------------------------
-- Storage
---------------------------------------------------------------------------
local function routeDB()
local c = _G.SolarynCharDB
return c and c.route
end
function Route:Get()
return routeDB() or { stops = {}, built = 0 }
end
function Route:Save(state)
local c = _G.SolarynCharDB
if c then c.route = state end
end
function Route:Clear()
self:Save({ stops = {}, built = 0 })
ns:Fire("route_changed")
end
---------------------------------------------------------------------------
-- Building
---------------------------------------------------------------------------
--- Resolve the ordered objective positions for one quest.
-- Yields a list of { kind="turnin"|"objective", questID, uiMapID, x, y, world }.
local function QuestStops(questID)
local QuestData = ns.QuestData
local stops = {}
local pending = QuestData:PendingObjectives(questID)
local hasPending = #pending > 0
local isComplete = QuestData:IsComplete(questID)
-- Turn-in handling covers two different cases:
-- * the quest is finished, so the ONLY stop is the turn-in NPC
-- * the quest is unfinished, so we stop at the NPC first (useful when
-- objectives backtrack through the same hub) before the objectives
-- Either way the turn-in is worth a stop, and a finished quest is the
-- single most valuable stop on the route.
if ns.Settings().includeTurnIns and (hasPending or isComplete) then
local turnIn = QuestData:TurnInLocation(questID)
if turnIn then
table.insert(stops, {
kind = "turnin",
questID = questID,
uiMapID = turnIn.uiMapID,
x = turnIn.x, y = turnIn.y,
world = turnIn.world,
npcName = turnIn.npcName,
})
end
end
if hasPending then
local wps = QuestData:Waypoints(questID)
-- An override, when present, replaces the client's positions entirely.
local resolved, overridden = ns.Overrides:ResolveObjectives(questID, wps)
for _, wp in ipairs(resolved) do
if type(wp) == "table" and wp.uiMapID and wp.x and wp.y then
table.insert(stops, {
kind = "objective",
questID = questID,
uiMapID = wp.uiMapID,
x = wp.x, y = wp.y,
world = wp.world,
overridden = overridden or nil,
})
end
end
end
return stops
end
--- Order stops into a walking route, nearest-next from `from`.
-- Hand-ins are weighed as farther than they are (settings.batchTurnIns), so
-- the route finishes the objectives around you before walking back to hand
-- in, the way you'd quest an area by hand. A hand-in that's almost on top of
-- you (NEAR_HANDIN_YARDS) costs nothing extra: grab it on the way.
--
-- Objectives are also weighed by how far the quest's level is from yours
-- (settings.routeLevelWeight per level), so quests at your level come first
-- and ones well above it drift to the end. Levels below yours count half:
-- those quests are safe, just worth less. Hand-ins aren't weighed by level;
-- the dangerous part is already done.
local NEAR_HANDIN_YARDS = 75
local HANDIN_WEIGHT_BATCHED, HANDIN_WEIGHT_EAGER = 3.0, 1.15
local function levelFactor(stop, playerLevel, perLevel)
local ql = tonumber(stop.questLevel)
if not ql or not playerLevel or perLevel <= 0 then return 1 end
local gap = ql - playerLevel
if gap < 0 then gap = -gap / 2 end
return 1 + gap * perLevel
end
Route.LevelFactor = levelFactor
local function orderStops(stops, from)
local settings = ns.Settings()
local weight = settings.batchTurnIns and HANDIN_WEIGHT_BATCHED or HANDIN_WEIGHT_EAGER
local playerLevel = ns.UnitLevelSafe()
local perLevel = tonumber(settings.routeLevelWeight) or 0
local pool, ordered = {}, {}
for i, st in ipairs(stops) do pool[i] = st end
local cur = from
while #pool > 0 do
local bestIdx, bestCost = 1, math.huge
for i, st in ipairs(pool) do
local d = (cur and ns:DistanceBetween(cur, st)) or 1e9
if st.kind == "turnin" then
if d > NEAR_HANDIN_YARDS then d = d * weight end
else
d = d * levelFactor(st, playerLevel, perLevel)
end
if d < bestCost then bestIdx, bestCost = i, d end
end
local chosen = table.remove(pool, bestIdx)
table.insert(ordered, chosen)
cur = chosen
end
for i, st in ipairs(ordered) do st.index = i end
return ordered
end
Route.OrderStops = orderStops
local QuestSortScore -- defined below Build
--- Build a route from the current quest log.
-- Returns a state table: { stops = {...}, built = time(), questIDs = {...} }
function Route:Build(opts)
local settings = ns.Settings()
opts = opts or {}
local limit = opts.limit or settings.maxRouteQuests
local QuestData = ns.QuestData
local entries = QuestData:Scan()
-- Collect candidate quests, skipping headers and anything suppressed.
-- Dungeon quests stay out unless asked for: they can't be done on the way.
-- Once one is complete its hand-in is an ordinary stop, so it comes back.
local candidates = {}
for _, e in ipairs(entries) do
if not e.isHeader and not e.isHidden then
local skipDungeon = e.isDungeon and not settings.includeDungeons
and not QuestData:IsComplete(e.questID)
if not (settings.ignoreTasks and e.isTask) and not skipDungeon then
table.insert(candidates, e)
end
end
end
-- Keep only quests we can actually locate.
local located = {}
for _, e in ipairs(candidates) do
local stops = QuestStops(e.questID)
if #stops > 0 then
e._stops = stops
table.insert(located, e)
end
end
if #located == 0 then
self:Save({ stops = {}, built = time(), questIDs = {} })
ns:Fire("route_changed")
return self:Get()
end
-- Order quests by proximity to the player, so the route starts nearby.
local pp = ns:PlayerPosition(ns:PlayerMapID())
table.sort(located, function(a, b)
return QuestSortScore(a, pp) > QuestSortScore(b, pp)
end)
local allStops = {}
local questIDs = {}
for _, e in ipairs(located) do
if #questIDs < limit then
for _, s in ipairs(e._stops) do
s.title = e.title
s.questLevel = e.level
s.isTask = e.isTask
table.insert(allStops, s)
end
table.insert(questIDs, e.questID)
end
end
-- Order the individual stops into a walking route from the player.
local ordered = orderStops(allStops, pp)
-- While guiding, stops already reached stay done: they go first and the
-- guide resumes after them. (Opening the panel rebuilds a stale route, and
-- without this, accepting a quest sent you back to a stop you'd reached.)
local prev = self:Get()
local wasGuiding = prev.guiding
local visited = wasGuiding and prev.visited or nil
local current = 1
if visited then
local done, rest = {}, {}
for _, st in ipairs(ordered) do
table.insert(visited[Route.StopKey(st)] and done or rest, st)
end
current = #done + 1
ordered = done -- (same table: count taken above)
for _, st in ipairs(rest) do table.insert(ordered, st) end
for i, st in ipairs(ordered) do st.index = i end
end
local state = {
stops = ordered,
built = time(),
questIDs = questIDs,
total = #ordered,
current = current,
guiding = wasGuiding or nil,
visited = visited,
}
self:Save(state)
if wasGuiding and ordered[current] then self:SetWaypointTo(ordered[current]) end
ns:Fire("route_changed")
return state
end
-- Ranking helper used inside Build. Declared here so it can reference the
-- suggester weights without creating a load-order dependency.
function QuestSortScore(entry, playerPos) -- the local declared above
local settings = ns.Settings()
local score = 0
-- Turn-in-ready quests jump the queue.
if ns.QuestData:IsComplete(entry.questID) then
score = score + settings.wTurnIn
end
-- Distance penalty.
local d = ns.QuestData:ObjectiveDistance(entry.questID)
if d and playerPos then
score = score - (d / 1000) * settings.wDistance
elseif d then
score = score - (d / 1000) * settings.wDistance
end
-- Level fit.
if entry.level and ns.UnitLevelSafe then
local fit = 1 - math.min(1, math.abs(entry.level - ns.UnitLevelSafe()) / 10)
score = score + fit * settings.wLevel
end
if entry.isStory then score = score + settings.wStory end
if entry.isTask then score = score + settings.wTask end
return score
end
---------------------------------------------------------------------------
-- Querying
---------------------------------------------------------------------------
function Route:Count()
return #(self:Get().stops or {})
end
--- The stop the player is heading to (the first unfinished one).
function Route:Next()
local state = self:Get()
local stops = state.stops
if not stops or #stops == 0 then return nil end
return stops[state.current or 1]
end
--- Index of the stop the player is heading to.
function Route:CurrentIndex()
return self:Get().current or 1
end
function Route:IsGuiding()
return self:Get().guiding and true or false
end
function Route:At(i)
local stops = self:Get().stops
if not stops then return nil end
return stops[i]
end
--- Rebuild only if the log looks meaningfully different since the last build.
function Route:IsStale()
local state = self:Get()
if not state.built then return true end
if not ns.QuestData:Scan() then return true end
-- Cheap fingerprint: sorted list of quest IDs in the log.
local ids = {}
for _, e in ipairs(ns.QuestData:Scan()) do
if not e.isHeader then table.insert(ids, e.questID) end
end
table.sort(ids)
local fp = table.concat(ids, ",")
if fp ~= state.fingerprint then return true end
return false
end
function Route:StampFingerprint()
local state = self:Get()
local ids = {}
for _, e in ipairs(ns.QuestData:Scan()) do
if not e.isHeader then table.insert(ids, e.questID) end
end
table.sort(ids)
state.fingerprint = table.concat(ids, ",")
self:Save(state)
end
---------------------------------------------------------------------------
-- Waypoint + map pin control
---------------------------------------------------------------------------
--- Set the client map waypoint to a route stop.
--- Point the client waypoint at a stop, without touching guide state.
function Route:SetWaypointTo(stop)
if not stop or not stop.uiMapID or not stop.x then return false end
if ns.Has.canSetWaypoint and not ns.Try("C_Map", "CanSetUserWaypointOnMap", stop.uiMapID) then
ns:Print("cannot place a waypoint on %s.", ns:MapName(stop.uiMapID))
return false
end
return ns.MapPins:SetWaypoint(stop.uiMapID, stop.x, stop.y)
end
--- Stable identity for a stop across rebuilds.
function Route.StopKey(stop)
return string.format("%s:%s:%.4f:%.4f", tostring(stop.questID), tostring(stop.kind),
tonumber(stop.x) or 0, tonumber(stop.y) or 0)
end
--- Find a stop's position in the current route (stops are saved tables,
--- so match by content rather than identity).
local function indexOf(stops, stop)
if stop.index and stops[stop.index] and stops[stop.index].questID == stop.questID
and stops[stop.index].kind == stop.kind then
return stop.index
end
for i, s in ipairs(stops) do
if s == stop or (s.questID == stop.questID and s.kind == stop.kind and s.x == stop.x and s.y == stop.y) then
return i
end
end
end
--- Set a waypoint to a stop. A stop on the route also starts the guide from
--- that stop, so the waypoint moves on by itself as you go.
function Route:GoToStop(stop)
if not stop then return false end
if not ns.Has.setUserWaypoint then
ns:Print("waypoint API unavailable in this build.")
return false
end
local ok = self:SetWaypointTo(stop)
if not ok then return false end
local state = self:Get()
local i = indexOf(state.stops or {}, stop)
if i and ns.Settings().autoAdvance then
state.current, state.guiding = i, true
self:Save(state)
ns:Fire("route_changed")
end
return true
end
---------------------------------------------------------------------------
-- Guide: follow the route stop by stop
---------------------------------------------------------------------------
-- While guiding, the waypoint sits on the current stop. The guide moves on
-- when you arrive within settings.arriveRadius yards, or, with
-- settings.advanceWhenDone, only once the stop's quest work is finished. A
-- stop whose quest is finished or gone from the log is always skipped,
-- whichever mode is on.
--- True once nothing is left to do at this stop.
function Route:IsStopDone(stop)
local e = stop and ns.QuestData.cache[stop.questID]
if not e or e.inLog == false then return true end -- turned in or abandoned
if stop.kind == "objective" then
return ns.QuestData:IsComplete(stop.questID)
end
return false -- turn-in: done once it leaves the log
end
--- Yards from the player to a stop, or nil.
function Route:DistanceTo(stop)
if not stop then return nil end
local pp = ns:PlayerPosition(stop.uiMapID) or ns:PlayerPosition(ns:PlayerMapID())
return ns:DistanceBetween(pp, stop)
end
local function playCue()
local kit = _G.SOUNDKIT
if type(_G.PlaySound) == "function" and type(kit) == "table" and kit.MAP_PING then
pcall(_G.PlaySound, kit.MAP_PING)
end
end
--- Start (or restart) the guide at a stop index (default: the current one).
function Route:Guide(index)
local state = self:Get()
local stops = state.stops or {}
if #stops == 0 then
ns:Print("no route to follow. Build one first.")
return false
end
state.current = math.max(1, math.min(#stops, index or state.current or 1))
-- A new guide starts with a clean slate of reached stops.
if not state.guiding then state.visited = nil end
state.guiding = true
self:Save(state)
-- Skip anything already finished before pointing the way.
if self:IsStopDone(stops[state.current]) then
return self:Advance("done")
end
local stop = stops[state.current]
self:SetWaypointTo(stop)
ns:Print("guiding: stop %d of %d — %s (%s)", state.current, #stops,
ns:Truncate(stop.title or "?", 30), ns:MapName(stop.uiMapID))
ns:Fire("route_changed")
return true
end
function Route:StopGuiding()
local state = self:Get()
state.guiding, state.visited = nil, nil
self:Save(state)
ns:Fire("route_changed")
end
--- Move to the next unfinished stop. `reason` is "arrived", "done" or
--- "skipped", for the chat message.
function Route:Advance(reason)
local state = self:Get()
local stops = state.stops or {}
local from = stops[state.current or 1]
if from then
state.visited = state.visited or {}
state.visited[Route.StopKey(from)] = true
end
local i = (state.current or 1) + 1
while stops[i] and self:IsStopDone(stops[i]) do i = i + 1 end
if not stops[i] then
state.current = #stops + 1
state.guiding, state.visited = nil, nil
self:Save(state)
if ns.Settings().clearWaypointOnArrive then ns.MapPins:ClearWaypoint() end
ns:Print("route complete!")
playCue()
ns:Fire("route_changed")
return false
end
state.current = i
self:Save(state)
local stop = stops[i]
self:SetWaypointTo(stop)
local verb = reason == "arrived" and "arrived" or reason == "skipped" and "skipped" or "done"
ns:Print("%s — next stop %d of %d: %s (%s)", verb, i, #stops,
ns:Truncate(stop.title or "?", 30), ns:MapName(stop.uiMapID))
playCue()
ns:Fire("route_changed")
return true
end
--- Check the current stop and advance if it's reached or finished.
-- `questChanged` is true when called for a quest event, which is the only
-- time completion needs re-checking; the frequent position ticks only
-- measure distance.
--- Re-plan the stops not yet reached, from where the player is now.
-- Stops before the current one (already done) keep their places.
function Route:Replan()
local state = self:Get()
local stops = state.stops or {}
local cur = state.current or 1
local head, tail = {}, {}
for i, st in ipairs(stops) do
table.insert(i < cur and head or tail, st)
end
local pp = ns:PlayerPosition(ns:PlayerMapID())
for _, st in ipairs(orderStops(tail, pp)) do table.insert(head, st) end
for i, st in ipairs(head) do st.index = i end
state.stops = head
self:Save(state)
end
--- A quest finished mid-route has no hand-in stop yet (those are only added
-- for quests already complete when the route was built). Swap the finished
-- quest's remaining objective stops for its hand-in, then re-plan the rest of
-- the route: the hand-in lands where it fits best, usually after the other
-- objectives nearby rather than straight away.
-- Returns `addedTitles` (list of quest titles whose hand-in was added), or nil.
function Route:PromoteTurnIns()
local state = self:Get()
local stops = state.stops or {}
local cur = state.current or 1
local hasTurnIn = {}
for _, st in ipairs(stops) do
if st.kind == "turnin" then hasTurnIn[st.questID] = true end
end
local finished = {}
for i = cur, #stops do
local st = stops[i]
if st.kind == "objective" and not hasTurnIn[st.questID] and not finished[st.questID]
and ns.QuestData:IsComplete(st.questID) then
local t = ns.QuestData:TurnInLocation(st.questID)
if t then finished[st.questID] = { from = st, at = t } end
end
end
if not next(finished) then return nil end
local kept, added = {}, {}
for i, st in ipairs(stops) do
if not (i >= cur and st.kind == "objective" and finished[st.questID]) then
table.insert(kept, st)
end
end
for questID, f in pairs(finished) do
table.insert(kept, {
kind = "turnin", questID = questID, title = f.from.title,
questLevel = f.from.questLevel, isTask = f.from.isTask,
uiMapID = f.at.uiMapID, x = f.at.x, y = f.at.y, world = f.at.world,
npcName = f.at.npcName,
})
table.insert(added, f.from.title or "?")
end
state.stops = kept
self:Save(state)
self:Replan()
return added
end
function Route:CheckProgress(questChanged)
local state = self:Get()
if not state.guiding or not ns.Settings().autoAdvance then return end
local stop = (state.stops or {})[state.current or 1]
if not stop then return end
-- Re-read the log first so a quest that just left it is seen as gone,
-- whichever module's event handler happened to run first.
if questChanged then
ns.QuestData:Scan()
local added = self:PromoteTurnIns()
if added then
state = self:Get()
stop = state.stops[state.current or 1]
for _, title in ipairs(added) do
ns:Print("%s complete — hand-in added to the route.", ns:Truncate(title, 30))
end
if stop and not self:IsStopDone(stop) then
self:SetWaypointTo(stop)
ns:Print("next: stop %d — %s (%s)", state.current or 1,
ns:Truncate(stop.title or "?", 30), ns:MapName(stop.uiMapID))
ns:Fire("route_changed")
return
end
ns:Fire("route_changed")
end
end
if questChanged and self:IsStopDone(stop) then
return self:Advance("done")
end
-- Arrival only counts when the player opted into advancing on arrival;
-- by default a stop is finished by finishing its quest work.
if not ns.Settings().advanceWhenDone then
local d = self:DistanceTo(stop)
if d and d <= (ns.Settings().arriveRadius or 30) then
return self:Advance("arrived")
end
end
end
--- Jump the world map to a stop's location.
function Route:ShowOnMap(stop)
if not stop then return false end
return ns.MapPins:OpenAt(stop.uiMapID, stop.x, stop.y)
end
--- Map pins for the whole route, so the map shows the full plan at a glance.
function Route:Pins()
local out = {}
local stops = self:Get().stops
for _, s in ipairs(stops or {}) do
table.insert(out, {
uiMapID = s.uiMapID,
position = { x = s.x, y = s.y },
texture = s.kind == "turnin" and "Interface\\TargetingFrame\\UI-HitMarker" or nil,
isMini = false,
isMiniMap = false,
alpha = 1,
label = s.title,
})
end
return out
end
---------------------------------------------------------------------------
-- Events
---------------------------------------------------------------------------
local function onQuestLogChanged()
ns.QuestData:ClearPendingLinks()
end
ns:RegisterEvent("QUEST_LOG_UPDATE", onQuestLogChanged, Route)
-- Keeping the route current. Every quest event funnels into one debounced
-- Sync: if the set of quests in the log, or which of them are complete, has
-- changed since the last sync (a quest accepted, finished, handed in or
-- abandoned), the route is rebuilt from where the player stands. Objective
-- progress alone ("4/8") doesn't rebuild. While guiding, Build keeps the
-- stops already reached, so the guide carries on from the re-optimised route;
-- then the current stop is checked for completion as before.
local lastSyncPrint
--- "questID:complete" for every quest in the log, sorted.
local function logFingerprint()
local parts = {}
for _, e in ipairs(ns.QuestData:Scan()) do
if not e.isHeader then
parts[#parts + 1] = e.questID .. (ns.QuestData:IsComplete(e.questID) and ":c" or ":o")
end
end
table.sort(parts)
return table.concat(parts, ","), #parts
end
function Route:Sync()
local print, count = logFingerprint()
local changed = print ~= lastSyncPrint
lastSyncPrint = print
if changed and ns.Settings().autoRebuild and count > 0 then
local before = self:Next()
local beforeKey = before and Route.StopKey(before)
self:Build()
self:StampFingerprint()
local after = self:Next()
if self:IsGuiding() and after and Route.StopKey(after) ~= beforeKey and not self:IsStopDone(after) then
ns:Print("route updated — next: %s (%s)", ns:Truncate(after.title or "?", 30), ns:MapName(after.uiMapID))
end
end
self:CheckProgress(true)
end
--- Forget the last synced quest log (tests; also forces the next Sync to rebuild).
function Route:ResetSync() lastSyncPrint = nil end
local function onQuestEvent()
ns:Debounce("route-sync", 0.4, function() Route:Sync() end)
end
for _, event in ipairs({ "QUEST_LOG_UPDATE", "QUEST_ACCEPTED", "QUEST_TURNED_IN", "QUEST_REMOVED" }) do
ns:RegisterEvent(event, onQuestEvent, Route)
end
-- Arrival: a light position check twice a second, only while guiding.
local GUIDE_INTERVAL = 0.5
local guideElapsed = 0
local guideTicker = CreateFrame("Frame")
guideTicker:SetScript("OnUpdate", function(_, elapsed)
guideElapsed = guideElapsed + (elapsed or 0)
if guideElapsed < GUIDE_INTERVAL then return end
guideElapsed = 0
if not ns.playerName then return end
local ok, err = pcall(Route.CheckProgress, Route, false)
if not ok then ns:Debug("guide tick failed: %s", tostring(err)) end
end)
Route.guideTicker = guideTicker
return Route