update changes
[QuestHelper.git] / pathfinding.lua
blob6fa8e03118129983c969284091c95527047b8a09
1 QuestHelper_File["pathfinding.lua"] = "Development Version"
3 local IRONFORGE_PORTAL = {25,0.255,0.084, "Ironforge portal site"}
4 local STORMWIND_CITY_PORTAL = QuestHelper_ConvertCoordsFromWrath(QuestHelper_ConvertCoordsToWrath({36,0.387,0.802, "Stormwind City portal site"}, true)) -- The annoying "wrath" construction here first forces it into Wrath format, then puts it into Native format.
5 local DARNASSUS_PORTAL = {21,0.397,0.824, "Darnassus portal site"}
6 local EXODAR_PORTAL = {12,0.476,0.598, "Exodar portal site"}
8 local SHATTRATH_CITY_PORTAL = {60,0.530,0.492, "Shattrath City portal site"}
9 local DALARAN_PORTAL = {67,0.500,0.394, "Dalaran portal site"}
10 local MOONGLADE_PORTAL = {20,0.563,0.320, "Moonglade portal site"}
12 local SILVERMOON_CITY_PORTAL = {52,0.583,0.192, "Silvermoon City portal site"}
13 local UNDERCITY_PORTAL = {45,0.846,0.163, "Undercity portal site"}
14 local ORGRIMMAR_PORTAL = {1,0.386,0.859, "Orgrimmar portal site"}
15 local THUNDER_BLUFF_PORTAL = {23,0.222,0.168, "Thunder Bluff portal site"}
17 local static_horde_routes =
19 {{7, 0.505, 0.124}, {38, 0.313, 0.303}, 210}, -- Durotar <--> Grom'gol Base Camp
20 {{38, 0.316, 0.289}, {43, 0.621, 0.591}, 210}, -- Grom'gol Base Camp <--> Tirisfal Glades
21 {{43, 0.605, 0.587}, {7, 0.509, 0.141}, 210}, -- Tirisfal Glades <--> Durotar
22 {{45, 0.549, 0.11}, {52, 0.495, 0.148}, 5}, -- Undercity <--> Silvermoon City
24 {{7, 0.413, 0.178}, {65, 0.414, 0.536}, 5}, -- Durotar <--> Warsong Hold
25 {{43, 0.591, 0.590}, {70, 0.777, 0.283}, 5}, -- Tirisfal Glades <--> Vengeance Landing
27 {{60, 0.592, 0.483}, SILVERMOON_CITY_PORTAL, 5, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Shattrath City --> Silvermoon City
28 {{60, 0.528, 0.531}, THUNDER_BLUFF_PORTAL, 5, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Shattrath City --> Thunder Bluff
29 {{60, 0.522, 0.529}, ORGRIMMAR_PORTAL, 5, true, nil, "ORGRIMMAR_PORTAL"}, -- Shattrath City --> Orgrimmar
30 {{60, 0.517, 0.525}, UNDERCITY_PORTAL, 5, true, nil, "UNDERCITY_PORTAL"}, -- Shattrath City --> Undercity
32 {{67, 0.583, 0.216}, SILVERMOON_CITY_PORTAL, 5, true, nil, "SILVERMOON_CITY_PORTAL"}, -- Dalaran --> Silvermoon City
33 {{67, 0.573, 0.219}, THUNDER_BLUFF_PORTAL, 5, true, nil, "THUNDER_BLUFF_PORTAL"}, -- Dalaran --> Thunder Bluff
34 {{67, 0.553, 0.255}, ORGRIMMAR_PORTAL, 5, true, nil, "ORGRIMMAR_PORTAL"}, -- Dalaran --> Orgrimmar
35 {{67, 0.556, 0.238}, UNDERCITY_PORTAL, 5, true, nil, "UNDERCITY_PORTAL"}, -- Dalaran --> Undercity
36 {{67, 0.563, 0.226}, SHATTRATH_CITY_PORTAL, 5, true, nil, "SHATTRATH_CITY_PORTAL"}, -- Dalaran --> Shatt
40 local static_alliance_routes =
42 {{36, 0.639, 0.083}, {25, 0.764, 0.512}, 120}, -- Deeprun Tram
43 {{10, 0.718, 0.565}, {51, 0.047, 0.636}, 210}, -- Theramore Isle <--> Menethil Harmor
45 {QuestHelper_ConvertCoordsFromWrath({36, 0.183, 0.255}), {65, 0.597, 0.694}, 210}, -- Stormwind City <--> Valiance Keep (note: new Stormwind location)
46 {{51, 0.047, 0.571}, {70, 0.612, 0.626}, 210}, -- Menethil <--> Daggercap Bay
48 {{60, 0.558, 0.366}, STORMWIND_CITY_PORTAL, 5, true, nil, "STORMWIND_CITY_PORTAL"}, -- Shattrath City --> Stormwind City
49 {{60, 0.563, 0.37}, IRONFORGE_PORTAL, 5, true, nil, "IRONFORGE_PORTAL"}, -- Shattrath City --> Ironforge
50 {{60, 0.552, 0.364}, DARNASSUS_PORTAL, 5, true, nil, "DARNASSUS_PORTAL"}, -- Shattrath City --> Darnassus
51 {{60, 0.596, 0.467}, EXODAR_PORTAL, 5, true, nil, "EXODAR_PORTAL"}, -- Shattrath City --> Exodar
53 {{67, 0.401, 0.628}, STORMWIND_CITY_PORTAL, 5, true, nil, "STORMWIND_CITY_PORTAL"}, -- Dalaran --> Stormwind City
54 {{67, 0.395, 0.640}, IRONFORGE_PORTAL, 5, true, nil, "IRONFORGE_PORTAL"}, -- Dalaran --> Ironforge
55 {{67, 0.389, 0.651}, DARNASSUS_PORTAL, 5, true, nil, "DARNASSUS_PORTAL"}, -- Dalaran --> Darnassus
56 {{67, 0.382, 0.664}, EXODAR_PORTAL, 5, true, nil, "EXODAR_PORTAL"}, -- Dalaran --> Exodar
57 {{67, 0.371, 0.667}, SHATTRATH_CITY_PORTAL, 5, true, nil, "SHATTRATH_CITY_PORTAL"}, -- Dalaran --> Shatt
60 if QuestHelper:IsWrath() then -- siiiiigh
61 table.insert(static_alliance_routes, {{36, 0.228, 0.560}, {16, 0.323, 0.441}, 210}) -- Stormwind City <--> Auberdine (note: new Stormwind location)
62 else
63 table.insert(static_alliance_routes, {{51, 0.044, 0.569}, {16, 0.323, 0.441}, 210}) -- Menethil Harbor <--> Auberdine
64 end
66 local static_shared_routes =
68 {{11, 0.638, 0.387}, {38, 0.257, 0.73}, 210}, -- Ratchet <--> Booty Bay
69 {{40, 0.318, 0.503}, {32, 0.347, 0.84}, 130}, -- Burning Steppes <--> Searing Gorge
71 -- More Alliance routes than anything, but without them theres no valid path to these areas for Horde characters.
72 {{24, 0.559, 0.896}, {21, 0.305, 0.414}, 5}, -- Rut'Theran Village <--> Darnassus
73 {{16, 0.332, 0.398}, {24, 0.548, 0.971}, 210}, -- Auberdine <--> Rut'Theran Village
74 {{16, 0.306, 0.409}, {3, 0.2, 0.546}, 210}, -- Auberdine <--> Azuremyst Isle
76 -- Route to new zone. Not valid, exists only to keep routing from exploding if you don't have the flight routes there.
77 {{41, 0.5, 0.5}, {64, 0.5, 0.5}, 7200}, -- Eversong Woods <--> Sunwell
79 {{70, 0.235, 0.578}, {68, 0.496, 0.784}, 210}, -- Kamagua <--> Moa'ki
80 {{65, 0.789, 0.536}, {68, 0.480, 0.787}, 210}, -- Unu'pe <--> Moa'ki
81 {{67, 0.559, 0.467}, {66, 0.158, 0.428}, 5, true}, -- Dalaran --> Violet Stand
82 {{66, 0.157, 0.425}, {67, 0.559, 0.468}, 5, true}, -- Violent Stand --> Dalaran (slightly different coordinates, may be important once solid walls are in)
84 {{34, 0.817, 0.461}, {78, 0.492, 0.312}, 86400}, -- EPL Ebon Hold <--> Scarlet Enclave Ebon Hold. Exists solely to fix some pathing crashes.
87 -- Darkportal is handled specially, depending on whether or not you're level 58+ or not.
88 local dark_portal_route = {{33, 0.587, 0.599}, {56, 0.898, 0.502}, 5}
90 local static_zone_transitions =
92 {2, 11, 0.687, 0.872}, -- Ashenvale <--> The Barrens
93 {2, 6, 0.423, 0.711}, -- Ashenvale <--> Stonetalon Mountains
94 {2, 15, 0.954, 0.484}, -- Ashenvale <--> Azshara
95 {2, 16, 0.289, 0.144}, -- Ashenvale <--> Darkshore
96 {2, 13, 0.557, 0.29}, -- Ashenvale <--> Felwood
97 {21, 24, 0.894, 0.358}, -- Darnassus <--> Teldrassil
98 {22, 11, 0.697, 0.604}, -- Mulgore <--> The Barrens
99 {22, 23, 0.376, 0.33}, -- Mulgore <--> Thunder Bluff
100 {22, 23, 0.403, 0.193}, -- Mulgore <--> Thunder Bluff
101 {3, 12, 0.247, 0.494}, -- Azuremyst Isle <--> The Exodar
102 {3, 12, 0.369, 0.469}, -- Azuremyst Isle <--> The Exodar
103 {3, 12, 0.310, 0.487}, -- Azuremyst Isle <--> The Exodar
104 {3, 12, 0.335, 0.494}, -- Azuremyst Isle <--> The Exodar
105 {3, 9, 0.42, 0.013}, -- Azuremyst Isle <--> Bloodmyst Isle
106 {4, 6, 0.539, 0.032}, -- Desolace <--> Stonetalon Mountains
107 {4, 17, 0.428, 0.976}, -- Desolace <--> Feralas
108 {5, 18, 0.865, 0.115}, -- Silithus <--> Un'Goro Crater
109 {7, 11, 0.341, 0.424}, -- Durotar <--> The Barrens
110 {7, 1, 0.455, 0.121}, -- Durotar <--> Orgrimmar
111 {8, 18, 0.269, 0.516}, -- Tanaris <--> Un'Goro Crater
112 {8, 14, 0.512, 0.21}, -- Tanaris <--> Thousand Needles
113 {10, 11, 0.287, 0.472}, -- Dustwallow Marsh <--> The Barrens
114 {10, 11, 0.563, 0.077}, -- Dustwallow Marsh <--> The Barrens
115 {11, 14, 0.442, 0.915}, -- The Barrens <--> Thousand Needles
116 {13, 19, 0.685, 0.06}, -- Felwood <--> Winterspring
117 {13, 20, 0.669, -0.063}, -- Felwood <--> Moonglade
118 {1, 11, 0.118, 0.69}, -- Orgrimmar <--> The Barrens
119 {17, 14, 0.899, 0.46}, -- Feralas <--> Thousand Needles
120 {6, 11, 0.836, 0.973}, -- Stonetalon Mountains <--> The Barrens
121 {26, 48, 0.521, 0.7}, -- Alterac Mountains <--> Hillsbrad Foothills
122 {26, 35, 0.173, 0.482}, -- Alterac Mountains <--> Silverpine Forest
123 {26, 50, 0.807, 0.347}, -- Alterac Mountains <--> Western Plaguelands
124 {39, 51, 0.454, 0.89}, -- Arathi Highlands <--> Wetlands
125 {39, 48, 0.2, 0.293}, -- Arathi Highlands <--> Hillsbrad Foothills
126 {27, 29, 0.49, 0.071}, -- Badlands <--> Loch Modan
127 {27, 32, -0.005, 0.636}, -- Badlands <--> Searing Gorge
128 {33, 46, 0.519, 0.051}, -- Blasted Lands <--> Swamp of Sorrows
129 {40, 30, 0.79, 0.842}, -- Burning Steppes <--> Redridge Mountains
130 {47, 31, 0.324, 0.363}, -- Deadwind Pass <--> Duskwood
131 {47, 46, 0.605, 0.41}, -- Deadwind Pass <--> Swamp of Sorrows
132 {28, 25, 0.534, 0.349}, -- Dun Morogh <--> Ironforge
133 {28, 29, 0.863, 0.514}, -- Dun Morogh <--> Loch Modan
134 {28, 29, 0.844, 0.31}, -- Dun Morogh <--> Loch Modan
135 {31, 37, 0.801, 0.158}, -- Duskwood <--> Elwynn Forest
136 {31, 37, 0.15, 0.214}, -- Duskwood <--> Elwynn Forest
137 {31, 38, 0.447, 0.884}, -- Duskwood <--> Stranglethorn Vale
138 {31, 38, 0.209, 0.863}, -- Duskwood <--> Stranglethorn Vale
139 {31, 30, 0.941, 0.103}, -- Duskwood <--> Redridge Mountains
140 {31, 49, 0.079, 0.638}, -- Duskwood <--> Westfall
141 {34, 50, 0.107, 0.726}, -- Eastern Plaguelands <--> Western Plaguelands
142 {34, 44, 0.625, 0.03}, -- Eastern Plaguelands <--> Ghostlands
143 {37, 36, 0.321, 0.493}, -- Elwynn Forest <--> Stormwind City -- Don't need to convert because it's in Elwynn coordinates, not Stormwind coordinates
144 {37, 49, 0.202, 0.804}, -- Elwynn Forest <--> Westfall
145 {37, 30, 0.944, 0.724}, -- Elwynn Forest <--> Redridge Mountains
146 {41, 52, 0.567, 0.494}, -- Eversong Woods <--> Silvermoon City
147 {41, 44, 0.486, 0.916}, -- Eversong Woods <--> Ghostlands
148 {35, 43, 0.678, 0.049}, -- Silverpine Forest <--> Tirisfal Glades
149 {42, 50, 0.217, 0.264}, -- The Hinterlands <--> Western Plaguelands
150 {43, 45, 0.619, 0.651}, -- Tirisfal Glades <--> Undercity
151 {43, 50, 0.851, 0.703}, -- Tirisfal Glades <--> Western Plaguelands
152 {38, 49, 0.292, 0.024}, -- Stranglethorn Vale <--> Westfall
153 {48, 35, 0.137, 0.458}, -- Hillsbrad Foothills <--> Silverpine Forest
154 {48, 42, 0.899, 0.253}, -- Hillsbrad Foothills <--> The Hinterlands
155 {29, 51, 0.252, 0}, -- Loch Modan <--> Wetlands
157 -- These are just guesses, since I haven't actually been to these areas.
158 {58, 60, 0.783, 0.545}, -- Nagrand <--> Shattrath City
159 {60, 55, 0.782, 0.492}, -- Shattrath City <--> Terokkar Forest
160 {54, 59, 0.842, 0.284}, -- Blade's Edge Mountains <--> Netherstorm
161 {54, 57, 0.522, 0.996}, -- Blade's Edge Mountains <--> Zangarmarsh
162 {54, 57, 0.312, 0.94}, -- Blade's Edge Mountains <--> Zangarmarsh
163 {56, 55, 0.353, 0.901}, -- Hellfire Peninsula <--> Terokkar Forest
164 {56, 57, 0.093, 0.519}, -- Hellfire Peninsula <--> Zangarmarsh
165 {58, 55, 0.8, 0.817}, -- Nagrand <--> Terokkar Forest
166 {58, 57, 0.343, 0.159}, -- Nagrand <--> Zangarmarsh
167 {58, 57, 0.754, 0.331}, -- Nagrand <--> Zangarmarsh
168 {53, 55, 0.208, 0.271}, -- Shadowmoon Valley <--> Terokkar Forest
169 {55, 57, 0.341, 0.098}, -- Terokkar Forest <--> Zangarmarsh
171 -- Most of these are also guesses :)
172 {65, 72, 0.547, 0.059}, -- Borean Tundra <--> Sholazar Basin
173 {65, 68, 0.967, 0.359}, -- Borean Tundra <--> Dragonblight
174 {74, 72, 0.208, 0.191}, -- Wintergrasp <--> Sholazar
175 {68, 74, 0.250, 0.410}, -- Dragonblight <--> Wintergrasp
176 {68, 71, 0.359, 0.155}, -- Dragonblight <--> Icecrown
177 {68, 66, 0.612, 0.142}, -- Dragonblight <--> Crystalsong
178 {68, 75, 0.900, 0.200}, -- Dragonblight <--> Zul'Drak
179 {68, 69, 0.924, 0.304}, -- Dragonblight <--> Grizzly Hills
180 {68, 69, 0.931, 0.634}, -- Dragonblight <--> Grizzly Hills
181 {70, 69, 0.540, 0.042}, -- Howling Fjord <--> Grizzly Hills
182 {70, 69, 0.233, 0.074}, -- Howling Fjord <--> Grizzly Hills
183 {70, 69, 0.753, 0.060}, -- Howling Fjord <--> Grizzly Hills
184 {69, 75, 0.432, 0.253}, -- Grizzly Hills <--> Zul'Drak
185 {69, 75, 0.583, 0.136}, -- Grizzly Hills <--> Zul'Drak
186 {66, 75, 0.967, 0.599}, -- Crystalsong <--> Zul'Drak
187 {66, 71, 0.156, 0.085}, -- Crystalsong <--> Icecrown
188 {66, 73, 0.706, 0.315}, -- Crystalsong <--> Storm Peaks
189 {66, 73, 0.839, 0.340}, -- Crystalsong <--> Storm Peaks
190 {71, 73, 0.920, 0.767}, -- Icecrown <--> Storm Peaks
193 local walkspeed_multiplier = 1/7 -- Every yard walked takes this many seconds.
195 QuestHelper.prepared_objectives = {}
196 QuestHelper.named_nodes = {}
198 local function cont_dist(a, b)
199 local x, y = a.x-b.x, a.y-b.y
200 return math.sqrt(x*x+y*y)
203 function QuestHelper:ComputeRoute(p1, p2)
204 if not p1 or not p2 then QuestHelper:Error("Boom!") end
205 local graph = self.world_graph
207 graph:PrepareSearch()
209 local l = p2[2]
210 local el = p2[1]
211 for i, n in ipairs(el) do
212 n.e, n.w = l[i], 1
213 n.s = 3
216 l = p1[2]
217 for i, n in ipairs(p1[1]) do
218 graph:AddRouteStartNode(n, l[i], el)
221 local e = graph:DoRouteSearch(el)
223 assert(e)
225 local d = e.g+e.e
227 if p1[1] == p2[1] then
228 local x, y = p1[3]-p2[3], p1[4]-p2[4]
229 local d2 = math.sqrt(x*x+y*y)
230 if d2 < d then
231 d = d2
232 e = nil
236 return e, d
239 -- Let's annotate the hell out of this
240 -- ComputeTravelTime finds the shortest path between points p1 and p2. It will cheerfully use route boundaries, ships, etc. It returns the distance of that path, gleefully throwing away the path itself. Thanks, ComputeTravelTime! Thomputetraveltime. (That joke does not work as well in this case.)
241 -- ZORBANOTE: Given that Graph works properly, this does too! Almost - it doesn't actually keep track of the last leg when optimizing, though it does create a valid path with a valid cost. Yaaaaaaaay :(
242 function QuestHelper:ComputeTravelTime(p1, p2)
243 if not p1 or not p2 then QuestHelper:Error("Boom!") end
244 local graph = self.world_graph
246 graph:PrepareSearch()
248 local l = p2[2] -- Distance to zone boundaries in p2
249 local el = p2[1] -- Zone object for the zone that p2 is in
250 for i, n in ipairs(el) do -- i is the zone index, n is the zone data
251 n.e, n.w = l[i], 1 -- n.e is distance from p2 to the current zone boundary, n.w is 1 (weight?)
252 assert(n.e)
253 n.s = 3 -- this is "state", I think it means "visited". TODO: untangle n.s and make it suck less than it currently does
256 l = p1[2] -- Distance to zone boundaries, again
257 for i, n in ipairs(p1[1]) do
258 graph:AddStartNode(n, l[i], el) -- We're adding start nodes - a prebuilt cost of the distance-to-that-zone. Each startnode also contains its own endlist, for reasons unknown yet byzantine. "n" is the zone link itself, l[i] is the cost that we still have stored. Why does this need to be in both n.e and AddStartNode?
261 local e = graph:DoSearch(el)
263 assert(e)
265 local d = e.g+e.e -- e.e is presumably the same n.e we introduced earlier. e.g - graph cost? wait a second - does this mean that the graph system is not taking e.e into account? ha ha no it isn't, oh boy oh boy
267 if p1[1] == p2[1] then -- if they're in the same zone, we allow the user to walk from one point to another
268 local x, y = p1[3]-p2[3], p1[4]-p2[4]
269 d = math.min(d, math.sqrt(x*x+y*y))
272 return d
275 function QuestHelper:CreateGraphNode(c, x, y, n)
276 local node = self.world_graph:CreateNode()
278 if y then
279 node.c = c
280 node.x = x
281 node.y = y
282 node.name = n
283 else
284 if not QuestHelper_ZoneLookup[c[1]] then -- exception for Wrath changeover
285 QuestHelper:Assert(not QuestHelper:IsWrath(), "Zone couldn't be found, and should have been")
286 return
288 local cont, zone = unpack(QuestHelper_ZoneLookup[c[1]])
289 node.c = cont
290 node.x, node.y = self.Astrolabe:TranslateWorldMapPosition(cont, zone, c[2], c[3], cont, 0)
291 node.x = node.x * self.continent_scales_x[node.c]
292 node.y = node.y * self.continent_scales_y[node.c]
293 node.name = c[5] or QuestHelper_NameLookup[c[1]]
296 node.w = 1
297 return node
300 function QuestHelper:CreateAndAddZoneNode(z, c, x, y)
301 local node = self:CreateGraphNode(c, x, y)
302 if not node then return end -- exception for Wrath changeover
303 -- Not going to merge nodes.
304 --[[local closest, travel_time = nil, 0
306 for i, n in ipairs(z) do
307 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
308 if not closest or t < travel_time then
309 closest, travel_time = n, t
313 if closest and travel_time < 10 then
314 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
315 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
316 closest.w = closest.w + 1
317 self.world_graph:DestroyNode(node)
318 return closest
319 else]]
320 table.insert(z, node)
321 return node
322 --end
325 function QuestHelper:CreateAndAddStaticNodePair(data)
326 local node1, node2
328 if data[5] and self.named_nodes[data[5]] then
329 node1 = self.named_nodes[data[5]]
330 else
331 node1 = self:CreateAndAddZoneNode(self.zone_nodes[data[1][1]], data[1])
332 if not node1 then return end -- exception for Wrath changeover
333 if data[5] then self.named_nodes[data[5]] = node1 end
336 if data[6] and self.named_nodes[data[6]] then
337 node2 = self.named_nodes[data[6]]
338 else
339 node2 = self:CreateAndAddZoneNode(self.zone_nodes[data[2][1]], data[2])
340 if not node2 then return end -- exception for Wrath changeover
341 if data[6] then self.named_nodes[data[6]] = node2 end
344 node1.name = node1.name or "route to "..QuestHelper_NameLookup[data[2][1]]
345 node2.name = node2.name or "route to "..QuestHelper_NameLookup[data[1][1]]
347 node1:Link(node2, data[3])
349 if not data[4] then -- If data[4] is true, then this is a one-way trip.
350 node2:Link(node1, data[3])
353 self:yieldIfNeeded()
354 return node1, node2
357 function QuestHelper:GetNodeByName(name, fallback_data)
358 local node = self.named_nodes[name]
359 if not node and fallback_data then
360 node = self:CreateAndAddZoneNode(self.zone_nodes[fallback_data[1]], fallback_data)
361 self.named_nodes[name] = node
363 return node
366 local function nodeLeavesContinent(node, c)
367 if node.c == c then
368 for n, d in pairs(node.n) do
369 if n.c ~= c then
370 return true
374 return false
377 local function isGoodPath(start_node, end_node, i, j)
378 -- Checks to make sure a path doesn't leave the continent only to reenter it.
379 while true do
380 if end_node.p then
381 if end_node.c == i then
382 return false
384 end_node = end_node.p
385 if end_node.c == j then
386 return false
388 else
389 return end_node == start_node
394 local function shouldLink(a, b)
395 -- TODO: Need to have objectives not create links to unreachable nodes.
396 return a ~= b
397 --[[
398 if a == b then
399 return false
400 else
401 for id in pairs(a.id_from) do
402 if not b.id_to[id] then
403 for id in pairs(b.id_to) do
404 if not a.id_from[id] then
405 return true
411 return false
412 end]]
415 local function getNPCNode(npc)
416 local npc_objective = QuestHelper:GetObjective("monster", npc)
417 if npc_objective:Known() then
418 npc_objective:PrepareRouting()
419 local p = npc_objective:Position()
420 local node = nil
422 if p then
423 node = QuestHelper:CreateAndAddZoneNode(p[1], p[1].c, p[3], p[4])
426 npc_objective:DoneRouting()
427 return node
429 return nil
432 function QuestHelper:CreateAndAddTransitionNode(z1, z2, pos)
433 if not z1 or not z2 then
434 QuestHelper:Assert(not QuestHelper:IsWrath(), "Zone couldn't be found, and should have been")
435 return
438 local node = self:CreateGraphNode(pos)
440 local closest, travel_time = nil, 0
442 for i, n in ipairs(z1) do
443 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
444 if not closest or t < travel_time then
445 closest, travel_time = n, t
449 if z1 ~= z2 then
450 for i, n in ipairs(z2) do
451 local t = math.sqrt((n.x-node.x)*(n.x-node.x)+(n.y-node.y)*(n.y-node.y))
452 if not closest or t < travel_time then
453 closest, travel_time = n, t
458 if closest and travel_time < 10 then
459 --QuestHelper:TextOut("Node already exists at "..closest.x..", "..closest.y..", name="..(closest.name or "nil"))
460 closest.x = (closest.x * closest.w + node.x)/(closest.w+1)
461 closest.y = (closest.y * closest.w + node.y)/(closest.w+1)
462 closest.w = closest.w + 1
463 local z1_has, z2_has = false, false
465 -- Just because the node already exists, doesn't mean its already in both lists!
467 for i, n in ipairs(z1) do
468 if n == closest then
469 z1_has = true
470 break
474 if z1 ~= z2 then
475 for i, n in ipairs(z2) do
476 if n == closest then
477 z2_has = true
478 break
481 else
482 z2_has = true
485 if not z1_has then table.insert(z1, closest) end
486 if not z2_has then table.insert(z2, closest) end
488 self.world_graph:DestroyNode(node)
489 self:yieldIfNeeded()
490 return closest
491 else
492 table.insert(z1, node)
493 if z1 ~= z2 then table.insert(z2, node) end
494 self:yieldIfNeeded()
495 return node
499 function QuestHelper:ReleaseObjectivePathingInfo(o)
500 if o.setup then
501 for z, pl in pairs(o.p) do
502 self:ReleaseTable(o.d[z])
504 for i, p in ipairs(pl) do
505 self:ReleaseTable(p[2])
506 self:ReleaseTable(p)
509 self:ReleaseTable(pl)
512 self:ReleaseTable(o.d)
513 self:ReleaseTable(o.p)
514 self:ReleaseTable(o.nm)
515 self:ReleaseTable(o.nm2)
516 self:ReleaseTable(o.nl)
518 local cache = o.distance_cache
519 for k, v in pairs(cache) do
520 self:ReleaseTable(v)
521 cache[k] = nil
523 self:ReleaseTable(cache)
525 o.d, o.p, o.nm, o.nm2, o.nl = nil, nil, nil, nil, nil
526 o.distance_cache = nil
527 o.pos, o.sop = nil, nil -- ResetPathing will preserve these values if needed.
528 o.setup = nil
532 function QuestHelper:SetupTeleportInfo(info, can_create)
533 self:TeleportInfoClear(info)
535 if QuestHelper_Home then
536 local node = self:GetNodeByName("HOME_PORTAL", can_create and QuestHelper_Home)
537 if node then
538 local cooldown = self:ItemCooldown(6948)
539 if cooldown then
540 self:SetTeleportInfoTarget(info, node, GetTime()-60*60+cooldown, 60*60, 10)
542 else
543 self.defered_graph_reset = true
547 -- TODO: Compact this. . . and find a better way to tell if the player has a spell.
549 if GetSpellTexture("Teleport: Darnassus") then
550 local node = self:GetNodeByName("DARNASSUS_PORTAL", can_create and DARNASSUS_PORTAL)
551 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
554 if GetSpellTexture("Teleport: Exodar") then
555 local node = self:GetNodeByName("EXODAR_PORTAL", can_create and EXODAR_PORTAL)
556 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
559 if GetSpellTexture("Teleport: Ironforge") then
560 local node = self:GetNodeByName("IRONFORGE_PORTAL", can_create and IRONFORGE_PORTAL)
561 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
564 if GetSpellTexture("Teleport: Moonglade") then
565 local node = self:GetNodeByName("MOONGLADE_PORTAL", can_create and MOONGLADE_PORTAL)
566 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10) else self.defered_graph_reset = true end
569 if GetSpellTexture("Teleport: Orgrimmar") then
570 local node = self:GetNodeByName("ORGRIMMAR_PORTAL", can_create and ORGRIMMAR_PORTAL)
571 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
574 if GetSpellTexture("Teleport: Shattrath") then
575 local node = self:GetNodeByName("SHATTRATH_CITY_PORTAL", can_create and SHATTRATH_CITY_PORTAL)
576 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
579 if GetSpellTexture("Teleport: Silvermoon") then
580 local node = self:GetNodeByName("SILVERMOON_CITY_PORTAL", can_create and SILVERMOON_CITY_PORTAL)
581 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
584 if GetSpellTexture("Teleport: Stormwind") then
585 local node = self:GetNodeByName("STORMWIND_CITY_PORTAL", can_create and STORMWIND_CITY_PORTAL)
586 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
589 if GetSpellTexture("Teleport: Thunder Bluff") then
590 local node = self:GetNodeByName("THUNDER_BLUFF_PORTAL", can_create and THUNDER_BLUFF_PORTAL)
591 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
594 if GetSpellTexture("Teleport: Undercity") then
595 local node = self:GetNodeByName("UNDERCITY_PORTAL", can_create and UNDERCITY_PORTAL)
596 if node then self:SetTeleportInfoTarget(info, node, 0, 0, 10, 17031) else self.defered_graph_reset = true end
599 self:SetTeleportInfoReagent(info, 17031, self:CountItem(17031))
602 function QuestHelper:ResetPathing()
603 for key in pairs(self.named_nodes) do
604 self.named_nodes[key] = nil
607 -- Objectives may include cached information that depends on the world graph.
608 local i = 1
610 while i <= #self.prepared_objectives do
611 local o = self.prepared_objectives[i]
613 if o.setup_count == 0 then
614 table.remove(self.prepared_objectives, i)
615 self:ReleaseObjectivePathingInfo(o)
616 else
617 -- Routing should reset the positions of objectives in the route after the reset is complete.
618 o.pos = nil
619 self:ReleaseObjectivePathingInfo(o)
620 i = i + 1
624 local to_readd = self.prepared_objectives
625 self.prepared_objectives = self.old_prepared_objectives or {}
626 self.old_prepared_objectives = to_readd
628 local zone_nodes = self.zone_nodes
629 if not zone_nodes then
630 zone_nodes = {}
631 self.zone_nodes = zone_nodes
634 local flight_master_nodes = self.flight_master_nodes
635 if not flight_master_nodes then
636 flight_master_nodes = {}
637 self.flight_master_nodes = flight_master_nodes
638 else
639 for key in pairs(flight_master_nodes) do
640 flight_master_nodes[key] = nil
644 self.world_graph:Reset()
645 self:yieldIfNeeded()
647 local continent_scales_x, continent_scales_y = self.continent_scales_x, self.continent_scales_y
648 if not continent_scales_x then
649 continent_scales_x = {}
650 continent_scales_y = {}
651 self.continent_scales_x = continent_scales_x
652 self.continent_scales_y = continent_scales_y
655 for c in pairs(self.Astrolabe:GetMapVirtualContinents()) do
656 if not continent_scales_x[c] then
657 local _, x, y = self.Astrolabe:ComputeDistance(c, 0, 0.25, 0.25, c, 0, 0.75, 0.75)
659 continent_scales_x[c] = x*walkspeed_multiplier*2
660 continent_scales_y[c] = y*walkspeed_multiplier*2
664 for i, name in pairs(QuestHelper_NameLookup) do
665 local z = zone_nodes[i]
666 if not z then
667 z = QuestHelper:CreateTable("zone") -- This was originally z = {}. I'm pretty sure that the CreateTable/ReleaseTable system is (largely) immune to leaks, and I'm also pretty sure that zones are only created once, at the beginning of the system. Keep this in mind if leaks start occuring.
668 zone_nodes[i] = z
669 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
670 else
671 for key in pairs(z) do
672 z[key] = nil
674 z.i, z.c, z.z = i, unpack(QuestHelper_ZoneLookup[i])
678 self:SetupTeleportInfo(self.teleport_info, true)
679 self:yieldIfNeeded()
681 --[[for node, info in pairs(self.teleport_info.node) do
682 self:TextOut("You can teleport to "..(node.name or "nil").. " in "..self:TimeString(info[1]+info[2]-GetTime()))
683 end]]
685 if self.faction == 1 then
686 for i, data in ipairs(static_alliance_routes) do
687 self:CreateAndAddStaticNodePair(data)
689 elseif self.faction == 2 then
690 for i, data in ipairs(static_horde_routes) do
691 self:CreateAndAddStaticNodePair(data)
695 for i, data in ipairs(static_shared_routes) do
696 self:CreateAndAddStaticNodePair(data)
699 if self.player_level >= 58 then
700 dark_portal_route[3] = 5
701 else
702 -- If you can't take the route yet, we'll still add it and just pretend it will take a really long time.
703 dark_portal_route[3] = 86400
706 self:CreateAndAddStaticNodePair(dark_portal_route)
708 local st = self:CreateTable("ResetPathing local st")
710 for i, data in pairs(static_zone_transitions) do
711 st[1], st[2], st[3] = data[1], data[3], data[4]
713 local transnode = self:CreateAndAddTransitionNode(zone_nodes[data[1]],
714 zone_nodes[data[2]],
716 if transnode then transnode.name = QHFormat("ZONE_BORDER", QuestHelper_NameLookup[data[1]], QuestHelper_NameLookup[data[2]]) end -- if the transition node wasn't creatable, we obviously can't name it
719 self:ReleaseTable(st)
721 -- Create and link the flight route nodes.
722 local flight_times = self.flight_times
723 if not flight_times then
724 self:buildFlightTimes()
725 flight_times = self.flight_times
728 for start, list in pairs(flight_times) do
729 for dest, duration in pairs(list) do
730 local a_npc, b_npc = self:getFlightInstructor(start), self:getFlightInstructor(dest)
732 if a_npc and b_npc then
733 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
735 if not a then
736 a = getNPCNode(a_npc)
737 if a then
738 flight_master_nodes[start] = a
739 a.name = (select(3, string.find(start, "^(.*),")) or start).." flight point"
743 if not b then
744 b = getNPCNode(b_npc)
745 if b then
746 flight_master_nodes[dest] = b
747 b.name = (select(3, string.find(dest, "^(.*),")) or dest).." flight point"
751 if a and b then
752 a:Link(b, duration+5)
756 self:yieldIfNeeded()
759 -- id_from, id_to, and id_local will be used in determining whether there is a point to linking nodes together.
760 for i, n in ipairs(self.world_graph.nodes) do
761 n.id_from = self:CreateTable("ResetPathing n.id_from")
762 n.id_to = self:CreateTable("ResetPathing n.id_to")
763 n.id_local = self:CreateTable("ResetPathing n.id_local")
766 -- Setup the local ids a node exists in.
767 for i, list in pairs(zone_nodes) do
768 for _, n in ipairs(list) do
769 n.id_local[i] = true
770 n.id_to[i] = true
771 n.id_from[i] = true
775 -- Figure out where each node can come from or go to.
776 for i, list in pairs(zone_nodes) do
777 for _, node in ipairs(list) do
778 for n in pairs(node.n) do
779 for id in pairs(n.id_local) do node.id_to[id] = true end
780 for id in pairs(node.id_local) do n.id_from[id] = true end
785 -- We'll treat 0 as a special id for where ever it is the player happens to be.
786 for node in pairs(self.teleport_info.node) do
787 node.id_from[0] = true
790 -- Will go through each zone and link all the nodes we have so far with every other node.
791 for _, list in pairs(zone_nodes) do
792 for i = 1,#list do
793 for j = 1,#list do
794 if shouldLink(list[i], list[j]) then
795 list[i]:Link(list[j], cont_dist(list[i], list[j]))
801 self:yieldIfNeeded()
803 -- We don't need to know where the nodes can go or come from now.
804 for i, n in ipairs(self.world_graph.nodes) do
805 self:ReleaseTable(n.id_from)
806 self:ReleaseTable(n.id_to)
807 self:ReleaseTable(n.id_local)
808 n.id_from, n.id_to, n.id_local = nil, nil, nil
811 -- TODO: This is a work around until I fix shouldLink
812 for start, list in pairs(flight_times) do
813 for dest, duration in pairs(list) do
814 local a, b = flight_master_nodes[start], flight_master_nodes[dest]
815 if a and b then
816 a:Link(b, duration+5)
821 self:yieldIfNeeded()
822 -- self.world_graph:SanityCheck()
824 -- Remove objectives again, since we created some for the flight masters.
825 while true do
826 local o = table.remove(self.prepared_objectives)
827 if not o then break end
829 self:ReleaseObjectivePathingInfo(o)
831 if o.setup_count > 0 then
832 -- There's a chance an objective could end up in the list twice, but we'll deal with that by not actually
833 -- adding locations for it if it's already setup.
834 table.insert(to_readd, o)
838 while true do
839 local obj = table.remove(to_readd)
840 if not obj then break end
842 if not obj.setup then -- In case the objective was added multiple times to the to_readd list.
843 obj.d = QuestHelper:CreateTable("ResetPathing obj.d")
844 obj.p = QuestHelper:CreateTable("ResetPathing obj.p")
845 obj.nm = QuestHelper:CreateTable("ResetPathing obj.nm")
846 obj.nm2 = QuestHelper:CreateTable("ResetPathing obj.nm2")
847 obj.nl = QuestHelper:CreateTable("ResetPathing obj.nl")
848 obj.distance_cache = QuestHelper:CreateTable("ResetPathing obj.distance_cache")
849 obj:AppendPositions(obj, 1, nil)
850 obj:FinishAddLoc()
854 if self.i then
855 self.pos[1] = self.zone_nodes[self.i]
856 for i, n in ipairs(self.pos[1]) do
857 local a, b = n.x-self.pos[3], n.y-self.pos[4]
858 self.pos[2][i] = math.sqrt(a*a+b*b)
862 -- And if all went according to plan, we now have a graph we can follow to get from anywhere to anywhere.
864 if self.graph_walker then
865 self.graph_walker:GraphChanged()
870 function QuestHelper:Disallowed(index)
871 return QuestHelper_RestrictedZones[index] ~= QuestHelper_RestrictedZones[self.i]