update changes
[QuestHelper.git] / objective.lua
blob4702f0f5ec2bd877917e56ea61e85baa01b0813c
1 QuestHelper_File["objective.lua"] = "Development Version"
2 QuestHelper_Loadtime["objective.lua"] = GetTime()
5 local UserIgnored = {
6 name = "user_manual_ignored",
7 no_disable = true,
8 friendly_reason = QHText("FILTERED_USER"),
9 AddException = function(self, node)
10 QH_Route_UnignoreNode(node, self) -- there isn't really state with this one
11 end
14 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
15 local submenu = self:CreateMenu()
17 local pri = (QH_Route_GetClusterPriority(obj.cluster) or 0) + 3
18 for i = 1, 5 do
19 local name = QHText("PRIORITY"..i)
20 local item = self:CreateMenuItem(submenu, name)
21 local tex
23 if pri == i then
24 tex = self:CreateIconTexture(item, 10)
25 else
26 tex = self:CreateIconTexture(item, 12)
27 tex:SetVertexColor(1, 1, 1, 0)
28 end
30 item:AddTexture(tex, true)
31 item:SetFunction(QH_Route_SetClusterPriority, obj.cluster, i - 3)
32 end
34 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
36 --[[if self.sharing then
37 submenu = self:CreateMenu()
38 local item = self:CreateMenuItem(submenu, QHText("SHARING_ENABLE"))
39 local tex = self:CreateIconTexture(item, 10)
40 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
41 item:AddTexture(tex, true)
42 item:SetFunction(obj.Share, obj)
44 local item = self:CreateMenuItem(submenu, QHText("SHARING_DISABLE"))
45 local tex = self:CreateIconTexture(item, 10)
46 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
47 item:AddTexture(tex, true)
48 item:SetFunction(obj.Unshare, obj)
50 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
51 end]]
53 --self:CreateMenuItem(menu, "(No options available)")
55 if not obj.map_suppress_ignore then
56 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(function () if obj.cluster then for _, v in ipairs(obj.cluster) do QH_Route_IgnoreNode(v, UserIgnored) end end end) -- There is probably a nasty race condition here. I'm not entirely happy about it.
57 end
58 if obj.map_custom_menu then
59 obj.map_custom_menu(menu)
60 end
62 if obj.cluster and #obj.cluster > 1 and QH_Route_Ignored_Cluster_Active(obj.cluster) > 1 then
63 self:CreateMenuItem(menu, QHText("IGNORE_LOCATION")):SetFunction(QH_Route_IgnoreNode, obj, UserIgnored)
64 end
65 end
67 do return end
69 local function ObjectiveCouldBeFirst(self)
70 if (self.user_ignore == nil and self.auto_ignore) or self.user_ignore then
71 return false
72 end
74 for i, j in pairs(self.after) do
75 if i.watched then
76 return false
77 end
78 end
80 return true
81 end
83 local function DefaultObjectiveKnown(self)
84 if self.user_ignore == nil then
85 if (self.filter_zone and QuestHelper_Pref.filter_zone) or
86 (self.filter_done and QuestHelper_Pref.filter_done) or
87 (self.filter_level and QuestHelper_Pref.filter_level) or
88 (self.filter_blocked and QuestHelper_Pref.filter_blocked) or
89 (self.filter_watched and QuestHelper_Pref.filter_watched) then
90 return false
91 end
92 elseif self.user_ignore then
93 return false
94 end
96 for i, j in pairs(self.after) do
97 if i.watched and not i:Known() then -- Need to know how to do everything before this objective.
98 return false
99 end
102 return true
105 local function ObjectiveReason(self, short)
106 local reason, rc = nil, 0
107 if self.reasons then
108 for r, c in pairs(self.reasons) do
109 if not reason or c > rc or (c == rc and r > reason) then
110 reason, rc = r, c
115 if not reason then reason = "Do some extremely secret unspecified something." end
117 if not short and self.pos and self.pos[6] then
118 reason = reason .. "\n" .. self.pos[6]
121 return reason
124 local function Uses(self, obj, text)
125 if self == obj then return end -- You cannot use yourself. A purse is not food.
126 local uses, used = self.uses, obj.used
128 if not uses then
129 uses = QuestHelper:CreateTable("uses")
130 self.uses = uses
133 if not used then
134 used = QuestHelper:CreateTable("used")
135 obj.used = used
138 if not uses[obj] then
139 uses[obj] = true
140 used[self] = text
141 obj:MarkUsed()
145 local function DoMarkUsed(self)
146 -- Objectives should call 'self:Uses(objective, text)' to mark objectives they use by don't directly depend on.
147 -- This information is used in tooltips.
148 -- text is passed to QHFormat with the name of the objective being used.
151 local function MarkUsed(self)
152 if not self.marked_used then
153 self.marked_used = 1
154 self:DoMarkUsed()
155 else
156 self.marked_used = self.marked_used + 1
160 local function MarkUnused(self)
161 assert(self.marked_used)
163 if self.marked_used == 1 then
164 local uses = self.uses
166 if uses then
167 for obj in pairs(uses) do
168 obj.used[self] = nil
169 obj:MarkUnused()
172 QuestHelper:ReleaseTable(uses)
173 self.uses = nil
176 if self.used then
177 assert(not next(self.used))
178 QuestHelper:ReleaseTable(self.used)
179 self.used = nil
182 self.marked_used = nil
183 else
184 self.marked_used = self.marked_used - 1
188 local function DummyObjectiveKnown(self)
189 return (self.o.pos or self.fb.pos) and DefaultObjectiveKnown(self)
192 local function ItemKnown(self)
193 if not DefaultObjectiveKnown(self) then return false end
195 if self.o.vendor then
196 for i, npc in ipairs(self.o.vendor) do
197 local n = self.qh:GetObjective("monster", npc)
198 local faction = n.o.faction or n.fb.faction
199 if (not faction or faction == self.qh.faction) and n:Known() then
200 return true
205 if self.fb.vendor then
206 for i, npc in ipairs(self.fb.vendor) do
207 local n = self.qh:GetObjective("monster", npc)
208 local faction = n.o.faction or n.fb.faction
209 if (not faction or faction == self.qh.faction) and n:Known() then
210 return true
215 if self.o.pos or self.fb.pos then
216 return true
219 if self.o.drop then for monster in pairs(self.o.drop) do
220 if self.qh:GetObjective("monster", monster):Known() then
221 return true
223 end end
225 if self.fb.drop then for monster in pairs(self.fb.drop) do
226 if self.qh:GetObjective("monster", monster):Known() then
227 return true
229 end end
231 if self.o.contained then for item in pairs(self.o.contained) do
232 if self.qh:GetObjective("item", item):Known() then
233 return true
235 end end
237 if self.fb.contained then for item in pairs(self.fb.contained) do
238 if self.qh:GetObjective("item", item):Known() then
239 return true
241 end end
243 if self.quest then
244 local item=self.quest.o.item
245 item = item and item[self.obj]
247 if item then
248 if item.pos then
249 return true
251 if item.drop then
252 for monster in pairs(item.drop) do
253 if self.qh:GetObjective("monster", monster):Known() then
254 return true
260 item=self.quest.fb.item
261 item = item and item[self.obj]
262 if item then
263 if item.pos then
264 return true
266 if item.drop then
267 for monster in pairs(item.drop) do
268 if self.qh:GetObjective("monster", monster):Known() then
269 return true
276 return false
279 local function ObjectiveAppendPositions(self, objective, weight, why, restrict)
280 local high = 0
282 if self.o.pos then for i, p in ipairs(self.o.pos) do
283 high = math.max(high, p[4])
284 end end
286 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
287 high = math.max(high, p[4])
288 end end
290 high = weight/high
292 if self.o.pos then for i, p in ipairs(self.o.pos) do
293 if not restrict or not self.qh:Disallowed(p[1]) then
294 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
296 end end
298 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
299 if not restrict or not self.qh:Disallowed(p[1]) then
300 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
302 end end
306 local function ObjectivePrepareRouting(self, anywhere)
307 self.setup_count = self.setup_count + 1
308 if not self.setup then
309 assert(not self.d)
310 assert(not self.p)
311 assert(not self.nm)
312 assert(not self.nm2)
313 assert(not self.nl)
315 self.d = QuestHelper:CreateTable("objective.d")
316 self.p = QuestHelper:CreateTable("objective.p")
317 self.nm = QuestHelper:CreateTable("objective.nm")
318 self.nm2 = QuestHelper:CreateTable("objective.nm2")
319 self.nl = QuestHelper:CreateTable("objective.nl")
320 self.distance_cache = QuestHelper:CreateTable("objective.distance_cache")
322 if not anywhere then
323 self:AppendPositions(self, 1, nil, true)
325 if not next(self.p) then
326 QuestHelper:TextOut(QHFormat("INACCESSIBLE_OBJ", self.obj or "whatever it was you just requested"))
327 anywhere = true
331 if anywhere then
332 self:AppendPositions(self, 1, nil, false)
335 self:FinishAddLoc(args)
339 local function ItemAppendPositions(self, objective, weight, why, restrict)
340 why2 = why and why.."\n" or ""
342 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
343 local n = self.qh:GetObjective("monster", npc)
344 local faction = n.o.faction or n.fb.faction
345 if (not faction or faction == self.qh.faction) then
346 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
348 end end
350 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
351 local n = self.qh:GetObjective("monster", npc)
352 local faction = n.o.faction or n.fb.faction
353 if (not faction or faction == self.qh.faction) then
354 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc), restrict)
356 end end
358 if next(objective.p, nil) then
359 -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
360 -- towns people just because you had to talk to them anyway, and it saves walking to the store.
361 return
364 if self.o.drop then for monster, count in pairs(self.o.drop) do
365 local m = self.qh:GetObjective("monster", monster)
366 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
367 end end
369 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
370 local m = self.qh:GetObjective("monster", monster)
371 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
372 end end
374 if self.o.contained then for item, count in pairs(self.o.contained) do
375 local i = self.qh:GetObjective("item", item)
376 i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
377 end end
379 if self.fb.contained then for item, count in pairs(self.fb.contained) do
380 local i = self.qh:GetObjective("item", item)
381 i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item), restrict)
382 end end
384 if self.o.pos then for i, p in ipairs(self.o.pos) do
385 if not restrict or not self.qh:Disallowed(p[1]) then
386 objective:AddLoc(p[1], p[2], p[3], p[4], why)
388 end end
390 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
391 if not restrict or not self.qh:Disallowed(p[1]) then
392 objective:AddLoc(p[1], p[2], p[3], p[4], why)
394 end end
396 if self.quest then
397 local item_list=self.quest.o.item
398 if item_list then
399 local data = item_list[self.obj]
400 if data and data.drop then
401 for monster, count in pairs(data.drop) do
402 local m = self.qh:GetObjective("monster", monster)
403 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
405 elseif data and data.pos then
406 for i, p in ipairs(data.pos) do
407 if not restrict or not self.qh:Disallowed(p[1]) then
408 objective:AddLoc(p[1], p[2], p[3], p[4], why)
414 item_list=self.quest.fb.item
415 if item_list then
416 local data = item_list[self.obj]
417 if data and data.drop then
418 for monster, count in pairs(data.drop) do
419 local m = self.qh:GetObjective("monster", monster)
420 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster), restrict)
422 elseif data and data.pos then
423 for i, p in ipairs(data.pos) do
424 if not restrict or not self.qh:Disallowed(p[1]) then
425 objective:AddLoc(p[1], p[2], p[3], p[4], why)
433 local function ItemDoMarkUsed(self)
434 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
435 local n = self.qh:GetObjective("monster", npc)
436 local faction = n.o.faction or n.fb.faction
437 if (not faction or faction == self.qh.faction) then
438 self:Uses(n, "TOOLTIP_PURCHASE")
440 end end
442 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
443 local n = self.qh:GetObjective("monster", npc)
444 local faction = n.o.faction or n.fb.faction
445 if (not faction or faction == self.qh.faction) then
446 self:Uses(n, "TOOLTIP_PURCHASE")
448 end end
450 if self.o.drop then for monster, count in pairs(self.o.drop) do
451 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
452 end end
454 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
455 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
456 end end
458 if self.o.contained then for item, count in pairs(self.o.contained) do
459 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
460 end end
462 if self.fb.contained then for item, count in pairs(self.fb.contained) do
463 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
464 end end
466 if self.quest then
467 local item_list=self.quest.o.item
468 if item_list then
469 local data = item_list[self.obj]
470 if data and data.drop then
471 for monster, count in pairs(data.drop) do
472 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
477 item_list=self.quest.fb.item
478 if item_list then
479 local data = item_list[self.obj]
480 if data and data.drop then
481 for monster, count in pairs(data.drop) do
482 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
498 ---------------
512 local function AddLoc(self, index, x, y, w, why)
513 assert(not self.setup)
515 if w > 0 then
516 local pair = QuestHelper_ZoneLookup[index]
517 if not pair then return end -- that zone doesn't exist! We require more vespene gas. Not enough rage!
518 local c, z = pair[1], pair[2]
519 x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
521 x = x * self.qh.continent_scales_x[c]
522 y = y * self.qh.continent_scales_y[c]
523 local list = self.qh.zone_nodes[index]
525 local points = self.p[list]
526 if not points then
527 points = QuestHelper:CreateTable("objective.p[zone] (objective nodes per-zone)")
528 self.p[list] = points
531 for i, p in pairs(points) do
532 local u, v = x-p[3], y-p[4]
533 if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
534 p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
535 p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
536 p[5] = p[5]+w
537 if w > p[7] then
538 p[6], p[7] = why, w
540 return
544 local new = QuestHelper:CreateTable("objective.p[zone] (possible objective node)")
545 new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
546 table.insert(points, new)
550 local function FinishAddLoc(self, args)
551 local mx = 0
553 for z, pl in pairs(self.p) do
554 for i, p in ipairs(pl) do
555 if p[5] > mx then
556 self.location = p
557 mx = p[5]
562 if not self.zones then
563 -- Not using CreateTable, because it will not be released when routing is complete.
564 self.zones = {}
565 else
566 -- We could remove the already known zones, but I'm operating under the assumtion that locations will only be added,
567 -- not removed, so this isn't necessary.
570 -- Remove probably useless locations.
571 for z, pl in pairs(self.p) do
572 local remove_zone = true
573 local i = 1
574 while i <= #pl do
575 if pl[i][5] < mx*0.2 then
576 QuestHelper:ReleaseTable(pl[i])
577 table.remove(pl, i)
578 else
579 remove_zone = false
580 i = i + 1
583 if remove_zone then
584 QuestHelper:ReleaseTable(self.p[z])
585 self.p[z] = nil
586 else
587 self.zones[z.i] = true
591 local node_map = self.nm
592 local node_list = self.nl
594 for list, pl in pairs(self.p) do
595 local dist = self.d[list]
597 assert(not dist)
599 if not dist then
600 dist = QuestHelper:CreateTable("self.d[list]")
601 self.d[list] = dist
604 for i, point in ipairs(pl) do
605 point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
607 point[2] = QuestHelper:CreateTable("possible objective node to zone edge cache")
609 for i, node in ipairs(list) do
610 QuestHelper: Assert(type(point[3]) == "number", string.format("p3 %s", tostring(point[3])))
611 QuestHelper: Assert(type(point[4]) == "number", string.format("p4 %s", tostring(point[4])))
612 QuestHelper: Assert(type(node.x) == "number", string.format("nx %s", tostring(node.x)))
613 QuestHelper: Assert(type(node.y) == "number", string.format("ny %s", tostring(node.y)))
614 local u, v = point[3]-node.x, point[4]-node.y
615 local d = math.sqrt(u*u+v*v)
617 point[2][i] = d
619 if dist[i] then
620 if d*point[5] < dist[i][1]*dist[i][2] then
621 dist[i][1], dist[i][2] = d, point[5]
622 node_map[node] = point
624 else
625 local pair = QuestHelper:CreateTable()
626 pair[1], pair[2] = d, point[5]
627 dist[i] = pair
629 if not node_map[node] then
630 table.insert(node_list, node)
631 node_map[node] = point
632 else
633 u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
635 if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
636 node_map[node] = point
644 -- Disabled because we're having some data sanity issues. This should be solved at buildtime, but I'm leery of mucking with the build system right now, so it isn't. Re-enable later.
645 --if not args or not args.failable then
646 -- if #node_list == 0 and QuestHelper:IsWrath() then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
647 --end
649 assert(not self.setup)
650 self.setup = true
651 table.insert(self.qh.prepared_objectives, self)
654 local function GetPosition(self)
655 assert(self.setup)
657 return self.location
660 local QH_TESTCACHE = nil -- make this "true" or something if you want to test caching (i.e. recalculate everything, then verify that the cache is valid)
662 -- Note: Pos is the starting point, the objective is the destination. These are different data formats - "self" can be a set of points.
663 -- More annotation here, if you're trying to learn the codebase. This function is a more complicated version of QH:ComputeTravelTime, so refer to that for information first before reading this one.
664 local function ObjectiveTravelTime(self, pos, nocache)
665 assert(self.setup)
667 -- The caching is pretty obvious.
668 local key, cached
669 if not nocache then
670 assert(pos ~= QuestHelper.pos)
671 if not pos.key then
672 pos.key = math.random()..""
674 key = pos.key
675 cached = self.distance_cache[key]
676 if cached then
677 if not QH_TESTCACHE then
678 return unpack(cached)
683 local graph = self.qh.world_graph
684 local nl = self.nl
686 graph:PrepareSearch()
688 -- This is quite similar to the same "create nodes for all zone links" in ComputeTravelTime except that it's creating nodes for all zone links for a set of possible destinations. I'm not sure if the weighting is backwards. It might be.
689 for z, l in pairs(self.d) do
690 for i, n in ipairs(z) do
691 if n.s == 0 then
692 n.e, n.w = unpack(l[i])
693 n.s = 3
694 elseif n.e * n.w < l[i][1]*l[i][2] then
695 n.e, n.w = unpack(l[i])
700 local d = pos[2]
701 for i, n in ipairs(pos[1]) do
702 graph:AddStartNode(n, d[i], nl)
705 local e = graph:DoSearch(nl)
707 -- d changes datatype here. I hate this codebase. Hell, e probably changes datatype also! yaaaay. what does .nm mean? what does .d mean?
708 d = e.g+e.e
709 e = self.nm[e]
711 -- There's something going on with weighting here that I don't understand
712 local l = self.p[pos[1]]
713 if l then
714 local x, y = pos[3], pos[4]
715 local score = d*e[5]
717 for i, n in ipairs(l) do
718 local u, v = x-n[3], y-n[4]
719 local d2 = math.sqrt(u*u+v*v)
720 local s = d2*n[5]
721 if s < score then
722 d, e, score = d2, n, s
727 assert(e)
728 if not nocache then
729 assert( not cached or (cached[1] == d and cached[2] == e))
730 if not QH_TESTCACHE or not cached then
731 local new = self.qh:CreateTable()
732 new[1], new[2] = d, e
733 self.distance_cache[key] = new
734 self.qh:CacheRegister(self)
736 else
737 if self.distance_cache and self.distance_cache[key] then
738 assert(self.distance_cache[key][1] == d)
742 return d, e
745 -- Note: pos1 is the starting point, pos2 is the ending point, the objective is somewhere between them.
746 -- Yet more annotation! This one is based off ObjectiveTravelTime. Yes, it's nasty that there are three (edit: four) functions with basically the same goal. Have I mentioned this codebase kind of sucks?
747 local function ObjectiveTravelTime2(self, pos1, pos2, nocache)
748 assert(self.setup)
750 -- caching is pretty simple as usual
751 local key, cached
752 if not nocache then
753 assert(pos1 ~= QuestHelper.pos)
754 assert(pos2 ~= QuestHelper.pos)
755 -- We don't want to cache distances involving the player's current position, as that would spam the table
756 if not pos1.key then
757 pos1.key = math.random()..""
759 if not pos2.key then
760 pos2.key = math.random()..""
762 key = pos1.key..pos2.key
763 cached = self.distance_cache[key]
764 if cached then
765 if not QH_TESTCACHE then
766 return unpack(cached)
771 local graph = self.qh.world_graph
772 local nl = self.nl
774 -- This is the standard pos1-to-self code that we're used to seeing . . .
775 graph:PrepareSearch()
777 for z, l in pairs(self.d) do
778 for i, n in ipairs(z) do
779 if n.s == 0 then
780 n.e, n.w = unpack(l[i])
781 n.s = 3
782 elseif n.e * n.w < l[i][1]*l[i][2] then
783 n.e, n.w = unpack(l[i])
788 local d = pos1[2]
789 for i, n in ipairs(pos1[1]) do
790 graph:AddStartNode(n, d[i], nl)
793 graph:DoFullSearch(nl)
795 graph:PrepareSearch()
797 -- . . . and here's where it gets wonky
798 -- Now, we need to figure out how long it takes to get to each node.
799 for z, point_list in pairs(self.p) do
800 if z == pos1[1] then
801 -- Will also consider min distance.
802 local x, y = pos1[3], pos1[4]
804 for i, p in ipairs(point_list) do
805 local a, b = p[3]-x, p[4]-y
806 local u, v = p[3], p[4]
807 local d = math.sqrt(a*a+b*b)
808 local w = p[5]
809 local score = d*w
810 for i, n in ipairs(z) do
811 a, b = n.x-u, n.y-v
812 local bleh = math.sqrt(a*a+b*b)+n.g
813 local s = bleh*w
814 if s < score then
815 d, score = bleh, d
818 p[7] = d
820 else
821 for i, p in ipairs(point_list) do
822 local x, y = p[3], p[4]
823 local w = p[5]
824 local d
825 local score
827 for i, n in ipairs(z) do
828 local a, b = n.x-x, n.y-y
829 local d2 = math.sqrt(a*a+b*b)+n.g
830 local s = d2*w
831 if not score or s < score then
832 d, score = d2, s
835 p[7] = d
840 d = pos2[2]
842 for i, n in ipairs(pos2[1]) do
843 n.e = d[i]
844 n.s = 3
847 local el = pos2[1]
848 local nm = self.nm2
850 for z, l in pairs(self.d) do
851 for i, n in ipairs(z) do
852 local x, y = n.x, n.y
853 local bp
854 local bg
855 local bs
856 for i, p in ipairs(self.p[z]) do
857 local a, b = x-p[3], y-p[4]
858 d = p[7]+math.sqrt(a*a+b*b)
859 s = d*p[5]
860 if not bs or s < bs then
861 bg, bp, bs = d, p, s
865 nm[n] = bp
866 -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
867 graph:AddStartNode(n, bs, el)
871 local e = graph:DoSearch(pos2[1])
873 d = nm[e.p][7]
874 local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
876 e = nm[e.p]
877 local total = (d+d2)*e[5]
879 if self.p[el] then
880 local x, y = pos2[3], pos2[4]
881 for i, p in ipairs(self.p[el]) do
882 local a, b = x-p[3], y-p[4]
883 local c = math.sqrt(a*a+b*b)
884 local t = (p[7]+c)*p[5]
885 if t < total then
886 total, d, d2, e = t, p[7], c, p
891 -- grim stabilization hack, since obviously the numbers it generates are only vaguely based in reality. This should be fixed and removed ASAP (you know, once I figure out WTF this thing is doing)
892 d = QuestHelper:ComputeTravelTime(pos1, e)
893 d2 = QuestHelper:ComputeTravelTime(e, pos2)
895 assert(e)
896 if not nocache then
897 assert( not cached or (cached[1] == d and cached[2] == d2 and cached[3] == e))
898 if not QH_TESTCACHE or not cached then
899 local new = self.qh:CreateTable("ObjectiveTravelTime2 cache")
900 new[1], new[2], new[3] = d, d2, e
901 self.distance_cache[key] = new
902 self.qh:CacheRegister(self)
904 else
905 if self.distance_cache and self.distance_cache[key] then
906 assert(self.distance_cache[key][1] == d and self.distance_cache[key][2] == d2)
910 --[[if pos1 and pos2 then -- Debug code so I can maybe actually fix the problems someday
911 QuestHelper:TextOut("Beginning dumping here")
913 local laxa = QuestHelper:ComputeTravelTime(pos1, e, true)
914 if math.abs(laxa-d) >= 0.0001 then
915 QuestHelper:TextOut(QuestHelper:StringizeTable(pos1))
916 QuestHelper:TextOut(QuestHelper:StringizeRecursive(pos1, 2))
917 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
918 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
919 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
920 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
921 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxa.." vs "..d) -- wonky commenting is thanks to the de-assert script, fix later
922 --[[end
923 local laxb = QuestHelper:ComputeTravelTime(e, pos2, true)
924 if math.abs(laxb-d2) >= 0.0001 then
925 QuestHelper:TextOut(QuestHelper:StringizeTable(pos2))
926 QuestHelper:TextOut(QuestHelper:StringizeTable(e))
927 QuestHelper:TextOut(QuestHelper:StringizeTable(e[1]))
928 QuestHelper:TextOut(QuestHelper:StringizeTable(e[2]))
929 QuestHelper:TextOut(QuestHelper:StringizeRecursive(e[1], 2))]]
930 --QuestHelper:Assert(math.abs(laxa-d) < 0.0001, "Compare: "..laxb.." vs "..d2)
931 --[[end
932 end]]
934 return d, d2, e
937 local function DoneRouting(self)
938 assert(self.setup_count > 0)
939 assert(self.setup)
941 if self.setup_count == 1 then
942 self.setup_count = 0
943 QuestHelper:ReleaseObjectivePathingInfo(self)
944 for i, obj in ipairs(self.qh.prepared_objectives) do
945 if o == obj then
946 table.remove(self.qh.prepared_objectives, i)
947 break
950 else
951 self.setup_count = self.setup_count - 1
955 local function IsObjectiveWatched(self)
956 -- Check if an objective is being watched. Note that this is an external query, not a simple Selector.
957 local info
959 if self.cat == "quest" then
960 info = QuestHelper.quest_log[self]
961 else
962 info = QuestHelper.quest_log[self.quest]
965 if info then
966 local index = info.index
967 if index then
968 if UberQuest then
969 -- UberQuest has it's own way of tracking quests.
970 local uq_settings = UberQuest_Config[UnitName("player")]
971 if uq_settings then
972 local list = uq_settings.selected
973 if list then
974 return list[GetQuestLogTitle(index)]
977 else
978 return IsQuestWatched(index)
983 return false
987 local next_objective_id = 0
989 local function ObjectiveShare(self)
990 self.want_share = true
993 local function ObjectiveUnshare(self)
994 self.want_share = false
997 QuestHelper.default_objective_param =
999 CouldBeFirst=ObjectiveCouldBeFirst,
1001 Uses=Uses,
1002 DoMarkUsed=DoMarkUsed,
1003 MarkUsed=MarkUsed,
1004 MarkUnused=MarkUnused,
1006 DefaultKnown=DefaultObjectiveKnown,
1007 Known=DummyObjectiveKnown,
1008 Reason=ObjectiveReason,
1010 AppendPositions=ObjectiveAppendPositions,
1011 PrepareRouting=ObjectivePrepareRouting,
1012 AddLoc=AddLoc,
1013 FinishAddLoc=FinishAddLoc,
1014 DoneRouting=DoneRouting,
1016 Position=GetPosition,
1017 TravelTime=ObjectiveTravelTime,
1018 TravelTime2=ObjectiveTravelTime2,
1020 IsWatched=IsObjectiveWatched,
1022 Share=ObjectiveShare, -- Invoke to share this objective with your peers.
1023 Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
1026 QuestHelper.default_objective_item_param =
1028 Known = ItemKnown,
1029 AppendPositions = ItemAppendPositions,
1030 DoMarkUsed = ItemDoMarkUsed
1033 for key, value in pairs(QuestHelper.default_objective_param) do
1034 if not QuestHelper.default_objective_item_param[key] then
1035 QuestHelper.default_objective_item_param[key] = value
1039 QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
1040 QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }
1042 function QuestHelper:NewObjectiveObject()
1043 next_objective_id = next_objective_id+1
1044 return
1045 setmetatable({
1046 qh=self,
1047 id=next_objective_id,
1049 want_share=false, -- True if we want this objective shared.
1050 is_sharing=false, -- Set to true if we've told other users about this objective.
1052 user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
1054 priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
1055 real_priority=3, -- This will be set to the priority routing actually decided to assign it.
1057 setup_count=0,
1059 icon_id=12,
1060 icon_bg=14,
1062 match_zone=false,
1063 match_level=false,
1064 match_done=false,
1066 before={}, -- List of objectives that this objective must appear before.
1067 after={}, -- List of objectives that this objective must appear after.
1069 -- Routing related junk.
1071 --[[ Will be created as needed.
1072 d=nil,
1073 p=nil,
1074 nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
1075 nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
1076 nl=nil, -- List of all the nodes we need to consider.
1077 location=nil, -- Will be set to the best position for the node.
1078 pos=nil, -- Zone node list, distance list, x, y, reason.
1079 sop=nil ]]
1080 }, QuestHelper.default_objective_meta)
1083 local explicit_support_warning_given = false
1085 function QuestHelper:GetObjective(category, objective)
1086 local objective_list = self.objective_objects[category]
1088 if not objective_list then
1089 objective_list = {}
1090 self.objective_objects[category] = objective_list
1093 local objective_object = objective_list[objective]
1095 if not objective_object then
1096 if category == "quest" then
1097 local level, hash, name = string.match(objective, "^(%d+)/(%d*)/(.*)$")
1098 if not level then
1099 level, name = string.match(objective, "^(%d+)/(.*)$")
1100 if not level then
1101 name = objective
1105 if hash == "" then hash = nil end
1106 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
1107 objective_list[objective] = objective_object
1108 return objective_object
1111 objective_object = self:NewObjectiveObject()
1113 objective_object.cat = category
1114 objective_object.obj = objective
1116 if category == "item" then
1117 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
1118 objective_object.icon_id = 2
1119 elseif category == "monster" then
1120 objective_object.icon_id = 1
1121 elseif category == "object" then
1122 objective_object.icon_id = 3
1123 elseif category == "event" then
1124 objective_object.icon_id = 4
1125 elseif category == "loc" then
1126 objective_object.icon_id = 6
1127 elseif category == "reputation" then
1128 objective_object.icon_id = 5
1129 elseif category == "player" then
1130 objective_object.icon_id = 1 -- not ideal, will improve later
1131 else
1132 if not explicit_support_warning_given then
1133 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
1134 explicit_support_warning_given = true
1138 objective_list[objective] = objective_object
1140 if category == "loc" then
1141 -- Loc is special, we don't store it, and construct it from the string.
1142 -- Don't have any error checking here, will assume it's correct.
1143 local i
1144 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
1146 if not y then
1147 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
1148 else
1149 i = QuestHelper_IndexLookup[c][z]
1152 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
1153 objective_object.fb = {}
1154 else
1155 objective_list = QuestHelper_Objectives_Local[category]
1156 if not objective_list then
1157 objective_list = {}
1158 QuestHelper_Objectives_Local[category] = objective_list
1160 objective_object.o = objective_list[objective]
1161 if not objective_object.o then
1162 objective_object.o = {}
1163 objective_list[objective] = objective_object.o
1165 local l = QuestHelper_StaticData[self.locale]
1166 if l then
1167 objective_list = l.objective[category]
1168 if objective_list then
1169 objective_object.fb = objective_list[objective]
1172 if not objective_object.fb then
1173 objective_object.fb = {}
1176 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
1181 return objective_object
1184 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
1185 if not i then return end -- We don't have a player position. We have a pile of poop. Enjoy your poop.
1187 local pos = objective.o.pos
1188 if not pos then
1189 if objective.o.drop or objective.o.contained then
1190 return -- If it's dropped by a monster, don't record the position we got the item at.
1192 objective.o.pos = self:AppendPosition({}, i, x, y, w)
1193 else
1194 self:AppendPosition(pos, i, x, y, w)
1198 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
1199 local drop = objective.o.drop
1200 if drop then
1201 drop[monster] = (drop[monster] or 0)+(count or 1)
1202 else
1203 objective.o.drop = {[monster] = count or 1}
1204 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
1208 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
1209 local quest = self:ItemIsForQuest(item_object, item_name)
1210 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1211 self:AppendQuestDrop(quest, item_name, monster_name, count)
1212 else
1213 if not item_object.o.drop and not item_object.o.pos then
1214 self:PurgeQuestItem(item_object, item_name)
1216 self:AppendObjectiveDrop(item_object, monster_name, count)
1220 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1221 local quest = self:ItemIsForQuest(item_object, item_name)
1222 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1223 self:AppendQuestPosition(quest, item_name, i, x, y)
1224 else
1225 if not item_object.o.vendor and not item_object.o.drop and not item_object.o.contained and not item_object.o.pos then
1226 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1227 self:PurgeQuestItem(item_object, item_name)
1229 self:AppendObjectivePosition(item_object, i, x, y)
1233 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1234 local container = objective.o.contained
1235 if container then
1236 container[container_name] = (container[container_name] or 0)+(count or 1)
1237 else
1238 objective.o.contained = {[container_name] = count or 1}
1239 objective.o.pos = nil -- Forget the position.
1243 function QuestHelper:AddObjectiveWatch(objective, reason)
1244 if not objective.reasons then
1245 objective.reasons = {}
1248 if not next(objective.reasons, nil) then
1249 objective.watched = true
1250 objective:MarkUsed()
1252 objective.filter_blocked = false
1253 for obj in pairs(objective.swap_after or objective.after) do
1254 if obj.watched then
1255 objective.filter_blocked = true
1256 break
1260 for obj in pairs(objective.swap_before or objective.before) do
1261 if obj.watched then
1262 obj.filter_blocked = true
1266 if self.to_remove[objective] then
1267 self.to_remove[objective] = nil
1268 else
1269 self.to_add[objective] = true
1273 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1276 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1277 if objective.reasons[reason] == 1 then
1278 objective.reasons[reason] = nil
1279 if not next(objective.reasons, nil) then
1280 objective:MarkUnused()
1281 objective.watched = false
1283 for obj in pairs(objective.swap_before or objective.before) do
1284 if obj.watched then
1285 obj.filter_blocked = false
1286 for obj2 in pairs(obj.swap_after or obj.after) do
1287 if obj2.watched then
1288 obj.filter_blocked = true
1289 break
1295 if self.to_add[objective] then
1296 self.to_add[objective] = nil
1297 else
1298 self.to_remove[objective] = true
1301 else
1302 objective.reasons[reason] = objective.reasons[reason] - 1
1306 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1307 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1308 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1311 -- We store the new relationships in objective.swap_[before|after],
1312 -- creating and copying them from objective.[before|after],
1313 -- the routing coroutine will check for those, swap them, and release the originals
1314 -- when it gets to a safe place to do so.
1316 if not (objective.swap_after or objective.after)[needs] then
1317 if objective.peer then
1318 for u, l in pairs(objective.peer) do
1319 -- Make sure other users know that the dependencies for this objective changed.
1320 objective.peer[u] = math.min(l, 1)
1324 if not objective.swap_after then
1325 objective.swap_after = self:CreateTable("swap_after")
1326 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1329 if not needs.swap_before then
1330 needs.swap_before = self:CreateTable("swap_before")
1331 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1334 if needs.watched then
1335 objective.filter_blocked = true
1338 objective.swap_after[needs] = true
1339 needs.swap_before[objective] = true
1343 function QuestHelper:IgnoreObjective(objective)
1344 if self.user_objectives[objective] then
1345 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1346 self.user_objectives[objective] = nil
1347 else
1348 objective.user_ignore = true
1351 --self:ForceRouteUpdate()
1354 function QuestHelper:SetObjectivePriority(objective, level)
1355 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1356 if level ~= objective.priority then
1357 objective.priority = level
1358 if objective.peer then
1359 for u, l in pairs(objective.peer) do
1360 -- Peers don't know about this new priority.
1361 objective.peer[u] = math.min(l, 2)
1364 --self:ForceRouteUpdate()
1368 local function CalcObjectivePriority(obj)
1369 local priority = obj.priority
1371 for o in pairs(obj.before) do
1372 if o.watched then
1373 priority = math.min(priority, CalcObjectivePriority(o))
1377 return priority
1380 local function ApplyBlockPriority(obj, level)
1381 for o in pairs(obj.before) do
1382 if o.watched then
1383 ApplyBlockPriority(o, level)
1387 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1390 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1391 self:SetObjectivePriority(objective, level)
1392 if CalcObjectivePriority(objective) ~= level then
1393 local menu = self:CreateMenu()
1394 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1395 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1396 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1397 menu:ShowAtCursor()
1401 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1402 if have and need then
1403 local list = objective.progress
1404 if not list then
1405 list = self:CreateTable("objective.progress")
1406 objective.progress = list
1409 local user_progress = list[user]
1410 if not user_progress then
1411 user_progress = self:CreateTable("objective.progress[user]")
1412 list[user] = user_progress
1415 local pct = 0
1416 local a, b = tonumber(have), tonumber(need)
1417 if a and b then
1418 if b ~= 0 then
1419 pct = a/b
1420 elseif a == 0 then
1421 pct = 1
1423 elseif a == b then
1424 pct = 1
1427 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1428 else
1429 if objective.progress then
1430 if objective.progress[user] then
1431 self:ReleaseTable(objective.progress[user])
1432 objective.progress[user] = nil
1434 if not next(objective.progress, nil) then
1435 self:ReleaseTable(objective.progress)
1436 objective.progress = nil