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 = ≤
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