1 /**
2  * @file
3  * @brief Showing the level map (X and background).
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "viewmap.h"
9 
10 #include <algorithm>
11 
12 #include "branch.h"
13 #include "colour.h"
14 #include "command.h"
15 #include "coord.h"
16 #include "coordit.h"
17 #include "dgn-overview.h"
18 #include "directn.h"
19 #include "env.h"
20 #include "files.h"
21 #include "format.h"
22 #include "fprop.h"
23 #include "libutil.h"
24 #include "macro.h"
25 #include "map-knowledge.h"
26 #include "message.h"
27 #include "options.h"
28 #include "output.h"
29 #include "showsymb.h"
30 #include "state.h"
31 #include "stringutil.h"
32 #include "terrain.h"
33 #include "tileview.h"
34 #include "tiles-build-specific.h"
35 #include "travel.h"
36 #include "ui.h"
37 #include "unicode.h"
38 #include "view.h"
39 #include "viewchar.h"
40 #include "viewgeom.h"
41 
42 #ifdef USE_TILE
43 #endif
44 
45 #ifndef USE_TILE_LOCAL
46 /**
47  * Get a console colour for representing the travel possibility at the given
48  * position based on the last travel update. Used when drawing the console map.
49  *
50  * @param p The position.
51  * @returns An unsigned int value for the colour.
52 */
_get_travel_colour(const coord_def & p)53 static unsigned _get_travel_colour(const coord_def& p)
54 {
55 #ifdef WIZARD
56     if (you.wizard && testbits(env.pgrid(p), FPROP_HIGHLIGHT))
57         return LIGHTGREEN;
58 #endif
59 
60     if (is_waypoint(p))
61         return LIGHTGREEN;
62 
63     if (is_stair_exclusion(p))
64         return Options.tc_excluded;
65 
66     const unsigned no_travel_col
67         = feat_is_traversable(env.grid(p)) ? Options.tc_forbidden
68                                       : Options.tc_dangerous;
69 
70     const short dist = travel_point_distance[p.x][p.y];
71     return dist > 0?                    Options.tc_reachable        :
72            dist == PD_EXCLUDED ?        Options.tc_excluded         :
73            dist == PD_EXCLUDED_RADIUS ? Options.tc_exclude_circle   :
74            dist < 0?                    no_travel_col               :
75                                         Options.tc_disconnected;
76 }
77 #endif
78 
79 #ifndef USE_TILE_LOCAL
travel_colour_override(const coord_def & p)80 bool travel_colour_override(const coord_def& p)
81 {
82     if (is_waypoint(p) || is_stair_exclusion(p)
83        || travel_point_distance[p.x][p.y] == PD_EXCLUDED)
84     {
85         return true;
86     }
87 #ifdef WIZARD
88     if (you.wizard && testbits(env.pgrid(p), FPROP_HIGHLIGHT))
89         return true;
90 #endif
91 
92     const map_cell& cell = env.map_knowledge(p);
93     show_class cls = get_cell_show_class(cell);
94     if (cls == SH_FEATURE)
95     {
96         switch (cell.feat())
97         {
98         case DNGN_FLOOR:
99         case DNGN_LAVA:
100         case DNGN_DEEP_WATER:
101         case DNGN_SHALLOW_WATER:
102             return true;
103         default:
104             return false;
105         }
106     }
107     else
108         return false;
109 }
110 
_get_sightmap_char(dungeon_feature_type feat)111 static char32_t _get_sightmap_char(dungeon_feature_type feat)
112 {
113     return get_feature_def(feat).symbol();
114 }
115 
_get_magicmap_char(dungeon_feature_type feat)116 static char32_t _get_magicmap_char(dungeon_feature_type feat)
117 {
118     return get_feature_def(feat).magic_symbol();
119 }
120 #endif
121 
_is_player_defined_feature(char32_t feature)122 static bool _is_player_defined_feature(char32_t feature)
123 {
124     return feature == 'E' || feature == 'F' || feature == 'W';
125 }
126 
127 // Determines if the given feature is present at (x, y) in _feat_ coordinates.
128 // If you have map coords, add (1, 1) to get grid coords.
129 // Use one of
130 // 1. '<' and '>' to look for stairs
131 // 2. '\t' or '\\' for shops, portals.
132 // 3. '^' for traps
133 // 4. '_' for altars
134 // 5. Anything else will look for the exact same character in the level map.
is_feature(char32_t feature,const coord_def & where)135 bool is_feature(char32_t feature, const coord_def& where)
136 {
137     if (!env.map_knowledge(where).known() && !you.see_cell(where) && !_is_player_defined_feature(feature))
138         return false;
139 
140     dungeon_feature_type grid = env.map_knowledge(where).feat();
141 
142     switch (feature)
143     {
144     case 'E':
145         return is_exclude_root(where);
146     case 'F':
147     case 'W':
148         return is_waypoint(where);
149     case 'I':
150         return is_stash(where);
151     case '_':
152         return feat_is_altar(grid) || grid == DNGN_UNKNOWN_ALTAR;
153     case '\t':
154     case '\\':
155         return feat_is_gate(grid) || grid == DNGN_ENTER_SHOP
156                || grid == DNGN_UNKNOWN_PORTAL;
157     case '<':
158         // DNGN_UNKNOWN_ALTAR doesn't need to be excluded here, because
159         // feat_is_altar doesn't include it in the first place.
160         return feat_stair_direction(grid) == CMD_GO_UPSTAIRS
161                 && !feat_is_altar(grid)
162                 && grid != DNGN_ENTER_SHOP
163                 && grid != DNGN_TRANSPORTER;
164     case '>':
165         return feat_stair_direction(grid) == CMD_GO_DOWNSTAIRS
166                 && !feat_is_altar(grid)
167                 && grid != DNGN_ENTER_SHOP
168                 && grid != DNGN_TRANSPORTER;
169     case '^':
170         return feat_is_trap(grid);
171     default:
172         return get_cell_glyph(where).ch == feature;
173     }
174 }
175 
_is_feature_fudged(char32_t glyph,const coord_def & where)176 static bool _is_feature_fudged(char32_t glyph, const coord_def& where)
177 {
178     if (!env.map_knowledge(where).known() && !_is_player_defined_feature(glyph))
179         return false;
180 
181     if (is_feature(glyph, where))
182         return true;
183 
184     if (glyph == '<')
185     {
186         return env.grid(where) == DNGN_EXIT_ABYSS
187                || env.grid(where) == DNGN_EXIT_PANDEMONIUM
188                || env.grid(where) == DNGN_ENTER_HELL && player_in_hell();
189     }
190     else if (glyph == '>')
191     {
192         return env.grid(where) == DNGN_TRANSIT_PANDEMONIUM
193                || env.grid(where) == DNGN_TRANSPORTER;
194     }
195 
196     return false;
197 }
198 
search_path_around_point(coord_def centre)199 vector<coord_def> search_path_around_point(coord_def centre)
200 {
201     vector<coord_def> points;
202 
203     int cx = centre.x, cy = centre.y;
204 
205     // Find the first occurrence of given glyph, spiralling around (x,y)
206     int maxradius = GXM > GYM ? GXM : GYM;
207     for (int radius = 1; radius < maxradius; ++radius)
208         for (int axis = -2; axis < 2; ++axis)
209         {
210             int rad = radius - (axis < 0);
211             for (int var = -rad; var <= rad; ++var)
212             {
213                 int dx = radius, dy = var;
214                 if (axis % 2)
215                     dx = -dx;
216                 if (axis < 0)
217                 {
218                     int temp = dx;
219                     dx = dy;
220                     dy = temp;
221                 }
222 
223                 const auto x = cx + dx, y = cy + dy;
224 
225                 if (in_bounds(x, y))
226                     points.emplace_back(x, y);
227             }
228         }
229 
230     return points;
231 }
232 
_find_feature(const vector<coord_def> & features,char32_t feature,int ignore_count,coord_def & out_pos,bool forward)233 static int _find_feature(const vector<coord_def>& features,
234                          char32_t feature,
235                          int ignore_count,
236                          coord_def &out_pos,
237                          bool forward)
238 {
239     int firstx = -1, firsty = -1, firstmatch = -1;
240     int matchcount = 0;
241 
242     for (coord_def coord : features)
243     {
244         if (_is_feature_fudged(feature, coord))
245         {
246             ++matchcount;
247             if (forward? !ignore_count-- : --ignore_count == 1)
248             {
249                 out_pos = coord;
250                 // We want to cursor to (x,y)
251                 return matchcount;
252             }
253             else if (!forward || firstx == -1)
254             {
255                 firstx = coord.x;
256                 firsty = coord.y;
257                 firstmatch = matchcount;
258             }
259         }
260     }
261 
262     // We found something, but ignored it because of an ignorecount
263     if (firstx != -1)
264     {
265         out_pos = coord_def(firstx, firsty);
266         return firstmatch;
267     }
268     return 0;
269 }
270 
271 #ifndef USE_TILE_LOCAL
_get_number_of_lines_levelmap()272 static int _get_number_of_lines_levelmap()
273 {
274     return get_number_of_lines() - 1;
275 }
276 
_draw_level_map(int start_x,int start_y,bool travel_mode,bool on_level,ui::Region region)277 static void _draw_level_map(int start_x, int start_y, bool travel_mode,
278         bool on_level, ui::Region region)
279 {
280     region.width = min(region.width, GXM);
281     region.height = min(region.height, GYM);
282 
283     const coord_def extents(region.width, region.height);
284     crawl_view_buffer vbuf(extents);
285     screen_cell_t *cell = vbuf;
286 
287     cursor_control cs(false);
288 
289     for (int screen_y = 0; screen_y < region.height; screen_y++)
290         for (int screen_x = 0; screen_x < region.width; screen_x++)
291         {
292             coord_def c(start_x + screen_x, start_y + screen_y);
293 
294             if (!map_bounds(c))
295             {
296                 cell->colour = DARKGREY;
297                 cell->glyph  = 0;
298             }
299             else
300             {
301                 cglyph_t g = get_cell_glyph(c, false, -1);
302                 cell->glyph = g.ch;
303                 cell->colour = g.col;
304 
305                 const show_class show = get_cell_show_class(env.map_knowledge(c));
306 
307                 if (show == SH_NOTHING && is_explore_horizon(c))
308                 {
309                     const feature_def& fd = get_feature_def(DNGN_EXPLORE_HORIZON);
310                     cell->glyph = fd.symbol();
311                     cell->colour = fd.colour();
312                 }
313 
314                 if (travel_mode && travel_colour_override(c))
315                     cell->colour = _get_travel_colour(c);
316 
317                 if (c == you.pos() && !crawl_state.arena_suspended && on_level)
318                 {
319                     // [dshaligram] Draw the @ symbol on the
320                     // level-map. It's no longer saved into the
321                     // env.map_knowledge, so we need to draw it
322                     // directly.
323                     cell->colour = WHITE;
324                     cell->glyph  = mons_char(you.symbol);
325                 }
326 
327                 // If we've a waypoint on the current square, *and* the
328                 // square is a normal floor square with nothing on it,
329                 // show the waypoint number.
330                 // XXX: This is a horrible hack.
331                 char32_t bc   = cell->glyph;
332                 uint8_t ch = is_waypoint(c);
333                 if (ch && (bc == _get_sightmap_char(DNGN_FLOOR)
334                            || bc == _get_magicmap_char(DNGN_FLOOR)))
335                 {
336                     cell->glyph = ch;
337                 }
338 
339                 if (Options.show_travel_trail && travel_trail_index(c) >= 0)
340                 {
341                     const feature_def& fd = get_feature_def(DNGN_TRAVEL_TRAIL);
342 
343                     // Don't overwrite the player's symbol
344                     if (fd.symbol() && c != you.pos())
345                         cell->glyph = fd.symbol();
346                     if (fd.colour() != COLOUR_UNDEF)
347                         cell->colour = fd.colour();
348 
349                     cell->colour |= COLFLAG_REVERSE;
350                 }
351             }
352 
353             cell++;
354         }
355 
356     puttext(region.x + 1, region.y + 1, vbuf);
357 }
358 #endif // !USE_TILE_LOCAL
359 
_reset_travel_colours(vector<coord_def> & features,bool on_level)360 static void _reset_travel_colours(vector<coord_def> &features, bool on_level)
361 {
362     // We now need to redo travel colours.
363     features.clear();
364 
365     if (on_level)
366         fill_travel_point_distance(you.pos(), &features);
367     else
368     {
369         travel_pathfind tp;
370         tp.set_feature_vector(&features);
371         tp.get_features();
372     }
373 }
374 
375 // Sort glyphs within a group, for the feature list.
_comp_glyphs(const cglyph_t & g1,const cglyph_t & g2)376 static bool _comp_glyphs(const cglyph_t& g1, const cglyph_t& g2)
377 {
378     return g1.ch < g2.ch || g1.ch == g2.ch && g1.col < g2.col;
379 }
380 
381 #ifndef USE_TILE_LOCAL
382 static cglyph_t _get_feat_glyph(const coord_def& gc);
383 #endif
384 
385 class feature_list
386 {
387     enum group
388     {
389         G_UP, G_DOWN, G_PORTAL, G_OTHER, G_NONE, NUM_GROUPS = G_NONE
390     };
391 
392     vector<cglyph_t> data[NUM_GROUPS];
393 
feat_dir(dungeon_feature_type feat)394     static group feat_dir(dungeon_feature_type feat)
395     {
396         switch (feat_stair_direction(feat))
397         {
398         case CMD_GO_UPSTAIRS:
399             return G_UP;
400         case CMD_GO_DOWNSTAIRS:
401             return G_DOWN;
402         default:
403             return G_NONE;
404         }
405     }
406 
get_group(const coord_def & gc)407     group get_group(const coord_def& gc)
408     {
409         dungeon_feature_type feat = env.map_knowledge(gc).feat();
410 
411         if (feat_is_staircase(feat) || feat_is_escape_hatch(feat))
412             return feat_dir(feat);
413         if (feat == DNGN_TRAP_SHAFT)
414             return G_DOWN;
415         if (feat_is_altar(feat) || feat == DNGN_ENTER_SHOP)
416             return G_OTHER;
417         if (get_feature_dchar(feat) == DCHAR_ARCH)
418             return G_PORTAL;
419         return G_NONE;
420     }
421 
maybe_add(const coord_def & gc)422     void maybe_add(const coord_def& gc)
423     {
424 #ifndef USE_TILE_LOCAL
425         if (!env.map_knowledge(gc).known())
426             return;
427 
428         group grp = get_group(gc);
429         if (grp != G_NONE)
430             data[grp].push_back(_get_feat_glyph(gc));
431 #else
432         UNUSED(gc);
433 #endif
434     }
435 
436 public:
init()437     void init()
438     {
439         for (vector<cglyph_t> &groupdata : data)
440             groupdata.clear();
441         for (rectangle_iterator ri(0); ri; ++ri)
442             maybe_add(*ri);
443         for (vector<cglyph_t> &groupdata : data)
444             sort(begin(groupdata), end(groupdata), _comp_glyphs);
445     }
446 
format() const447     formatted_string format() const
448     {
449         formatted_string s;
450         for (const vector<cglyph_t> &groupdata : data)
451             for (cglyph_t gly : groupdata)
452                 s.add_glyph(gly);
453         return s;
454     }
455 };
456 
457 #ifndef USE_TILE_LOCAL
_draw_title(const coord_def & cpos,const feature_list & feats,const int columns)458 static void _draw_title(const coord_def& cpos, const feature_list& feats, const int columns)
459 {
460     const formatted_string help =
461         formatted_string::parse_string("(Press <w>?</w> for help)");
462     const int helplen = help.width();
463 
464     if (columns < helplen)
465         return;
466 
467     const formatted_string title = feats.format();
468     const int titlelen = title.width();
469     if (columns < titlelen)
470         return;
471 
472     string pstr = "";
473 #ifdef WIZARD
474     if (you.wizard)
475     {
476         char buf[10];
477         snprintf(buf, sizeof(buf), " (%d, %d)", cpos.x, cpos.y);
478         pstr = string(buf);
479     }
480 #endif // WIZARD
481 
482     cgotoxy(1, 1);
483     textcolour(WHITE);
484 
485     cprintf("%s", chop_string(
486                     uppercase_first(level_id::current().describe(true, true))
487                       + pstr, columns - helplen).c_str());
488 
489     cgotoxy(max(1, (columns - titlelen) / 2), 1);
490     title.display();
491 
492     textcolour(LIGHTGREY);
493     cgotoxy(max(1, columns - helplen + 1), 1);
494     help.display();
495 }
496 #endif
497 
_stair_dest(const coord_def & p,command_type dir)498 static level_pos _stair_dest(const coord_def& p, command_type dir)
499 {
500     if (!in_bounds(p))
501         return level_pos();
502 
503     if (feat_stair_direction(env.map_knowledge(p).feat()) != dir)
504         return level_pos();
505 
506     LevelInfo *linf = travel_cache.find_level_info(level_id::current());
507     if (!linf)
508         return level_pos();
509 
510     const stair_info *sinf = linf->get_stair(p);
511     if (!sinf)
512         return level_pos();
513 
514     return sinf->destination;
515 }
516 
_unforget_map()517 static void _unforget_map()
518 {
519     ASSERT(env.map_forgotten);
520     MapKnowledge &old(*env.map_forgotten);
521 
522     for (rectangle_iterator ri(0); ri; ++ri)
523         if (!env.map_knowledge(*ri).seen() && old(*ri).seen())
524         {
525             // Don't overwrite known squares, nor magic-mapped with
526             // magic-mapped data -- what was forgotten is less up to date.
527             env.map_knowledge(*ri) = old(*ri);
528             env.map_seen.set(*ri);
529 #ifdef USE_TILE
530             tiles.update_minimap(*ri);
531 #endif
532         }
533 }
534 
_forget_map(bool wizard_forget=false)535 static void _forget_map(bool wizard_forget = false)
536 {
537     for (rectangle_iterator ri(0); ri; ++ri)
538     {
539         auto& flags = env.map_knowledge(*ri).flags;
540         // don't touch squares we can currently see
541         if (flags & MAP_VISIBLE_FLAG)
542             continue;
543         if (wizard_forget)
544         {
545             env.map_knowledge(*ri).clear();
546 #ifdef USE_TILE
547             tile_forget_map(*ri);
548 #endif
549         }
550         else if (flags & MAP_SEEN_FLAG)
551         {
552             // squares we've seen in the past, pretend we've mapped instead
553             flags |= MAP_MAGIC_MAPPED_FLAG;
554             flags &= ~MAP_SEEN_FLAG;
555         }
556         env.map_seen.set(*ri, false);
557 #ifdef USE_TILE
558         tiles.update_minimap(*ri);
559 #endif
560     }
561 }
562 
563 map_control_state process_map_command(command_type cmd, const map_control_state &state);
564 
_recentre_map_target(const level_id level,const level_id original)565 static coord_def _recentre_map_target(const level_id level,
566                                       const level_id original)
567 {
568     if (level == original)
569         return you.pos();
570 
571     const auto bounds = known_map_bounds();
572     return (bounds.first + bounds.second + 1) / 2;
573 }
574 
575 #ifndef USE_TILE_LOCAL
_get_view_state(coord_def cur_ul,const map_control_state & state)576 static map_view_state _get_view_state(coord_def cur_ul, const map_control_state& state)
577 {
578     // TODO: why should local tiles use different logic?
579     // also, these might make more sense as member functions of UIMapView.
580 
581     // recalculate cursor position, and scroll if necessary
582     map_view_state view;
583     const int num_lines = _get_number_of_lines_levelmap();
584 
585     view.start.x = cur_ul.x;
586     view.start.y = cur_ul.y;
587 
588     // Scroll when needed to keep MARGIN rows/cols to the left or right of the
589     // cursor -- as long as there is map to scroll to.
590     const int MARGIN = 3;
591 
592     // careful with off-by-one errors in these calcs
593     view.cursor = state.lpos.pos - view.start + coord_def(1,1);
594     const auto bounds = known_map_bounds();
595     const coord_def map_min = state.lpos.pos - bounds.first;
596     const coord_def map_max = bounds.second - state.lpos.pos;
597     const coord_def ul_margin(max(0, min(MARGIN, map_min.x)),
598                               max(0, min(MARGIN, map_min.y)));
599     const coord_def lr_margin(max(0, min(MARGIN, map_max.x)),
600                               max(0, min(MARGIN, map_max.y)));
601 
602     // First check upper-left margins
603     // In principle x doesn't need to scroll; a level can't be larger than 80
604     // columns. But let's make the logic general.
605     coord_def shift = coord_def(min(0, view.cursor.x - ul_margin.x - 1),
606                                 min(0, view.cursor.y - ul_margin.y - 1));
607 
608     // If we didn't enforce any margins on the upper-left, check lower-right
609     if (shift.x >= 0)
610         shift.x = max(0, view.cursor.x - get_number_of_cols() + lr_margin.x);
611     if (shift.y >= 0)
612         shift.y = max(0, view.cursor.y - num_lines + lr_margin.y);
613 
614     view.start += shift;
615 
616     // now calculate the final cursor position relative to any view shifting
617     // that happened to enforce margins
618     view.cursor = state.lpos.pos - view.start + coord_def(1,1);
619     return view;
620 }
621 
_init_view_state(const map_control_state & state)622 static map_view_state _init_view_state(const map_control_state& state)
623 {
624     // initial placement calculations for the map viewport window.
625     // This code is a bit inscrutable but it seems to do some heuristics to
626     // try to make partially explored levels look reasonable in console map
627     // view, mostly to do with placement in the x direction.
628     const int num_lines = _get_number_of_lines_levelmap();
629     const int half_screen = (num_lines - 1) / 2;
630 
631     const auto bounds = known_map_bounds();
632     const auto min_x = bounds.first.x;
633     const auto min_y = bounds.first.y;
634     const auto max_x = bounds.second.x;
635     const auto max_y = bounds.second.y;
636 
637     const auto map_lines = max_y - min_y + 1;
638 
639     coord_def ul((min_x + max_x + 1) / 2 - 40, 0);
640 
641     auto screen_y = state.lpos.pos.y;
642 
643     // If close to top of known map, put min_y on top
644     // else if close to bottom of known map, put max_y on bottom.
645     //
646     // The num_lines comparisons are done to keep things neat, by
647     // keeping things at the top of the screen. By shifting an
648     // additional one in the num_lines > map_lines case, we can
649     // keep the top line clear... which makes things look a whole
650     // lot better for small maps.
651     if (num_lines > map_lines)
652         screen_y = min_y + half_screen - 1;
653     else if (num_lines == map_lines || screen_y - half_screen < min_y)
654         screen_y = min_y + half_screen;
655     else if (screen_y + half_screen > max_y)
656         screen_y = max_y - half_screen;
657 
658     ul.y = screen_y - half_screen;
659 
660     // Finally, adjust the upper left corner for the view state so that it is
661     // appropriately scrolled relative to the cursor.
662     return _get_view_state(ul, state);
663 }
664 #endif
665 
666 class UIMapView : public ui::Widget
667 {
668 public:
UIMapView(level_pos & lpos,levelview_excursion & le,bool travel_mode,bool allow_offlevel)669     UIMapView(level_pos& lpos, levelview_excursion& le, bool travel_mode,
670               bool allow_offlevel)
671     {
672         m_state.lpos = lpos;
673         m_state.features = &m_features;
674         m_state.feats = &m_feats;
675         m_state.excursion = &le;
676         m_state.allow_offlevel = allow_offlevel;
677         m_state.travel_mode = travel_mode;
678         m_state.original = level_id::current();
679         m_state.map_alive = true;
680         m_state.redraw_map = true;
681         m_state.search_anchor = coord_def(-1, -1);
682         m_state.chose = false;
683         m_state.on_level = true;
684 
685         goto_level();
686     }
~UIMapView()687     ~UIMapView() {}
688 
_render()689     void _render() override
690     {
691 #ifdef USE_TILE
692         tiles.load_dungeon(m_state.lpos.pos);
693 #endif
694 
695 #ifdef USE_TILE_LOCAL
696         display_message_window();
697         tiles.render_current_regions();
698         glmanager->reset_transform();
699 #endif
700 
701 #ifndef USE_TILE_LOCAL
702         const auto view = _get_view_state(view_ul, m_state);
703         view_ul = view.start;
704         _draw_title(m_state.lpos.pos, *m_state.feats, m_region.width);
705         const ui::Region map_region = {0, 1, m_region.width, m_region.height - 1};
706         _draw_level_map(view_ul.x, view_ul.y, m_state.travel_mode,
707                         m_state.on_level, map_region);
708         // the `+ 1` here is for the map overview line
709         ui::show_cursor_at(view.cursor.x, view.cursor.y + 1);
710 #endif
711     }
712 
_allocate_region()713     void _allocate_region() override
714     {
715         // Note: Tile versions just center on the current cursor
716         // location. It silently ignores everything else going
717         // on in this function.  --Enne
718 #ifdef USE_TILE_LOCAL
719         if (first_run)
720         {
721             tiles.update_tabs();
722             first_run = false;
723         }
724 #endif
725         _expose();
726     }
727 
on_event(const ui::Event & ev)728     bool on_event(const ui::Event& ev) override
729     {
730         if (ev.type() == ui::Event::Type::KeyDown)
731         {
732             auto key = static_cast<const ui::KeyEvent&>(ev).key();
733 #ifndef USE_TILE_LOCAL
734             key = unmangle_direction_keys(key, KMC_LEVELMAP);
735 #endif
736             command_type cmd = key_to_command(key, KMC_LEVELMAP);
737             if (cmd < CMD_MIN_OVERMAP || cmd > CMD_MAX_OVERMAP)
738                 cmd = CMD_NO_CMD;
739             process_command(cmd);
740             if (m_state.redraw_map)
741                 _expose();
742             return true;
743         }
744 
745 #ifdef USE_TILE_LOCAL
746         if (ev.type() == ui::Event::Type::MouseMove
747             || ev.type() == ui::Event::Type::MouseDown)
748         {
749             auto wm_event = to_wm_event(static_cast<const ui::MouseEvent&>(ev));
750             tiles.handle_mouse(wm_event);
751             if (ev.type() == ui::Event::Type::MouseDown)
752             {
753                 if (tiles.get_cursor() == m_state.lpos.pos)
754                 {
755                     process_command(CMD_MAP_GOTO_TARGET);
756                     return true;
757                 }
758                 else
759                 {
760                     m_state.lpos.pos
761                         = tiles.get_cursor().clamped(known_map_bounds());
762                 }
763             }
764             _expose();
765             return true;
766         }
767 #endif
768 
769         return false;
770     }
771 
process_command(command_type cmd)772     void process_command(command_type cmd)
773     {
774         m_state = process_map_command(cmd, m_state);
775         if (!m_state.map_alive)
776             return;
777 
778         if (m_state.lpos.id != level_id::current())
779             goto_level();
780 
781         m_state.lpos.pos = m_state.lpos.pos.clamped(known_map_bounds());
782     }
783 
goto_level()784     void goto_level()
785     {
786         if (m_state.lpos.id != level_id::current())
787             m_state.excursion->go_to(m_state.lpos.id);
788 
789         m_state.on_level = (level_id::current() == m_state.original);
790 
791         // Vector to track all state.features we can travel to, in
792         // order of distance.
793         if (m_state.travel_mode)
794             _reset_travel_colours(*m_state.features, m_state.on_level);
795         m_state.feats->init();
796         m_state.search_index = 0;
797         m_state.search_anchor.reset();
798 
799         // This happens when CMD_MAP_PREV_LEVEL etc. assign dest to lpos,
800         // with a position == (-1, -1).
801         if (!map_bounds(m_state.lpos.pos))
802         {
803             m_state.lpos.pos =
804                 _recentre_map_target(m_state.lpos.id, m_state.original);
805             m_state.lpos.id = level_id::current();
806         }
807 #ifndef USE_TILE_LOCAL
808         auto s = _init_view_state(m_state);
809         view_ul = s.start;
810 #endif
811 
812         _expose();
813     }
814 
is_alive() const815     bool is_alive() const
816     {
817         return m_state.map_alive;
818     }
819 
chose() const820     bool chose() const
821     {
822         return m_state.chose;
823     }
824 
lpos() const825     level_pos lpos() const
826     {
827         return m_state.lpos;
828     }
829 
830 private:
831     map_control_state m_state;
832 
833     // Vector to track all features we can travel to, in order of distance.
834     vector<coord_def> m_features;
835     // List of all interesting features for display in the (console) title.
836     feature_list m_feats;
837 
838 #ifndef USE_TILE_LOCAL
839     coord_def view_ul;
840 #endif
841 
842 #ifdef USE_TILE_LOCAL
843     bool first_run = true;
844 #endif
845 };
846 
847 // show_map() now centers the known map along x or y. This prevents
848 // the player from getting "artificial" location clues by using the
849 // map to see how close to the end they are. They'll need to explore
850 // to get that. This function is still a mess, though. -- bwr
show_map(level_pos & lpos,bool travel_mode,bool allow_offlevel)851 bool show_map(level_pos &lpos, bool travel_mode, bool allow_offlevel)
852 {
853 #ifdef USE_TILE_LOCAL
854     mouse_control mc(MOUSE_MODE_NORMAL);
855     tiles.do_map_display();
856 #endif
857 
858 #ifdef USE_TILE
859     ui::cutoff_point ui_cutoff_point;
860 #endif
861 #ifdef USE_TILE_WEB
862     tiles_ui_control ui(UI_VIEW_MAP);
863 #endif
864 
865     if (!lpos.id.is_valid() || !allow_offlevel)
866         lpos.id = level_id::current();
867 
868     levelview_excursion le(travel_mode);
869     auto map_view = make_shared<UIMapView>(lpos, le, travel_mode, allow_offlevel);
870 
871 #ifdef USE_TILE_LOCAL
872     unwind_bool inhibit_rendering(ui::should_render_current_regions, false);
873 #else
874     cursor_control cc(!Options.use_fake_cursor);
875 #endif
876 
877     ui::push_layout(map_view);
878     while (map_view->is_alive() && !crawl_state.seen_hups)
879         ui::pump_events();
880     ui::pop_layout();
881 
882 #ifdef USE_TILE_LOCAL
883     tiles.set_map_display(false);
884 #endif
885 #ifdef USE_TILE
886     tiles.place_cursor(CURSOR_MAP, NO_CURSOR);
887 #endif
888 
889     lpos = map_view->lpos();
890     return map_view->chose();
891 }
892 
process_map_command(command_type cmd,const map_control_state & prev_state)893 map_control_state process_map_command(command_type cmd, const map_control_state& prev_state)
894 {
895     // the map needs a cursor, but we need to hide it here
896     cursor_control cc(false);
897     map_control_state state = prev_state;
898     state.map_alive = true;
899     state.chose = false;
900 
901     const auto block_step = Options.level_map_cursor_step;
902 
903     switch (cmd)
904     {
905     case CMD_MAP_HELP:
906         show_levelmap_help();
907         break;
908 
909     case CMD_MAP_CLEAR_MAP:
910         clear_map_or_travel_trail();
911         break;
912 
913 #ifdef WIZARD
914     case CMD_MAP_WIZARD_FORGET:
915         {
916             // this doesn't seem useful outside of debugging and may
917             // be buggy in unexpected ways, so wizmode-only. (Though
918             // it doesn't leak information or anything.)
919             if (!you.wizard)
920                 break;
921             if (env.map_forgotten)
922                 _unforget_map();
923             MapKnowledge *old = new MapKnowledge(env.map_knowledge);
924             // completely wipe out map
925             _forget_map(true);
926             env.map_forgotten.reset(old);
927             mpr("Level map wiped.");
928             break;
929         }
930 #endif
931 
932     case CMD_MAP_FORGET:
933         {
934             // Merge it with already forgotten data first.
935             if (env.map_forgotten)
936                 _unforget_map();
937             MapKnowledge *old = new MapKnowledge(env.map_knowledge);
938             _forget_map();
939             env.map_forgotten.reset(old);
940             mpr("Level map cleared.");
941         }
942         break;
943 
944     case CMD_MAP_UNFORGET:
945         if (env.map_forgotten)
946         {
947             _unforget_map();
948             env.map_forgotten.reset();
949             mpr("Remembered map restored.");
950         }
951         else
952             mpr("No remembered map.");
953         break;
954 
955     case CMD_MAP_ADD_WAYPOINT:
956         travel_cache.add_waypoint(state.lpos.pos.x, state.lpos.pos.y);
957         // We need to do this all over again so that the user can jump
958         // to the waypoint he just created.
959         _reset_travel_colours(*state.features, state.on_level);
960         state.feats->init();
961         break;
962 
963         // Cycle the radius of an exclude.
964     case CMD_MAP_EXCLUDE_AREA:
965         if (!is_map_persistent())
966             break;
967 
968         cycle_exclude_radius(state.lpos.pos);
969 
970         _reset_travel_colours(*state.features, state.on_level);
971         state.feats->init();
972         break;
973 
974     case CMD_MAP_CLEAR_EXCLUDES:
975         clear_excludes();
976         _reset_travel_colours(*state.features, state.on_level);
977         state.feats->init();
978         break;
979 
980 #ifdef WIZARD
981     case CMD_MAP_EXCLUDE_RADIUS:
982         set_exclude(state.lpos.pos, getchm() - '0');
983 
984         _reset_travel_colours(*state.features, state.on_level);
985         state.feats->init();
986         break;
987 #endif
988 
989     case CMD_MAP_MOVE_DOWN_LEFT:
990         state.lpos.pos += coord_def(-1, 1);
991         break;
992 
993     case CMD_MAP_MOVE_DOWN:
994         state.lpos.pos += coord_def(0, 1);
995         break;
996 
997     case CMD_MAP_MOVE_UP_RIGHT:
998         state.lpos.pos += coord_def(1, -1);
999         break;
1000 
1001     case CMD_MAP_MOVE_UP:
1002         state.lpos.pos += coord_def(0, -1);
1003         break;
1004 
1005     case CMD_MAP_MOVE_UP_LEFT:
1006         state.lpos.pos += coord_def(-1, -1);
1007         break;
1008 
1009     case CMD_MAP_MOVE_LEFT:
1010         state.lpos.pos += coord_def(-1, 0);
1011         break;
1012 
1013     case CMD_MAP_MOVE_DOWN_RIGHT:
1014         state.lpos.pos += coord_def(1, 1);
1015         break;
1016 
1017     case CMD_MAP_MOVE_RIGHT:
1018         state.lpos.pos += coord_def(1, 0);
1019         break;
1020 
1021     case CMD_MAP_PREV_LEVEL:
1022     case CMD_MAP_NEXT_LEVEL:
1023     {
1024         if (!state.allow_offlevel)
1025             break;
1026 
1027         const bool up = (cmd == CMD_MAP_PREV_LEVEL);
1028         level_pos dest =
1029             _stair_dest(state.lpos.pos,
1030                         up ? CMD_GO_UPSTAIRS : CMD_GO_DOWNSTAIRS);
1031 
1032         if (!dest.id.is_valid())
1033         {
1034             dest.id = up ? find_up_level(level_id::current())
1035                 : find_down_level(level_id::current());
1036             dest.pos = coord_def(-1, -1);
1037         }
1038 
1039         if (dest.id.is_valid() && dest.id != level_id::current()
1040             && you.level_visited(dest.id))
1041         {
1042             state.lpos = dest;
1043         }
1044         los_changed();
1045         break;
1046     }
1047 
1048     case CMD_MAP_GOTO_LEVEL:
1049         if (!state.allow_offlevel)
1050             break;
1051 
1052         {
1053             string name;
1054 #ifdef USE_TILE_WEB
1055             tiles_ui_control msgwin(UI_NORMAL);
1056 #endif
1057             const level_pos pos
1058                 = prompt_translevel_target(TPF_DEFAULT_OPTIONS, name);
1059 
1060             if (pos.id.depth < 1
1061                 || pos.id.depth > brdepth[pos.id.branch]
1062                 || !you.level_visited(pos.id))
1063             {
1064                 canned_msg(MSG_OK);
1065                 state.redraw_map = true;
1066                 break;
1067             }
1068 
1069             state.lpos = pos;
1070         }
1071         break;
1072 
1073     case CMD_MAP_JUMP_DOWN_LEFT:
1074         state.lpos.pos += coord_def(-block_step, block_step);
1075         break;
1076 
1077     case CMD_MAP_JUMP_DOWN:
1078         state.lpos.pos += coord_def(0, block_step);
1079         break;
1080 
1081     case CMD_MAP_JUMP_UP_RIGHT:
1082         state.lpos.pos += coord_def(block_step, -block_step);
1083         break;
1084 
1085     case CMD_MAP_JUMP_UP:
1086         state.lpos.pos += coord_def(0, -block_step);
1087         break;
1088 
1089     case CMD_MAP_JUMP_UP_LEFT:
1090         state.lpos.pos += coord_def(-block_step, -block_step);
1091         break;
1092 
1093     case CMD_MAP_JUMP_LEFT:
1094         state.lpos.pos += coord_def(-block_step, 0);
1095         break;
1096 
1097     case CMD_MAP_JUMP_DOWN_RIGHT:
1098         state.lpos.pos += coord_def(block_step, block_step);
1099         break;
1100 
1101     case CMD_MAP_JUMP_RIGHT:
1102         state.lpos.pos += coord_def(block_step, 0);
1103         break;
1104 
1105     case CMD_MAP_SCROLL_DOWN:
1106         state.lpos.pos += coord_def(0, 20);
1107         break;
1108 
1109     case CMD_MAP_SCROLL_UP:
1110         state.lpos.pos += coord_def(0, -20);
1111         break;
1112 
1113     case CMD_MAP_FIND_YOU:
1114         if (state.on_level)
1115             state.lpos.pos += you.pos() - state.lpos.pos;
1116         break;
1117 
1118 #ifdef USE_TILE
1119     case CMD_MAP_ZOOM_IN:
1120     case CMD_MAP_ZOOM_OUT:
1121         tiles.zoom_dungeon(cmd == CMD_MAP_ZOOM_IN);
1122         break;
1123 #endif
1124 
1125     case CMD_MAP_FIND_UPSTAIR:
1126     case CMD_MAP_FIND_DOWNSTAIR:
1127     case CMD_MAP_FIND_PORTAL:
1128     case CMD_MAP_FIND_TRAP:
1129     case CMD_MAP_FIND_ALTAR:
1130     case CMD_MAP_FIND_EXCLUDED:
1131     case CMD_MAP_FIND_WAYPOINT:
1132     case CMD_MAP_FIND_STASH:
1133     case CMD_MAP_FIND_STASH_REVERSE:
1134     {
1135         bool forward = (cmd != CMD_MAP_FIND_STASH_REVERSE);
1136 
1137         char32_t getty;
1138         switch (cmd)
1139         {
1140         case CMD_MAP_FIND_UPSTAIR:
1141             getty = '<';
1142             break;
1143         case CMD_MAP_FIND_DOWNSTAIR:
1144             getty = '>';
1145             break;
1146         case CMD_MAP_FIND_PORTAL:
1147             getty = '\t';
1148             break;
1149         case CMD_MAP_FIND_TRAP:
1150             getty = '^';
1151             break;
1152         case CMD_MAP_FIND_ALTAR:
1153             getty = '_';
1154             break;
1155         case CMD_MAP_FIND_EXCLUDED:
1156             getty = 'E';
1157             break;
1158         case CMD_MAP_FIND_WAYPOINT:
1159             getty = 'W';
1160             break;
1161         default:
1162         case CMD_MAP_FIND_STASH:
1163         case CMD_MAP_FIND_STASH_REVERSE:
1164             getty = 'I';
1165             break;
1166         }
1167 
1168         if (state.search_anchor.zero())
1169             state.search_anchor = state.lpos.pos;
1170 
1171         if (state.travel_mode && !_is_player_defined_feature(getty))
1172         {
1173             state.search_index = _find_feature(*state.features, getty,
1174                                             state.search_index,
1175                                             state.lpos.pos,
1176                                             forward);
1177         }
1178         else
1179         {
1180             const auto search_path =
1181                 search_path_around_point(state.search_anchor);
1182             state.search_index = _find_feature(*state.features, getty,
1183                                             state.search_index,
1184                                             state.lpos.pos,
1185                                             true);
1186         }
1187         break;
1188     }
1189 
1190     case CMD_MAP_GOTO_TARGET:
1191         if (state.travel_mode && state.on_level && state.lpos.pos == you.pos())
1192         {
1193             if (you.travel_x > 0 && you.travel_y > 0)
1194             {
1195                 if (you.travel_z == level_id::current())
1196                     state.lpos.pos = coord_def(you.travel_x, you.travel_y);
1197                 else if (state.allow_offlevel && you.travel_z.is_valid()
1198                                 && can_travel_to(you.travel_z)
1199                                 && you.level_visited(you.travel_z))
1200                 {
1201                     // previous travel target is offlevel
1202                     state.lpos = level_pos(you.travel_z,
1203                                 coord_def(you.travel_x, you.travel_y));
1204                     los_changed();
1205                 }
1206             }
1207         }
1208         else
1209         {
1210             state.chose = true;
1211             state.map_alive = false;
1212         }
1213 
1214         break;
1215 
1216     case CMD_MAP_ANNOTATE_LEVEL:
1217         state.excursion->go_to(state.original);
1218         redraw_screen();
1219         update_screen();
1220         state.excursion->go_to(state.lpos.id);
1221 
1222         if (!is_map_persistent())
1223             mpr("You can't annotate this level.");
1224         else
1225         {
1226 #ifdef USE_TILE_WEB
1227             tiles_ui_control msgwin(UI_NORMAL);
1228 #endif
1229             annotate_level(state.lpos.id);
1230         }
1231 
1232         state.redraw_map = true;
1233         break;
1234 
1235     case CMD_MAP_EXPLORE:
1236         if (state.on_level)
1237         {
1238             travel_pathfind tp;
1239             tp.set_floodseed(you.pos(), true);
1240 
1241             coord_def whereto = tp.pathfind(Options.explore_greedy
1242                                             ? RMODE_EXPLORE_GREEDY
1243                                             : RMODE_EXPLORE);
1244             _reset_travel_colours(*state.features, state.on_level);
1245 
1246             if (!whereto.zero())
1247                 state.lpos.pos = whereto;
1248         }
1249         break;
1250 
1251 #ifdef WIZARD
1252     case CMD_MAP_WIZARD_TELEPORT:
1253         if (!you.wizard || !state.on_level || !in_bounds(state.lpos.pos))
1254             break;
1255         if (cell_is_solid(state.lpos.pos))
1256             you.wizmode_teleported_into_rock = true;
1257         you.moveto(state.lpos.pos);
1258         state.map_alive = false;
1259         break;
1260 #endif
1261 
1262     case CMD_MAP_EXIT_MAP:
1263         state.lpos = level_pos();
1264         state.map_alive = false;
1265         break;
1266 
1267 #ifdef USE_TILE_LOCAL
1268     case CMD_NEXT_CMD:
1269         break; // allow mouse clicks to move cursor without leaving map mode
1270 #endif
1271     case CMD_MAP_DESCRIBE:
1272         if (map_bounds(state.lpos.pos) && env.map_knowledge(state.lpos.pos).known())
1273         {
1274             full_describe_square(state.lpos.pos, false);
1275             state.redraw_map = true;
1276         }
1277         break;
1278 
1279     default:
1280         if (!state.travel_mode)
1281             state.redraw_map = false;
1282         break;
1283     }
1284 
1285     return state;
1286 }
1287 
emphasise(const coord_def & where)1288 bool emphasise(const coord_def& where)
1289 {
1290     return is_unknown_stair(where) && !player_in_branch(BRANCH_VESTIBULE)
1291            || is_unknown_transporter(where);
1292 }
1293 
1294 #ifndef USE_TILE_LOCAL
1295 // Get glyph for feature list; here because it's so similar
1296 // to get_map_col.
1297 // Except that that function doesn't exist...
_get_feat_glyph(const coord_def & gc)1298 static cglyph_t _get_feat_glyph(const coord_def& gc)
1299 {
1300     // XXX: it's unclear whether we want to display all features
1301     // or just those not obscured by remembered/detected stuff.
1302     dungeon_feature_type feat = env.map_knowledge(gc).feat();
1303     const bool terrain_seen = env.map_knowledge(gc).seen();
1304     const feature_def &fdef = get_feature_def(feat);
1305     cglyph_t g;
1306     g.ch  = terrain_seen ? fdef.symbol() : fdef.magic_symbol();
1307     unsigned col;
1308     if (travel_colour_override(gc))
1309         col = _get_travel_colour(gc);
1310     else if (emphasise(gc))
1311     {
1312         g.ch = fdef.magic_symbol();
1313         col = fdef.seen_em_colour();
1314     }
1315     else
1316         col = fdef.seen_colour();
1317     g.col = real_colour(col);
1318     return g;
1319 }
1320 #endif
1321