1------------------------------------------------------------------------------
2-- Iterationeering functions.
3-- The positional iterators are all dlua only and iterate over regions in
4-- dungeon grid coordinates.
5-- @module iter
6
7-- Example usage of adjacent_iterator:
8--
9-- function iter.rats()
10--   local i
11--   for i in iter.adjacent_iterator() do
12--     dgn.create_monster(i.x, i.y, "rat")
13--   end
14-- end
15--
16-- The above function will surround the player with rats.
17
18-- dgn.point iterators.
19-- All functions expect dgn.point parameters, and return dgn.points.
20iter = {}
21
22-- generic filters for use with iterators
23--   these always need to be used with 'rvi', and any filter that acts on them
24--   is assumed to as well (but only return a point)
25
26function iter.monster_filter (filter)
27  local function filter_fn (point)
28    if filter ~= nil then
29      if filter(point) == nil then
30        return nil
31      else
32        point = filter(point)
33      end
34    end
35
36    if not dgn.in_bounds(point.x, point.y) then
37      return nil
38    end
39
40    return dgn.mons_at(point.x, point.y)
41  end
42  return filter_fn
43end
44
45-- rectangle_iterator
46iter.rectangle_iterator = {}
47
48function iter.rectangle_iterator:_new ()
49  local m = {}
50  setmetatable(m, self)
51  self.__index = self
52  return m
53end
54
55function iter.rectangle_iterator:new (corner1, corner2, filter, rvi)
56  if corner1 == nil or corner2 == nil then
57    error("need two corners to a rectangle")
58  end
59
60  if corner2.x < corner1.x then
61    error("corner2's x less than corner1's; did you swap them?")
62  elseif corner2.y < corner1.y then
63    error("corner2's y less than corner1's; did you swap them?")
64  end
65
66  local mt = iter.rectangle_iterator:_new()
67  mt.min_x = corner1.x
68  mt.min_y = corner1.y
69  mt.max_x = corner2.x
70  mt.max_y = corner2.y
71  mt.cur_x = corner1.x - 1
72  mt.cur_y = corner1.y
73  mt.filter = filter or nil
74  mt.rvi = rvi or false
75
76  return mt:iter()
77end
78
79function iter.rectangle_iterator:next()
80  local point = nil
81  repeat
82    if self.cur_x >= self.max_x then
83      self.cur_y = self.cur_y + 1
84      self.cur_x = self.min_x
85      if self.cur_y > self.max_y then
86        point = -1
87      else
88        point = self:check_filter(dgn.point(self.cur_x, self.cur_y))
89        if point == nil then
90          point = -2
91        end
92      end
93    else
94      self.cur_x = self.cur_x + 1
95      point = self:check_filter(dgn.point(self.cur_x, self.cur_y))
96      if point == nil then
97        point = -2
98      end
99    end
100    if point == -1 then
101      return nil
102    elseif point == -2 then
103      point = nil
104    end
105  until point
106
107  return point
108end
109
110function iter.rectangle_iterator:check_filter(point)
111  if self.filter ~= nil then
112    if self.filter(point) then
113      if self.rvi then
114        return self.filter(point)
115      else
116        return point
117      end
118    else
119      return nil
120    end
121  else
122    return point
123  end
124end
125
126function iter.rectangle_iterator:iter ()
127  return function() return self:next() end, nil, nil
128end
129
130function iter.rect_iterator(top_corner, bottom_corner, filter, rvi)
131  return iter.rectangle_iterator:new(top_corner, bottom_corner, filter, rvi)
132end
133
134function iter.rect_size_iterator(top_corner, size, filter, rvi)
135  return iter.rect_iterator(top_corner, top_corner + size - dgn.point(1, 1),
136                            filter, rvi)
137end
138
139function iter.mons_rect_iterator (top_corner, bottom_corner, filter)
140  return iter.rect_iterator(top_corner, bottom_corner, iter.monster_filter(filter), true)
141end
142
143function iter.border_iterator(top_corner, bottom_corner, rvi)
144  local function check_inner(point)
145    if point.x == top_corner.x or point.x == bottom_corner.x or point.y == top_corner.y or point.y == bottom_corner.y then
146      return point
147    end
148    return nil
149  end
150  return iter.rectangle_iterator:new(top_corner, bottom_corner, check_inner, rvi)
151end
152
153-- los_iterator
154function iter.los_iterator (ic, filter, center, rvi)
155  local y_x, y_y = you.pos()
156
157  if center ~= nil then
158    y_x, y_y = center:xy()
159  end
160
161  local include_center = ic
162  local top_corner = dgn.point(y_x - 8, y_y - 8)
163  local bottom_corner = dgn.point(y_x + 8, y_y + 8)
164
165  local function check_los (point)
166    local _x, _y = point:xy()
167    local npoint = true
168
169    if filter ~= nil then
170      if rvi then
171        npoint = filter(point)
172      end
173
174      if not filter(point) then
175        return nil
176      end
177    end
178
179    if y_x == _x and y_y == _y then
180      if rvi and include_center then
181        return npoint
182      else
183        return include_center
184      end
185    end
186
187    if not you.see_cell(_x, _y) then
188      return nil
189    end
190
191    if rvi then
192      return npoint
193    else
194      return true
195    end
196  end
197
198  return iter.rect_iterator(top_corner, bottom_corner, check_los, rvi)
199end
200
201function iter.mons_los_iterator (ic, filter, center)
202  return iter.los_iterator(ic, iter.monster_filter(filter), center, true)
203end
204
205-- adjacent_iterator
206function iter.adjacent_iterator (ic, filter, center, rvi)
207  local y_x, y_y = you.pos()
208
209  if center ~= nil then
210    y_x, y_y = center:xy()
211  end
212
213  local top_corner = dgn.point(y_x - 1, y_y - 1)
214  local bottom_corner = dgn.point(y_x + 1, y_y + 1)
215  local include_center = ic
216
217  local function check_adj (point)
218    local _x, _y = point:xy()
219    local npoint = point
220
221    if filter ~= nil then
222      if rvi then
223        npoint = filter(point)
224      end
225
226      if not filter(point) then
227        return nil
228      end
229    end
230
231    if y_x == _x and y_y == _y then
232      if rvi and include_center then
233        return npoint
234      else
235        return include_center
236      end
237    end
238
239    if rvi then
240      return npoint
241    else
242      return true
243    end
244  end
245
246  return iter.rect_iterator(top_corner, bottom_corner, check_adj, rvi)
247end
248
249function iter.mons_adjacent_iterator (ic, filter, center)
250  return iter.adjacent_iterator(ic, iter.monster_filter(filter), center, true)
251end
252
253function iter.adjacent_iterator_to(center, include_center, filter)
254  return iter.adjacent_iterator(include_center, filter, center, true)
255end
256
257-- circle_iterator
258function iter.circle_iterator (radius, ic, filter, center, rvi)
259  if radius == nil then
260    error("circle_iterator needs a radius")
261  end
262
263  local y_x, y_y = you.pos()
264
265  if center ~= nil then
266    y_x, y_y = center:xy()
267  end
268
269  local include_center = ic
270  local top_corner = dgn.point(y_x - radius, y_y - radius)
271  local bottom_corner = dgn.point(y_x + radius, y_y + radius)
272
273  local function check_dist (point)
274    local _x, _y = point:xy()
275    local dist = dgn.distance(y_x, y_y, _x, _y)
276    local npoint = nil
277
278    if filter ~= nil then
279      if rvi then
280        npoint = filter(point)
281      end
282
283      if not filter(point) then
284        return nil
285      end
286    end
287
288    if y_x == _x and y_y == _y then
289      if rvi and include_center then
290        return npoint
291      else
292        return include_center
293      end
294    end
295
296    if dist >= (radius * radius) + 1 then
297      return nil
298    end
299
300    if rvi then
301      return npoint
302    else
303      return true
304    end
305  end
306
307  return iter.rect_iterator(top_corner, bottom_corner, check_dist, rvi)
308end
309
310function iter.mons_circle_iterator (radius, ic, filter, center)
311  return iter.circle_iterator(radius, ic, iter.monster_filter(filter), center, true)
312end
313
314-- item stack-related functions
315--   not necessarily iterators
316function iter.stack_search (coord, term, extra)
317  local _x, _y
318  if extra ~= nil then
319    _x = coord
320    _y = term
321    term = extra
322  elseif coord == nil then
323    error("stack_search requires a location")
324  else
325    _x, _y = coord:xy()
326  end
327
328  if term == nil then
329    error("stack_search requires a search term")
330  end
331
332  local stack = dgn.items_at(_x, _y)
333  if #stack == 0 then
334    return nil
335  end
336
337  for _, item in ipairs(stack) do
338    if string.find(item.name(), (term)) then
339      return item
340    end
341  end
342
343  return nil
344end
345
346function iter.stack_destroy(coord, extra)
347  local _x, _y
348  if extra ~= nil then
349    _x = coord
350    _y = extra
351  elseif coord == nil then
352    error("stack_search requires a location")
353  else
354    _x, _y = coord:xy()
355  end
356
357  local stack = dgn.items_at(_x, _y)
358
359  while #stack ~= 0 do
360    local name = stack[1].name()
361    if stack[1].destroy() then
362      if #stack <= #dgn.items_at(_x, _y) then
363        error("destroyed an item ('" .. name .. "'), but the "
364              .. "stack size is the same")
365        return
366      end
367      stack = dgn.items_at(_x, _y)
368    else
369      error("couldn't destroy item '" .. name .. "'")
370      return false
371    end
372  end
373end
374
375-- subvault_iterator
376-- Iterates through all map locations in a subvault that will get written
377-- back to a parent.
378function iter.subvault_iterator (e, filter)
379
380  if e == nil then
381    error("subvault_iterator requires the env to be passed, e.g. _G")
382  end
383
384  local top_corner = dgn.point(0, 0)
385  local bottom_corner
386
387  local w,h = e.subvault_size()
388  if w == 0 or h == 0 then
389    -- Construct a valid rectangle.  subvault_cell_valid will always fail.
390    bottom_corner = top_corner
391  else
392    bottom_corner = dgn.point(w-1, h-1)
393  end
394
395
396  local function check_mask (point)
397    if filter ~= nil then
398      if not filter(point) then
399        return false
400      end
401    end
402
403    return e.subvault_cell_valid(point.x, point.y)
404  end
405
406  return iter.rect_iterator(top_corner, bottom_corner, check_mask)
407end
408
409-- Marker iterators
410--   firstly, a point iterator. It's really just a fancy ipairs(), but stateful
411--   and with the ability to filter points.
412
413iter.point_iterator = {}
414
415function iter.point_iterator:_new ()
416  local m = {}
417  setmetatable(m, self)
418  self.__index = self
419  return m
420end
421
422function iter.point_iterator:new (ptable, filter, rv_instead)
423  if ptable == nil then
424    error("ptable cannot be nil for point_iterator")
425  end
426
427  local mt  = iter.point_iterator:_new()
428  mt.cur_p  = 0
429  mt.table  = ptable
430  mt.rvi    = rv_instead or false
431  mt.filter = filter or nil
432
433  return mt:iter()
434end
435
436function iter.point_iterator:next()
437  local point = nil
438  local q = 0
439  repeat
440    q = q + 1
441    self.cur_p = self.cur_p + 1
442    point = self:check_filter(self.table[self.cur_p])
443  until point or q == 10
444
445  return point
446end
447
448function iter.point_iterator:check_filter(point)
449  if self.filter ~= nil then
450    if self.filter(point) then
451      if self.rvi then
452        return self.filter(point)
453      else
454        return point
455      end
456    else
457      return nil
458    end
459  else
460    return point
461  end
462end
463
464function iter.point_iterator:iter ()
465  return function() return self:next() end, nil, nil
466end
467
468-- An easier and more posh way of interfacing with find_marker_positions_by_prop.
469function iter.slave_iterator (prop, value)
470  local ptable = dgn.find_marker_positions_by_prop(prop, value)
471  if #ptable == 0 then
472    error("Didn't find any props for " .. prop .. "=" .. value)
473  else
474    return iter.point_iterator:new(ptable)
475  end
476end
477
478--- Inventory iterator.
479-- Iterates over tables of @{items.Item} objects, possibly filtering them.
480-- @usage `for item in iter.invent_iterator()
481--   if item.is_useless then
482--     item:drop()
483--   end
484-- end`
485-- @type iter.invent_iterator
486
487iter.invent_iterator = {}
488
489function iter.invent_iterator:_new ()
490  local m = {}
491  setmetatable(m, self)
492  self.__index = self
493  return m
494end
495
496--- Create a new inventory iterator
497-- @param itable the inventory table
498-- @param[opt] filter filter function
499-- @param[optchain=false] rv_instead return the rv of the filter instead
500function iter.invent_iterator:new (itable, filter, rv_instead)
501  if itable == nil then
502    error("itable cannot be nil for invent_iterator")
503  end
504
505  local mt  = iter.invent_iterator:_new()
506  mt.cur_p  = 0
507  mt.table  = itable
508  mt.rvi    = rv_instead or false
509  mt.filter = filter or nil
510
511  return mt:iter()
512end
513
514--- Advance the iterator
515function iter.invent_iterator:next()
516  local point = nil
517  local q = 0
518  repeat
519    q = q + 1
520    self.cur_p = self.cur_p + 1
521    point = self:check_filter(self.table[self.cur_p])
522  until point or q == 10
523
524  return point
525end
526
527--- Check an item agains the iterator's filter
528-- @param item
529-- @return either the item or filter(item) if rv is asked for
530function iter.invent_iterator:check_filter(item)
531  if self.filter ~= nil then
532    if self.filter(item) then
533      if self.rvi then
534        return self.filter(item)
535      else
536        return item
537      end
538    else
539      return nil
540    end
541  else
542    return item
543  end
544end
545
546function iter.invent_iterator:iter ()
547  return function() return self:next() end, nil, nil
548end
549
550--- An easier and more posh way of interfacing with inventory.
551function iter.inventory_iterator ()
552  return iter.invent_iterator:new(items.inventory())
553end
554
555function iter.stash_iterator (point, y)
556  if y == nil then
557    return iter.invent_iterator:new(dgn.items_at(point.x, point.y))
558  else
559    return iter.invent_iterator:new(dgn.items_at(point, y))
560  end
561end
562