Merge branch 'master' of git+ssh://questhelper@192.168.2.1/server/git/QuestHelper
[QuestHelper.git] / objective.lua
blob8e9aa23565fa5d617a7fd052ce0ed7d8dca7bc66
1 QuestHelper_File["objective.lua"] = "Development Version"
3 local function ObjectiveCouldBeFirst(self)
4 if (self.user_ignore == nil and self.auto_ignore) or self.user_ignore then
5 return false
6 end
8 for i, j in pairs(self.after) do
9 if i.watched then
10 return false
11 end
12 end
14 return true
15 end
17 local function DefaultObjectiveKnown(self)
18 if self.user_ignore == nil then
19 if (self.filter_zone and QuestHelper_Pref.filter_zone) or
20 (self.filter_done and QuestHelper_Pref.filter_done) or
21 (self.filter_level and QuestHelper_Pref.filter_level) or
22 (self.filter_blocked and QuestHelper_Pref.filter_blocked) then
23 return false
24 end
25 elseif self.user_ignore then
26 return false
27 end
29 for i, j in pairs(self.after) do
30 if i.watched and not i:Known() then -- Need to know how to do everything before this objective.
31 return false
32 end
33 end
35 return true
36 end
38 local function ObjectiveReason(self, short)
39 local reason, rc = nil, 0
40 if self.reasons then
41 for r, c in pairs(self.reasons) do
42 if not reason or c > rc or (c == rc and r > reason) then
43 reason, rc = r, c
44 end
45 end
46 end
48 if not reason then reason = "Do some extremely secret unspecified something." end
50 if not short and self.pos and self.pos[6] then
51 reason = reason .. "\n" .. self.pos[6]
52 end
54 return reason
55 end
57 local function Uses(self, obj, text)
58 assert(self ~= obj)
59 local uses, used = self.uses, obj.used
61 if not uses then
62 uses = QuestHelper:CreateTable()
63 self.uses = uses
64 end
66 if not used then
67 used = QuestHelper:CreateTable()
68 obj.used = used
69 end
71 if not uses[obj] then
72 uses[obj] = true
73 used[self] = text
74 obj:MarkUsed()
75 end
76 end
78 local function DoMarkUsed(self)
79 -- Objectives should call 'self:Uses(objective, text)' to mark objectives they use by don't directly depend on.
80 -- This information is used in tooltips.
81 -- text is passed to QHFormat with the name of the objective being used.
82 end
84 local function MarkUsed(self)
85 if not self.marked_used then
86 self.marked_used = 1
87 self:DoMarkUsed()
88 else
89 self.marked_used = self.marked_used + 1
90 end
91 end
93 local function MarkUnused(self)
94 assert(self.marked_used)
96 if self.marked_used == 1 then
97 local uses = self.uses
99 if uses then
100 for obj in pairs(uses) do
101 obj.used[self] = nil
102 obj:MarkUnused()
105 QuestHelper:ReleaseTable(uses)
106 self.uses = nil
109 if self.used then
110 assert(not next(self.used))
111 QuestHelper:ReleaseTable(self.used)
112 self.used = nil
115 self.marked_used = nil
116 else
117 self.marked_used = self.marked_used - 1
121 local function DummyObjectiveKnown(self)
122 return (self.o.pos or self.fb.pos) and DefaultObjectiveKnown(self)
125 local function ItemKnown(self)
126 if not DefaultObjectiveKnown(self) then return false end
128 if self.o.vendor then
129 for i, npc in ipairs(self.o.vendor) do
130 local n = self.qh:GetObjective("monster", npc)
131 local faction = n.o.faction or n.fb.faction
132 if (not faction or faction == self.qh.faction) and n:Known() then
133 return true
138 if self.fb.vendor then
139 for i, npc in ipairs(self.fb.vendor) do
140 local n = self.qh:GetObjective("monster", npc)
141 local faction = n.o.faction or n.fb.faction
142 if (not faction or faction == self.qh.faction) and n:Known() then
143 return true
148 if self.o.pos or self.fb.pos then
149 return true
152 if self.o.drop then for monster in pairs(self.o.drop) do
153 if self.qh:GetObjective("monster", monster):Known() then
154 return true
156 end end
158 if self.fb.drop then for monster in pairs(self.fb.drop) do
159 if self.qh:GetObjective("monster", monster):Known() then
160 return true
162 end end
164 if self.o.contained then for item in pairs(self.o.contained) do
165 if self.qh:GetObjective("item", item):Known() then
166 return true
168 end end
170 if self.fb.contained then for item in pairs(self.fb.contained) do
171 if self.qh:GetObjective("item", item):Known() then
172 return true
174 end end
176 if self.quest then
177 local item=self.quest.o.item
178 item = item and item[self.obj]
180 if item then
181 if item.pos then
182 return true
184 if item.drop then
185 for monster in pairs(item.drop) do
186 if self.qh:GetObjective("monster", monster):Known() then
187 return true
193 item=self.quest.fb.item
194 item = item and item[self.obj]
195 if item then
196 if item.pos then
197 return true
199 if item.drop then
200 for monster in pairs(item.drop) do
201 if self.qh:GetObjective("monster", monster):Known() then
202 return true
209 return false
212 local function ObjectiveAppendPositions(self, objective, weight, why)
213 local high = 0
215 if self.o.pos then for i, p in ipairs(self.o.pos) do
216 high = math.max(high, p[4])
217 end end
219 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
220 high = math.max(high, p[4])
221 end end
223 high = weight/high
225 if self.o.pos then for i, p in ipairs(self.o.pos) do
226 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
227 end end
229 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
230 objective:AddLoc(p[1], p[2], p[3], p[4]*high, why)
231 end end
235 local function ObjectivePrepareRouting(self)
236 self.setup_count = self.setup_count + 1
237 if not self.setup then
238 assert(not self.d)
239 assert(not self.p)
240 assert(not self.nm)
241 assert(not self.nm2)
242 assert(not self.nl)
244 self.d = QuestHelper:CreateTable()
245 self.p = QuestHelper:CreateTable()
246 self.nm = QuestHelper:CreateTable()
247 self.nm2 = QuestHelper:CreateTable()
248 self.nl = QuestHelper:CreateTable()
249 self.distance_cache = QuestHelper:CreateTable()
251 self:AppendPositions(self, 1, nil)
252 self:FinishAddLoc()
256 local function ItemAppendPositions(self, objective, weight, why)
257 why2 = why and why.."\n" or ""
259 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
260 local n = self.qh:GetObjective("monster", npc)
261 local faction = n.o.faction or n.fb.faction
262 if (not faction or faction == self.qh.faction) then
263 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc))
265 end end
267 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
268 local n = self.qh:GetObjective("monster", npc)
269 local faction = n.o.faction or n.fb.faction
270 if (not faction or faction == self.qh.faction) then
271 n:AppendPositions(objective, 1, why2..QHFormat("OBJECTIVE_PURCHASE", npc))
273 end end
275 if next(objective.p, nil) then
276 -- If we have points from vendors, then always use vendors. I don't want it telling you to killing the
277 -- towns people just because you had to talk to them anyway, and it saves walking to the store.
278 return
281 if self.o.drop then for monster, count in pairs(self.o.drop) do
282 local m = self.qh:GetObjective("monster", monster)
283 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
284 end end
286 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
287 local m = self.qh:GetObjective("monster", monster)
288 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
289 end end
291 if self.o.contained then for item, count in pairs(self.o.contained) do
292 local i = self.qh:GetObjective("item", item)
293 i:AppendPositions(objective, i.o.opened and count/i.o.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item))
294 end end
296 if self.fb.contained then for item, count in pairs(self.fb.contained) do
297 local i = self.qh:GetObjective("item", item)
298 i:AppendPositions(objective, i.fb.opened and count/i.fb.opened or 1, why2..QHFormat("OBJECTIVE_LOOT", item))
299 end end
301 if self.o.pos then for i, p in ipairs(self.o.pos) do
302 objective:AddLoc(p[1], p[2], p[3], p[4], why)
303 end end
305 if self.fb.pos then for i, p in ipairs(self.fb.pos) do
306 objective:AddLoc(p[1], p[2], p[3], p[4], why)
307 end end
309 if self.quest then
310 local item_list=self.quest.o.item
311 if item_list then
312 local data = item_list[self.obj]
313 if data and data.drop then
314 for monster, count in pairs(data.drop) do
315 local m = self.qh:GetObjective("monster", monster)
316 m:AppendPositions(objective, m.o.looted and count/m.o.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
318 elseif data and data.pos then
319 for i, p in ipairs(data.pos) do
320 objective:AddLoc(p[1], p[2], p[3], p[4], why)
325 item_list=self.quest.fb.item
326 if item_list then
327 local data = item_list[self.obj]
328 if data and data.drop then
329 for monster, count in pairs(data.drop) do
330 local m = self.qh:GetObjective("monster", monster)
331 m:AppendPositions(objective, m.fb.looted and count/m.fb.looted or 1, why2..QHFormat("OBJECTIVE_SLAY", monster))
333 elseif data and data.pos then
334 for i, p in ipairs(data.pos) do
335 objective:AddLoc(p[1], p[2], p[3], p[4], why)
342 local function ItemDoMarkUsed(self)
343 if self.o.vendor then for i, npc in ipairs(self.o.vendor) do
344 local n = self.qh:GetObjective("monster", npc)
345 local faction = n.o.faction or n.fb.faction
346 if (not faction or faction == self.qh.faction) then
347 self:Uses(n, "TOOLTIP_PURCHASE")
349 end end
351 if self.fb.vendor then for i, npc in ipairs(self.fb.vendor) do
352 local n = self.qh:GetObjective("monster", npc)
353 local faction = n.o.faction or n.fb.faction
354 if (not faction or faction == self.qh.faction) then
355 self:Uses(n, "TOOLTIP_PURCHASE")
357 end end
359 if self.o.drop then for monster, count in pairs(self.o.drop) do
360 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
361 end end
363 if self.fb.drop then for monster, count in pairs(self.fb.drop) do
364 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
365 end end
367 if self.o.contained then for item, count in pairs(self.o.contained) do
368 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
369 end end
371 if self.fb.contained then for item, count in pairs(self.fb.contained) do
372 self:Uses(self.qh:GetObjective("item", item), "TOOLTIP_LOOT")
373 end end
375 if self.quest then
376 local item_list=self.quest.o.item
377 if item_list then
378 local data = item_list[self.obj]
379 if data and data.drop then
380 for monster, count in pairs(data.drop) do
381 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
386 item_list=self.quest.fb.item
387 if item_list then
388 local data = item_list[self.obj]
389 if data and data.drop then
390 for monster, count in pairs(data.drop) do
391 self:Uses(self.qh:GetObjective("monster", monster), "TOOLTIP_SLAY")
407 ---------------
421 local function AddLoc(self, index, x, y, w, why)
422 assert(not self.setup)
424 if w > 0 then
425 local pair = QuestHelper_ZoneLookup[index]
426 local c, z = pair[1], pair[2]
427 x, y = self.qh.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
429 x = x * self.qh.continent_scales_x[c]
430 y = y * self.qh.continent_scales_y[c]
431 local list = self.qh.zone_nodes[index]
433 local points = self.p[list]
434 if not points then
435 points = QuestHelper:CreateTable()
436 self.p[list] = points
439 for i, p in pairs(points) do
440 local u, v = x-p[3], y-p[4]
441 if u*u+v*v < 25 then -- Combine points within a threshold of 5 seconds travel time.
442 p[3] = (p[3]*p[5]+x*w)/(p[5]+w)
443 p[4] = (p[4]*p[5]+y*w)/(p[5]+w)
444 p[5] = p[5]+w
445 if w > p[7] then
446 p[6], p[7] = why, w
448 return
452 local new = QuestHelper:CreateTable()
453 new[1], new[2], new[3], new[4], new[5], new[6], new[7] = list, nil, x, y, w, why, w
454 table.insert(points, new)
458 local function FinishAddLoc(self)
459 local mx = 0
461 for z, pl in pairs(self.p) do
462 for i, p in ipairs(pl) do
463 if p[5] > mx then
464 self.location = p
465 mx = p[5]
470 -- Remove probably useless locations.
471 for z, pl in pairs(self.p) do
472 local remove_zone = true
473 local i = 1
474 while i <= #pl do
475 if pl[i][5] < mx*0.2 then
476 QuestHelper:ReleaseTable(pl[i])
477 table.remove(pl, i)
478 else
479 remove_zone = false
480 i = i + 1
483 if remove_zone then
484 QuestHelper:ReleaseTable(self.p[z])
485 self.p[z] = nil
489 local node_map = self.nm
490 local node_list = self.nl
492 for list, pl in pairs(self.p) do
493 local dist = self.d[list]
495 assert(not dist)
497 if not dist then
498 dist = QuestHelper:CreateTable()
499 self.d[list] = dist
502 for i, point in ipairs(pl) do
503 point[5] = mx/point[5] -- Will become 1 for the most desired location, and become larger and larger for less desireable locations.
505 point[2] = QuestHelper:CreateTable()
507 for i, node in ipairs(list) do
508 local u, v = point[3]-node.x, point[4]-node.y
509 local d = math.sqrt(u*u+v*v)
511 point[2][i] = d
513 if dist[i] then
514 if d*point[5] < dist[i][1]*dist[i][2] then
515 dist[i][1], dist[i][2] = d, point[5]
516 node_map[node] = point
518 else
519 local pair = QuestHelper:CreateTable()
520 pair[1], pair[2] = d, point[5]
521 dist[i] = pair
523 if not node_map[node] then
524 table.insert(node_list, node)
525 node_map[node] = point
526 else
527 u, v = node_map[node][3]-node.x, node_map[node][4]-node.y
529 if dist[i][1]*dist[i][2] < math.sqrt(u*u+v*v)*node_map[node][5] then
530 node_map[node] = point
538 if #node_list == 0 then QuestHelper:Error(self.cat.."/"..self.obj..": zero nodes!") end
540 assert(not self.setup)
541 self.setup = true
542 table.insert(self.qh.prepared_objectives, self)
545 local function GetPosition(self)
546 assert(self.setup)
548 return self.location
551 local function ComputeTravelTime(self, pos, nocache)
552 assert(self.setup)
554 local key
555 if not nocache then
556 if not pos.key then
557 pos.key = math.random()..""
559 key = pos.key
560 if self.distance_cache[key] then
561 return unpack(self.distance_cache[key])
565 local graph = self.qh.world_graph
566 local nl = self.nl
568 graph:PrepareSearch()
570 for z, l in pairs(self.d) do
571 for i, n in ipairs(z) do
572 if n.s == 0 then
573 n.e, n.w = unpack(l[i])
574 n.s = 3
575 elseif n.e * n.w < l[i][1]*l[i][2] then
576 n.e, n.w = unpack(l[i])
581 local d = pos[2]
582 for i, n in ipairs(pos[1]) do
583 graph:AddStartNode(n, d[i], nl)
586 local e = graph:DoSearch(nl)
588 d = e.g+e.e
589 e = self.nm[e]
591 local l = self.p[pos[1]]
592 if l then
593 local x, y = pos[3], pos[4]
594 local score = d*e[5]
596 for i, n in ipairs(l) do
597 local u, v = x-n[3], y-n[4]
598 local d2 = math.sqrt(u*u+v*v)
599 local s = d2*n[5]
600 if s < score then
601 d, e, score = d2, n, s
606 assert(e)
607 if not nocache then
608 local new = self.qh:CreateTable()
609 new[1], new[2] = d, e
610 self.distance_cache[key] = new
612 return d, e
615 local function ComputeTravelTime2(self, pos1, pos2, nocache)
616 assert(self.setup)
618 local key
619 if not nocache then
620 -- We don't want to cache distances involving the player's current position, as that would spam the table
621 if not pos1.key then
622 pos1.key = math.random()..""
624 if not pos2.key then
625 pos2.key = math.random()..""
627 key = pos1.key..pos2.key
628 if self.distance_cache[key] then
629 return unpack(self.distance_cache[key])
633 local graph = self.qh.world_graph
634 local nl = self.nl
636 graph:PrepareSearch()
638 for z, l in pairs(self.d) do
639 for i, n in ipairs(z) do
640 if n.s == 0 then
641 n.e, n.w = unpack(l[i])
642 n.s = 3
643 elseif n.e * n.w < l[i][1]*l[i][2] then
644 n.e, n.w = unpack(l[i])
649 local d = pos1[2]
650 for i, n in ipairs(pos1[1]) do
651 graph:AddStartNode(n, d[i], nl)
654 graph:DoFullSearch(nl)
656 graph:PrepareSearch()
658 -- Now, we need to figure out how long it takes to get to each node.
659 for z, point_list in pairs(self.p) do
660 if z == pos1[1] then
661 -- Will also consider min distance.
662 local x, y = pos1[3], pos1[4]
664 for i, p in ipairs(point_list) do
665 local a, b = p[3]-x, p[4]-y
666 local u, v = p[3], p[4]
667 local d = math.sqrt(a*a+b*b)
668 local w = p[5]
669 local score = d*w
670 for i, n in ipairs(z) do
671 a, b = n.x-u, n.y-v
672 local bleh = math.sqrt(a*a+b*b)+n.g
673 local s = bleh*w
674 if s < score then
675 d, score = bleh, d
678 p[7] = d
680 else
681 for i, p in ipairs(point_list) do
682 local x, y = p[3], p[4]
683 local w = p[5]
684 local d
685 local score
687 for i, n in ipairs(z) do
688 local a, b = n.x-x, n.y-y
689 local d2 = math.sqrt(a*a+b*b)+n.g
690 local s = d2*w
691 if not score or s < score then
692 d, score = d2, s
695 p[7] = d
700 d = pos2[2]
702 for i, n in ipairs(pos2[1]) do
703 n.e = d[i]
704 n.s = 3
707 local el = pos2[1]
708 local nm = self.nm2
710 for z, l in pairs(self.d) do
711 for i, n in ipairs(z) do
712 local x, y = n.x, n.y
713 local bp
714 local bg
715 local bs
716 for i, p in ipairs(self.p[z]) do
717 local a, b = x-p[3], y-p[4]
718 d = p[7]+math.sqrt(a*a+b*b)
719 s = d*p[5]
720 if not bs or s < bs then
721 bg, bp, bs = d, p, s
725 nm[n] = bp
726 -- Using score instead of distance, because we want nodes we're not really interested in to be less likely to get chosen.
727 graph:AddStartNode(n, bs, el)
731 local e = graph:DoSearch(pos2[1])
733 d = nm[e.p][7]
734 local d2 = e.g+e.e-e.p.g+(e.p.g/nm[e.p][5]-nm[e.p][7])
736 e = nm[e.p]
737 local total = (d+d2)*e[5]
739 if self.p[el] then
740 local x, y = pos2[3], pos2[4]
741 for i, p in ipairs(self.p[el]) do
742 local a, b = x-p[3], y-p[4]
743 local c = math.sqrt(a*a+b*b)
744 local t = (p[7]+c)*p[5]
745 if t < total then
746 total, d, d2, e = t, p[7], c, p
751 assert(e)
752 if not nocache then
753 local new = self.qh:CreateTable()
754 new[1], new[2], new[3] = d, d2, e
755 self.distance_cache[key] = new
757 return d, d2, e
760 local function DoneRouting(self)
761 assert(self.setup_count > 0)
762 assert(self.setup)
764 if self.setup_count == 1 then
765 self.setup_count = 0
766 QuestHelper:ReleaseObjectivePathingInfo(self)
767 for i, obj in ipairs(self.qh.prepared_objectives) do
768 if o == obj then
769 table.remove(self.qh.prepared_objectives, i)
770 break
773 else
774 self.setup_count = self.setup_count - 1
778 local next_objective_id = 0
780 local function ObjectiveShare(self)
781 self.want_share = true
784 local function ObjectiveUnshare(self)
785 self.want_share = false
788 QuestHelper.default_objective_param =
790 CouldBeFirst=ObjectiveCouldBeFirst,
792 Uses=Uses,
793 DoMarkUsed=DoMarkUsed,
794 MarkUsed=MarkUsed,
795 MarkUnused=MarkUnused,
797 DefaultKnown=DefaultObjectiveKnown,
798 Known=DummyObjectiveKnown,
799 Reason=ObjectiveReason,
801 AppendPositions=ObjectiveAppendPositions,
802 PrepareRouting=ObjectivePrepareRouting,
803 AddLoc=AddLoc,
804 FinishAddLoc=FinishAddLoc,
805 DoneRouting=DoneRouting,
807 Position=GetPosition,
808 TravelTime=ComputeTravelTime,
809 TravelTime2=ComputeTravelTime2,
811 Share=ObjectiveShare, -- Invoke to share this objective with your peers.
812 Unshare=ObjectiveUnshare, -- Invoke to stop sharing this objective.
815 QuestHelper.default_objective_item_param =
817 Known = ItemKnown,
818 AppendPositions = ItemAppendPositions,
819 DoMarkUsed = ItemDoMarkUsed
822 for key, value in pairs(QuestHelper.default_objective_param) do
823 if not QuestHelper.default_objective_item_param[key] then
824 QuestHelper.default_objective_item_param[key] = value
828 QuestHelper.default_objective_meta = { __index = QuestHelper.default_objective_param }
829 QuestHelper.default_objective_item_meta = { __index = QuestHelper.default_objective_item_param }
831 function QuestHelper:NewObjectiveObject()
832 next_objective_id = next_objective_id+1
833 return
834 setmetatable({
835 qh=self,
836 id=next_objective_id,
838 want_share=false, -- True if we want this objective shared.
839 is_sharing=false, -- Set to true if we've told other users about this objective.
841 user_ignore=nil, -- When nil, will use filters. Will ignore, when true, always show (if known).
843 priority=3, -- A hint as to what priority the quest should have. Should be 1, 2, 3, 4, or 5.
844 real_priority=3, -- This will be set to the priority routing actually decided to assign it.
846 setup_count=0,
848 icon_id=12,
849 icon_bg=14,
851 match_zone=false,
852 match_level=false,
853 match_done=false,
855 before={}, -- List of objectives that this objective must appear before.
856 after={}, -- List of objectives that this objective must appear after.
858 -- Routing related junk.
860 --[[ Will be created as needed.
861 d=nil,
862 p=nil,
863 nm=nil, -- Maps nodes to their nearest zone/list/x/y position.
864 nm2=nil, -- Maps nodes to their nears position, but dynamically set in TravelTime2.
865 nl=nil, -- List of all the nodes we need to consider.
866 location=nil, -- Will be set to the best position for the node.
867 pos=nil, -- Zone node list, distance list, x, y, reason.
868 sop=nil ]]
869 }, QuestHelper.default_objective_meta)
872 function QuestHelper:GetObjective(category, objective)
873 local objective_list = self.objective_objects[category]
875 if not objective_list then
876 objective_list = {}
877 self.objective_objects[category] = objective_list
880 local objective_object = objective_list[objective]
882 if not objective_object then
883 if category == "quest" then
884 local _, _, level, hash, name = string.find(objective, "^(%d+)/(%d*)/(.*)$")
885 if not level then
886 _, _, level, name = string.find(objective, "^(%d+)/(.*)$")
887 if not level then
888 name = objective
892 if hash == "" then hash = nil end
893 objective_object = self:GetQuest(name, tonumber(level), tonumber(hash))
894 objective_list[objective] = objective_object
895 return objective_object
898 objective_object = self:NewObjectiveObject()
900 objective_object.cat = category
901 objective_object.obj = objective
903 if category == "item" then
904 setmetatable(objective_object, QuestHelper.default_objective_item_meta)
905 objective_object.icon_id = 2
906 elseif category == "monster" then
907 objective_object.icon_id = 1
908 elseif category == "object" then
909 objective_object.icon_id = 3
910 elseif category == "event" then
911 objective_object.icon_id = 4
912 elseif category == "loc" then
913 objective_object.icon_id = 6
914 elseif category == "reputation" then
915 objective_object.icon_id = 5
916 else
917 self:TextOut("FIXME: Objective type '"..category.."' for objective '"..objective.."' isn't explicitly supported yet; hopefully the dummy handler will do something sensible.")
920 objective_list[objective] = objective_object
922 if category == "loc" then
923 -- Loc is special, we don't store it, and construct it from the string.
924 -- Don't have any error checking here, will assume it's correct.
925 local i
926 local _, _, c, z, x, y = string.find(objective,"^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
928 if not y then
929 _, _, i, x, y = string.find(objective,"^(%d+),([%d%.]+),([%d%.]+)$")
930 else
931 i = QuestHelper_IndexLookup[c][z]
934 objective_object.o = {pos={{tonumber(i),tonumber(x),tonumber(y),1}}}
935 objective_object.fb = {}
936 else
937 objective_list = QuestHelper_Objectives[category]
938 if not objective_list then
939 objective_list = {}
940 QuestHelper_Objectives[category] = objective_list
942 objective_object.o = objective_list[objective]
943 if not objective_object.o then
944 objective_object.o = {}
945 objective_list[objective] = objective_object.o
947 local l = QuestHelper_StaticData[self.locale]
948 if l then
949 objective_list = l.objective[category]
950 if objective_list then
951 objective_object.fb = objective_list[objective]
954 if not objective_object.fb then
955 objective_object.fb = {}
958 -- TODO: If we have some other source of information (like LightHeaded) add its data to objective_object.fb
963 return objective_object
966 function QuestHelper:AppendObjectivePosition(objective, i, x, y, w)
967 local pos = objective.o.pos
968 if not pos then
969 if objective.o.drop or objective.o.contained then
970 return -- If it's dropped by a monster, don't record the position we got the item at.
972 objective.o.pos = self:AppendPosition({}, i, x, y, w)
973 else
974 self:AppendPosition(pos, i, x, y, w)
978 function QuestHelper:AppendObjectiveDrop(objective, monster, count)
979 local drop = objective.o.drop
980 if drop then
981 drop[monster] = (drop[monster] or 0)+(count or 1)
982 else
983 objective.o.drop = {[monster] = count or 1}
984 objective.o.pos = nil -- If it's dropped by a monster, then forget the position we found it at.
988 function QuestHelper:AppendItemObjectiveDrop(item_object, item_name, monster_name, count)
989 local quest = self:ItemIsForQuest(item_object, item_name)
990 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
991 self:AppendQuestDrop(quest, item_name, monster_name, count)
992 else
993 if not item_object.o.drop and not item_object.o.pos then
994 self:PurgeQuestItem(item_object, item_name)
996 self:AppendObjectiveDrop(item_object, monster_name, count)
1000 function QuestHelper:AppendItemObjectivePosition(item_object, item_name, i, x, y)
1001 local quest = self:ItemIsForQuest(item_object, item_name)
1002 if quest and not item_object.o.vendor and not item_object.o.drop and not item_object.o.pos then
1003 self:AppendQuestPosition(quest, item_name, i, x, y)
1004 else
1005 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
1006 -- Just learned that this item doesn't depend on a quest to drop, remove any quest references to it.
1007 self:PurgeQuestItem(item_object, item_name)
1009 self:AppendObjectivePosition(item_object, i, x, y)
1013 function QuestHelper:AppendItemObjectiveContainer(objective, container_name, count)
1014 local container = objective.o.contained
1015 if container then
1016 container[container_name] = (container[container_name] or 0)+(count or 1)
1017 else
1018 objective.o.contained = {[container_name] = count or 1}
1019 objective.o.pos = nil -- Forget the position.
1023 function QuestHelper:AddObjectiveWatch(objective, reason)
1024 if not objective.reasons then
1025 objective.reasons = {}
1028 if not next(objective.reasons, nil) then
1029 objective.watched = true
1030 objective:MarkUsed()
1032 if self.to_remove[objective] then
1033 self.to_remove[objective] = nil
1034 else
1035 self.to_add[objective] = true
1039 objective.reasons[reason] = (objective.reasons[reason] or 0) + 1
1042 function QuestHelper:RemoveObjectiveWatch(objective, reason)
1043 if objective.reasons[reason] == 1 then
1044 objective.reasons[reason] = nil
1045 if not next(objective.reasons, nil) then
1046 objective:MarkUnused()
1047 objective.watched = false
1049 if self.to_add[objective] then
1050 self.to_add[objective] = nil
1051 else
1052 self.to_remove[objective] = true
1055 else
1056 objective.reasons[reason] = objective.reasons[reason] - 1
1060 function QuestHelper:ObjectiveObjectDependsOn(objective, needs)
1061 assert(objective ~= needs) -- If this was true, ObjectiveIsKnown would get in an infinite loop.
1062 -- TODO: Needs sanity checking, especially now that dependencies can be assigned by remote users.
1065 -- We store the new relationships in objective.swap_[before|after],
1066 -- creating and copying them from objective.[before|after],
1067 -- the routing coroutine will check for those, swap them, and release the originals
1068 -- when it gets to a safe place to do so.
1070 if not (objective.swap_after or objective.after)[needs] then
1071 if objective.peer then
1072 for u, l in pairs(objective.peer) do
1073 -- Make sure other users know that the dependencies for this objective changed.
1074 objective.peer[u] = math.min(l, 1)
1078 if not objective.swap_after then
1079 objective.swap_after = self:CreateTable()
1080 for key,value in pairs(objective.after) do objective.swap_after[key] = value end
1083 if not needs.swap_before then
1084 needs.swap_before = self:CreateTable()
1085 for key,value in pairs(needs.before) do needs.swap_before[key] = value end
1088 objective.swap_after[needs] = true
1089 needs.swap_before[objective] = true
1093 function QuestHelper:AddObjectiveOptionsToMenu(obj, menu)
1094 local submenu = self:CreateMenu()
1096 for i = 1,5 do
1097 local name = QHText("PRIORITY"..i)
1098 local item = self:CreateMenuItem(submenu, name)
1099 local tex
1101 if obj.priority == i then
1102 tex = self:CreateIconTexture(item, 10)
1103 elseif obj.real_priority == i then
1104 tex = self:CreateIconTexture(item, 8)
1105 else
1106 tex = self:CreateIconTexture(item, 12)
1107 tex:SetVertexColor(1, 1, 1, 0)
1110 item:AddTexture(tex, true)
1111 item:SetFunction(self.SetObjectivePriorityPrompt, self, obj, i)
1114 self:CreateMenuItem(menu, QHText("PRIORITY")):SetSubmenu(submenu)
1116 if self.sharing then
1117 submenu = self:CreateMenu(QHText("SHARING"))
1118 local item = self:CreateMenuItem(submenu, QHText("ENABLE"))
1119 local tex = self:CreateIconTexture(item, 10)
1120 if not obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1121 item:AddTexture(tex, true)
1122 item:SetFunction(obj.Share, obj)
1124 local item = self:CreateMenuItem(submenu, QHText("DISABLE"))
1125 local tex = self:CreateIconTexture(item, 10)
1126 if obj.want_share then tex:SetVertexColor(1, 1, 1, 0) end
1127 item:AddTexture(tex, true)
1128 item:SetFunction(obj.Unshare, obj)
1130 self:CreateMenuItem(menu, QHText("SHARING")):SetSubmenu(submenu)
1133 self:CreateMenuItem(menu, QHText("IGNORE")):SetFunction(self.IgnoreObjective, self, obj)
1136 function QuestHelper:IgnoreObjective(objective)
1137 if self.user_objectives[objective] then
1138 self:RemoveObjectiveWatch(objective, self.user_objectives[objective])
1139 self.user_objectives[objective] = nil
1140 else
1141 objective.user_ignore = true
1144 --self:ForceRouteUpdate()
1147 function QuestHelper:SetObjectivePriority(objective, level)
1148 level = math.min(5, math.max(1, math.floor((tonumber(level) or 3)+0.5)))
1149 if level ~= objective.priority then
1150 objective.priority = level
1151 if objective.peer then
1152 for u, l in pairs(objective.peer) do
1153 -- Peers don't know about this new priority.
1154 objective.peer[u] = math.min(l, 2)
1157 --self:ForceRouteUpdate()
1161 local function CalcObjectivePriority(obj)
1162 local priority = obj.priority
1164 for o in pairs(obj.before) do
1165 if o.watched then
1166 priority = math.min(priority, CalcObjectivePriority(o))
1170 return priority
1173 local function ApplyBlockPriority(obj, level)
1174 for o in pairs(obj.before) do
1175 if o.watched then
1176 ApplyBlockPriority(o, level)
1180 if obj.priority < level then QuestHelper:SetObjectivePriority(obj, level) end
1183 function QuestHelper:SetObjectivePriorityPrompt(objective, level)
1184 self:SetObjectivePriority(objective, level)
1185 if CalcObjectivePriority(objective) ~= level then
1186 local menu = self:CreateMenu()
1187 self:CreateMenuTitle(menu, QHText("IGNORED_PRIORITY_TITLE"))
1188 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_FIX")):SetFunction(ApplyBlockPriority, objective, level)
1189 self:CreateMenuItem(menu, QHText("IGNORED_PRIORITY_IGNORE")):SetFunction(self.nop)
1190 menu:ShowAtCursor()
1194 function QuestHelper:SetObjectiveProgress(objective, user, have, need)
1195 if have and need then
1196 local list = objective.progress
1197 if not list then
1198 list = self:CreateTable()
1199 objective.progress = list
1202 local user_progress = list[user]
1203 if not user_progress then
1204 user_progress = self:CreateTable()
1205 list[user] = user_progress
1208 local pct = 0
1209 local a, b = tonumber(have), tonumber(need)
1210 if a and b then
1211 if b ~= 0 then
1212 pct = a/b
1213 elseif a == 0 then
1214 pct = 1
1216 elseif a == b then
1217 pct = 1
1220 user_progress[1], user_progress[2], user_progress[3] = have, need, pct
1221 else
1222 if objective.progress then
1223 if objective.progress[user] then
1224 self:ReleaseTable(objective.progress[user])
1225 objective.progress[user] = nil
1227 if not next(objective.progress, nil) then
1228 self:ReleaseTable(objective.progress)
1229 objective.progress = nil