1 /**
2 * @file
3 * @brief Functions used when picking squares.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "directn.h"
9
10 #include <algorithm>
11 #include <cstdarg>
12 #include <cstdio>
13 #include <cstdlib>
14 #include <cstring>
15 #include <functional>
16 #include <sstream>
17
18 #include "act-iter.h"
19 #include "areas.h"
20 #include "artefact.h"
21 #include "attitude-change.h"
22 #include "cloud.h"
23 #include "colour.h"
24 #include "command.h"
25 #include "coordit.h"
26 #include "describe.h"
27 #include "dungeon.h"
28 #include "english.h"
29 #include "externs.h" // INVALID_COORD
30 #include "fight.h" // melee_confuse_chance
31 #include "god-abil.h"
32 #include "hints.h"
33 #include "invent.h"
34 #include "item-prop.h"
35 #include "item-status-flag-type.h"
36 #include "items.h"
37 #include "libutil.h"
38 #include "losglobal.h"
39 #include "macro.h"
40 #include "mapmark.h"
41 #include "message.h"
42 #include "misc.h"
43 #include "mon-death.h"
44 #include "mon-tentacle.h"
45 #include "nearby-danger.h"
46 #include "output.h"
47 #include "prompt.h"
48 #include "showsymb.h"
49 #include "spl-goditem.h"
50 #include "stash.h"
51 #include "state.h"
52 #include "stringutil.h"
53 #include "tag-version.h"
54 #include "target.h"
55 #include "terrain.h"
56 #ifdef USE_TILE
57 #include "tileview.h"
58 #include "tile-flags.h"
59 #endif
60 #include "traps.h"
61 #include "travel.h"
62 #include "viewchar.h"
63 #include "view.h"
64 #include "viewmap.h"
65 #include "wiz-dgn.h"
66 #include "wiz-mon.h"
67
68 enum LOSSelect
69 {
70 LS_ANY = 0x00,
71
72 // Check only visible squares
73 LS_VISIBLE = 0x01,
74
75 // Check only hidden squares
76 LS_HIDDEN = 0x02,
77
78 LS_VISMASK = 0x03,
79
80 // Flip from visible to hidden when going forward,
81 // or hidden to visible when going backwards.
82 LS_FLIPVH = 0x20,
83
84 // Flip from hidden to visible when going forward,
85 // or visible to hidden when going backwards.
86 LS_FLIPHV = 0x40,
87
88 LS_NONE = 0xFFFF,
89 };
90
91 #ifdef WIZARD
92 static void _wizard_make_friendly(monster* m);
93 #endif
94 static dist _look_around_target(const coord_def &whence);
95 static void _describe_oos_feature(const coord_def& where);
96 static void _describe_cell(const coord_def& where, bool in_range = true);
97 static bool _print_cloud_desc(const coord_def where);
98 static bool _print_item_desc(const coord_def where);
99
100 static bool _blocked_ray(const coord_def &where);
101 static bool _want_target_monster(const monster *mon, targ_mode_type mode,
102 targeter* hitfunc);
103 static bool _find_monster(const coord_def& where, targ_mode_type mode,
104 bool need_path, int range, targeter *hitfunc);
105 static bool _find_monster_expl(const coord_def& where, targ_mode_type mode,
106 bool need_path, int range, targeter *hitfunc,
107 aff_type mon_aff, aff_type allowed_self_aff);
108 static bool _find_shadow_step_mons(const coord_def& where, targ_mode_type mode,
109 bool need_path, int range,
110 targeter *hitfunc);
111 static bool _find_object(const coord_def& where, bool need_path, int range,
112 targeter *hitfunc);
113 static bool _find_autopickup_object(const coord_def& where, bool need_path,
114 int range, targeter *hitfunc);
115
116 typedef function<bool (const coord_def& where)> target_checker;
117 static bool _find_square_wrapper(coord_def &mfp, int direction,
118 target_checker find_targ, targeter *hitfunc,
119 LOSSelect los = LS_ANY);
120
121 static int _targeting_cmd_to_compass(command_type command);
122 static void _describe_oos_square(const coord_def& where);
123 static void _extend_move_to_edge(dist &moves);
124 static vector<string> _get_monster_desc_vector(const monster_info& mi);
125 static string _get_monster_desc(const monster_info& mi);
126
127 #ifdef DEBUG_DIAGNOSTICS
128 static void _debug_describe_feature_at(const coord_def &where);
129 #endif
130
131 #ifdef WIZARD
_wizard_make_friendly(monster * m)132 static void _wizard_make_friendly(monster* m)
133 {
134 if (m == nullptr)
135 return;
136
137 mon_attitude_type att = m->attitude;
138
139 // During arena mode, skip directly from friendly to hostile.
140 if (crawl_state.arena_suspended && att == ATT_FRIENDLY)
141 att = ATT_NEUTRAL;
142
143 switch (att)
144 {
145 case ATT_FRIENDLY:
146 m->attitude = ATT_GOOD_NEUTRAL;
147 m->flags &= ~MF_NO_REWARD;
148 m->flags |= MF_WAS_NEUTRAL;
149 break;
150 case ATT_GOOD_NEUTRAL:
151 m->attitude = ATT_STRICT_NEUTRAL;
152 break;
153 case ATT_STRICT_NEUTRAL:
154 m->attitude = ATT_NEUTRAL;
155 break;
156 case ATT_NEUTRAL:
157 m->attitude = ATT_HOSTILE;
158 m->flags &= ~MF_WAS_NEUTRAL;
159 break;
160 case ATT_HOSTILE:
161 m->attitude = ATT_FRIENDLY;
162 m->flags |= MF_NO_REWARD;
163 break;
164 }
165 mons_att_changed(m);
166 }
167 #endif
168
dist()169 dist::dist()
170 : isValid(false), isTarget(false), isEndpoint(false), isCancel(false),
171 choseRay(false), interactive(false), target(), delta(), ray(),
172 find_target(false), fire_context(nullptr), cmd_result(CMD_NO_CMD)
173 {
174 }
175
isMe() const176 bool dist::isMe() const
177 {
178 // We hack the decision as to whether to use delta or target by
179 // assuming that we use delta only if target hasn't been touched.
180 return isValid && !isCancel
181 && (target == you.pos()
182 || (target.origin() && delta.origin()));
183 }
184
185 /**
186 * Does this dist object need `target` to be filled in somehow, e.g. with
187 * manual/interactive targeting? Affected by the following three dist fields:
188 *
189 * `interactive`: force interactive targeting, overrides the other two.
190 * `find_target`: requests to use the direction chooser default target, allows
191 * non-interactive mode. Overrides a value supplied by `target`.
192 * `target`: supplying a target coord directly allows non-interactive mode.
193 */
needs_targeting() const194 bool dist::needs_targeting() const
195 {
196 return interactive || !in_bounds(target) && !find_target;
197 }
198
_targeting_cmd_to_compass(command_type command)199 static int _targeting_cmd_to_compass(command_type command)
200 {
201 switch (command)
202 {
203 case CMD_TARGET_UP: case CMD_TARGET_DIR_UP:
204 return 0;
205 case CMD_TARGET_UP_RIGHT: case CMD_TARGET_DIR_UP_RIGHT:
206 return 1;
207 case CMD_TARGET_RIGHT: case CMD_TARGET_DIR_RIGHT:
208 return 2;
209 case CMD_TARGET_DOWN_RIGHT: case CMD_TARGET_DIR_DOWN_RIGHT:
210 return 3;
211 case CMD_TARGET_DOWN: case CMD_TARGET_DIR_DOWN:
212 return 4;
213 case CMD_TARGET_DOWN_LEFT: case CMD_TARGET_DIR_DOWN_LEFT:
214 return 5;
215 case CMD_TARGET_LEFT: case CMD_TARGET_DIR_LEFT:
216 return 6;
217 case CMD_TARGET_UP_LEFT: case CMD_TARGET_DIR_UP_LEFT:
218 return 7;
219 default:
220 return -1;
221 }
222 }
223
shift_direction(command_type cmd)224 static command_type shift_direction(command_type cmd)
225 {
226 switch (cmd)
227 {
228 case CMD_TARGET_DOWN_LEFT: return CMD_TARGET_DIR_DOWN_LEFT;
229 case CMD_TARGET_LEFT: return CMD_TARGET_DIR_LEFT;
230 case CMD_TARGET_DOWN: return CMD_TARGET_DIR_DOWN;
231 case CMD_TARGET_UP: return CMD_TARGET_DIR_UP;
232 case CMD_TARGET_RIGHT: return CMD_TARGET_DIR_RIGHT;
233 case CMD_TARGET_DOWN_RIGHT: return CMD_TARGET_DIR_DOWN_RIGHT;
234 case CMD_TARGET_UP_RIGHT: return CMD_TARGET_DIR_UP_RIGHT;
235 case CMD_TARGET_UP_LEFT: return CMD_TARGET_DIR_UP_LEFT;
236 default: return cmd;
237 }
238 }
239
targeted_actor() const240 actor* direction_chooser::targeted_actor() const
241 {
242 if (target() == you.pos())
243 return &you;
244 else
245 return targeted_monster();
246 }
247
targeted_monster() const248 monster* direction_chooser::targeted_monster() const
249 {
250 monster* m = monster_at(target());
251 if (m && you.can_see(*m))
252 return m;
253 else
254 return nullptr;
255 }
256
257 // Return your target, if it still exists and is visible to you.
_get_current_target()258 static monster* _get_current_target()
259 {
260 if (invalid_monster_index(you.prev_targ))
261 return nullptr;
262
263 monster* mon = &env.mons[you.prev_targ];
264 ASSERT(mon);
265 if (mon->alive() && you.can_see(*mon))
266 return mon;
267 else
268 return nullptr;
269 }
270
build_targeting_hint_string() const271 string direction_chooser::build_targeting_hint_string() const
272 {
273 string hint_string;
274
275 // Hint for 'p' - previous target, and for 'f' - current cell, if
276 // applicable.
277 // TODO: currently 'f' works for non-actor targets (features, apport) but
278 // shows nothing here
279 const actor* f_target = targeted_actor();
280 const monster* p_target = _get_current_target();
281
282 if (f_target && f_target == p_target)
283 hint_string = ", f/p - " + f_target->name(DESC_PLAIN);
284 else
285 {
286 if (f_target)
287 hint_string += ", f - " + f_target->name(DESC_PLAIN);
288 if (p_target)
289 hint_string += ", p - " + p_target->name(DESC_PLAIN);
290 }
291
292 return hint_string;
293 }
294
print_top_prompt() const295 void direction_chooser::print_top_prompt() const
296 {
297 if (!top_prompt.empty())
298 mprf(MSGCH_PROMPT, "%s", top_prompt.c_str());
299 else if (moves.fire_context)
300 {
301 // TODO: consolidate top prompt construction more
302 mprf(MSGCH_PROMPT, "%s",
303 moves.fire_context->get()->quiver_description().tostring().c_str());
304 }
305 }
306
print_key_hints() const307 void direction_chooser::print_key_hints() const
308 {
309 // TODO: build this as a vector and insert ,s and \ns in a smarter way
310 string prompt = "Press: ? - help";
311
312 if (just_looking)
313 {
314 if (you.see_cell(target()))
315 prompt += ", v - describe";
316 prompt += ", . - travel";
317 if (in_bounds(target()) && env.map_knowledge(target()).item())
318 prompt += ", g - get item";
319 }
320 else
321 {
322 if (moves.fire_context)
323 {
324 const string hint = moves.fire_context->fire_key_hints();
325 if (!hint.empty())
326 prompt += hint + "\n";
327 }
328 string direction_hint = "";
329 if (!behaviour->targeted())
330 direction_hint = "Dir - look around, f - activate";
331 else
332 {
333 switch (restricts)
334 {
335 case DIR_NONE:
336 direction_hint = "Shift-Dir - straight line";
337 break;
338 case DIR_TARGET:
339 case DIR_SHADOW_STEP:
340 case DIR_LEAP:
341 direction_hint = "Dir - move target";
342 break;
343 }
344 }
345
346 if (direction_hint.size())
347 {
348 if (prompt[prompt.size() - 1] != '\n')
349 prompt += ", ";
350 prompt += direction_hint;
351 }
352 if (behaviour->targeted())
353 prompt += build_targeting_hint_string();
354 }
355
356 // Display the prompt.
357 mprf(MSGCH_PROMPT, "%s", prompt.c_str());
358 }
359
targets_objects() const360 bool direction_chooser::targets_objects() const
361 {
362 return mode == TARG_MOVABLE_OBJECT;
363 }
364
365 /// Are we looking for enemies?
targets_enemies() const366 bool direction_chooser::targets_enemies() const
367 {
368 switch (mode)
369 {
370 case TARG_HOSTILE:
371 case TARG_HOSTILE_SUBMERGED:
372 return true;
373 default:
374 return false;
375 }
376 }
377
describe_cell() const378 void direction_chooser::describe_cell() const
379 {
380 print_top_prompt();
381 print_key_hints();
382
383 if (!you.see_cell(target()))
384 {
385 // FIXME: make this better integrated.
386 _describe_oos_square(target());
387 }
388 else
389 {
390 bool did_cloud = false;
391 print_target_description(did_cloud);
392 if (just_looking)
393 print_items_description();
394 if (just_looking || show_floor_desc)
395 {
396 print_floor_description(show_boring_feats);
397 if (!did_cloud)
398 _print_cloud_desc(target());
399 }
400 }
401
402 flush_prev_message();
403 }
404
405 #ifndef USE_TILE_LOCAL
_get_ray_glyph(const coord_def & pos,int colour,int glych,int mcol)406 static cglyph_t _get_ray_glyph(const coord_def& pos, int colour, int glych,
407 int mcol)
408 {
409 if (const monster* mons = monster_at(pos))
410 {
411 if (mons->alive() && mons->visible_to(&you))
412 {
413 glych = get_cell_glyph(pos).ch;
414 colour = mcol;
415 }
416 }
417 if (pos == you.pos())
418 {
419 glych = mons_char(you.symbol);
420 colour = mcol;
421 }
422 return {static_cast<char32_t>(glych),
423 static_cast<unsigned short>(real_colour(colour))};
424 }
425 #endif
426
427 // Unseen monsters in shallow water show a "strange disturbance".
428 // (Unless flying!)
429 // These should match tests in show.cc's _update_monster
_mon_exposed_in_water(const monster * mon)430 static bool _mon_exposed_in_water(const monster* mon)
431 {
432 return env.grid(mon->pos()) == DNGN_SHALLOW_WATER && !mon->airborne()
433 && !mon->submerged() && !cloud_at(mon->pos());
434 }
435
_mon_exposed_in_cloud(const monster * mon)436 static bool _mon_exposed_in_cloud(const monster* mon)
437 {
438 return cloud_at(mon->pos())
439 && is_opaque_cloud(cloud_at(mon->pos())->type)
440 && !mon->submerged() && !mon->is_insubstantial();
441 }
442
_mon_exposed(const monster * mon)443 static bool _mon_exposed(const monster* mon)
444 {
445 if (!mon || !you.see_cell(mon->pos()) || mon->visible_to(&you))
446 return false;
447
448 return _mon_exposed_in_water(mon) || _mon_exposed_in_cloud(mon);
449 }
450
_is_target_in_range(const coord_def & where,int range,targeter * hitfunc)451 static bool _is_target_in_range(const coord_def& where, int range,
452 targeter *hitfunc)
453 {
454 if (hitfunc)
455 return hitfunc->valid_aim(where);
456 // range == -1 means that range doesn't matter.
457 return range == -1 || grid_distance(you.pos(), where) <= range;
458 }
459
460 targeting_behaviour direction_chooser::stock_behaviour;
461
direction(dist & moves,const direction_chooser_args & args)462 void direction(dist &moves, const direction_chooser_args& args)
463 {
464 // TODO this might break pre-chosen delta targeting, if that ever happens
465 // (currently it looks like isTarget is always set to true)
466 moves.interactive = moves.needs_targeting();
467
468 if (moves.interactive)
469 direction_chooser(moves, args).choose_direction();
470 else
471 direction_chooser(moves, args).noninteractive();
472 }
473
direction_chooser(dist & moves_,const direction_chooser_args & args)474 direction_chooser::direction_chooser(dist& moves_,
475 const direction_chooser_args& args) :
476 moves(moves_),
477 restricts(args.restricts),
478 mode(args.mode),
479 range(args.range),
480 just_looking(args.just_looking),
481 prefer_farthest(args.prefer_farthest),
482 allow_shift_dir(args.allow_shift_dir),
483 self(args.self),
484 target_prefix(args.target_prefix),
485 top_prompt(args.top_prompt),
486 behaviour(args.behaviour),
487 show_floor_desc(args.show_floor_desc),
488 show_boring_feats(args.show_boring_feats),
489 hitfunc(args.hitfunc),
490 default_place(args.default_place),
491 renderer(*this),
492 unrestricted(args.unrestricted),
493 needs_path(args.needs_path)
494 {
495 if (!behaviour)
496 behaviour = &stock_behaviour;
497
498 behaviour->just_looking = just_looking;
499 behaviour->get_desc_func = args.get_desc_func;
500 if (unrestricted)
501 {
502 needs_path = false;
503 behaviour->needs_path = MB_MAYBE;
504 }
505 else if (hitfunc)
506 {
507 needs_path = true;
508 behaviour->needs_path = MB_MAYBE; // TODO: can this be relaxed?
509 }
510 if (behaviour->needs_path != MB_MAYBE)
511 needs_path = tobool(behaviour->needs_path, true);
512
513 show_beam = !just_looking && needs_path;
514 need_viewport_redraw = show_beam;
515 have_beam = false;
516
517 need_text_redraw = true;
518 need_cursor_redraw = true;
519 need_all_redraw = false;
520 }
521
522 class view_desc_proc
523 {
524 public:
view_desc_proc()525 view_desc_proc()
526 {
527 // This thing seems to be starting off 1 line above where it
528 // should be. -cao
529 nextline();
530 }
width()531 int width() { return crawl_view.msgsz.x; }
height()532 int height() { return crawl_view.msgsz.y; }
print(const string & str)533 void print(const string &str) { cprintf("%s", str.c_str()); }
nextline()534 void nextline() { cgotoxy(1, wherey() + 1); }
535 };
536
_full_describe_menu(vector<monster_info> const & list_mons,vector<item_def> const & list_items,vector<coord_def> const & list_features,string selectverb,bool examine_only=false,string title="")537 static coord_def _full_describe_menu(vector<monster_info> const &list_mons,
538 vector<item_def> const &list_items,
539 vector<coord_def> const &list_features,
540 string selectverb,
541 bool examine_only = false,
542 string title = "")
543 {
544 InvMenu desc_menu(MF_SINGLESELECT | MF_ANYPRINTABLE
545 | MF_ALLOW_FORMATTING | MF_SELECT_BY_PAGE);
546
547 string title_secondary;
548
549 if (title.empty())
550 {
551 if (!list_mons.empty())
552 title = "Monsters";
553 if (!list_items.empty())
554 {
555 if (!title.empty())
556 title += "/";
557 title += "Items";
558 }
559 if (!list_features.empty())
560 {
561 if (!title.empty())
562 title += "/";
563 title += "Features";
564 }
565 title = "Visible " + title;
566 if (examine_only)
567 title += " (select to examine)";
568 else
569 {
570 title_secondary = title + " (select to examine, '!' to "
571 + selectverb + "):";
572 title += " (select to " + selectverb + ", '!' to examine):";
573 }
574 }
575
576 desc_menu.set_tag("pickup");
577 // necessary for sorting of the item submenu
578 desc_menu.set_type(menu_type::pickup);
579 desc_menu.set_title(new MenuEntry(title, MEL_TITLE));
580 if (examine_only)
581 {
582 desc_menu.action_cycle = Menu::CYCLE_NONE;
583 desc_menu.menu_action = InvMenu::ACT_EXAMINE;
584 }
585 else
586 {
587 desc_menu.action_cycle = Menu::CYCLE_TOGGLE;
588 desc_menu.menu_action = InvMenu::ACT_EXECUTE;
589 desc_menu.set_title(new MenuEntry(title_secondary, MEL_TITLE), false);
590 }
591
592 // Start with hotkey 'a' and count from there.
593 menu_letter hotkey;
594 // Build menu entries for monsters.
595 if (!list_mons.empty())
596 {
597 desc_menu.add_entry(new MenuEntry("Monsters", MEL_SUBTITLE));
598 for (const monster_info &mi : list_mons)
599 {
600 // List monsters in the form
601 // (A) An angel (neutral), wielding a glowing long sword
602
603 ostringstream prefix;
604 #ifndef USE_TILE_LOCAL
605 cglyph_t g = get_mons_glyph(mi);
606 const string col_string = colour_to_str(g.col);
607 prefix << "(<" << col_string << ">"
608 << (g.ch == '<' ? "<<" : stringize_glyph(g.ch))
609 << "</" << col_string << ">) ";
610 #endif
611 if (Options.monster_item_view_coordinates)
612 {
613 const coord_def relpos = mi.pos - you.pos();
614 prefix << "(" << relpos.x << ", " << -relpos.y << ") ";
615 }
616
617 string str = get_monster_equipment_desc(mi, DESC_FULL, DESC_A, true);
618 if (mi.dam != MDAM_OKAY)
619 str += ", " + mi.damage_desc();
620
621 string consinfo = mi.constriction_description();
622 if (!consinfo.empty())
623 str += ", " + consinfo;
624
625 #ifndef USE_TILE_LOCAL
626 // Wraparound if the description is longer than allowed.
627 linebreak_string(str, get_number_of_cols() - 9);
628 #endif
629 vector<formatted_string> fss;
630 formatted_string::parse_string_to_multiple(str, fss);
631 MenuEntry *me = nullptr;
632 for (unsigned int j = 0; j < fss.size(); ++j)
633 {
634 if (j == 0)
635 me = new MonsterMenuEntry(prefix.str() + fss[j].tostring(), &mi, hotkey++);
636 #ifndef USE_TILE_LOCAL
637 else
638 {
639 str = " " + fss[j].tostring();
640 me = new MenuEntry(str, MEL_ITEM, 1);
641 }
642 #endif
643 desc_menu.add_entry(me);
644 }
645 }
646 }
647
648 // Build menu entries for items.
649 if (!list_items.empty())
650 {
651 vector<InvEntry*> all_items;
652 for (const item_def &item : list_items)
653 all_items.push_back(new InvEntry(item));
654
655 const menu_sort_condition *cond = desc_menu.find_menu_sort_condition();
656 desc_menu.sort_menu(all_items, cond);
657
658 desc_menu.add_entry(new MenuEntry("Items", MEL_SUBTITLE));
659 for (InvEntry *me : all_items)
660 {
661 #ifndef USE_TILE_LOCAL
662 // Show glyphs only for ASCII.
663 me->set_show_glyph(true);
664 #endif
665 me->set_show_coordinates(Options.monster_item_view_coordinates);
666 me->tag = "pickup";
667 me->hotkeys[0] = hotkey;
668 me->quantity = 2; // Hack to make items selectable.
669
670 desc_menu.add_entry(me);
671 ++hotkey;
672 }
673 }
674
675 if (!list_features.empty())
676 {
677 desc_menu.add_entry(new MenuEntry("Features", MEL_SUBTITLE));
678 for (const coord_def &c : list_features)
679 {
680 ostringstream desc;
681 #ifndef USE_TILE_LOCAL
682 cglyph_t g = get_cell_glyph(c, true);
683 const string colour_str = colour_to_str(g.col);
684 desc << "(<" << colour_str << ">";
685 desc << (g.ch == '<' ? "<<" : stringize_glyph(g.ch));
686
687 desc << "</" << colour_str << ">) ";
688 #endif
689 if (Options.monster_item_view_coordinates)
690 {
691 const coord_def relpos = c - you.pos();
692 desc << "(" << relpos.x << ", " << -relpos.y << ") ";
693 }
694
695 desc << feature_description_at(c, false, DESC_A);
696 if (is_unknown_stair(c) || is_unknown_transporter(c))
697 desc << " (not visited)";
698 FeatureMenuEntry *me = new FeatureMenuEntry(desc.str(), c, hotkey);
699 me->tag = "description";
700 // Hack to make features selectable.
701 me->quantity = c.x*100 + c.y + 3;
702 desc_menu.add_entry(me);
703 ++hotkey;
704 }
705 }
706
707 // Select an item to read its full description, or a monster to read its
708 // e'x'amine description. Toggle with '!' to return the coordinates to
709 // the caller so that it may perform the promised selectverb
710
711 coord_def target(-1, -1);
712
713 desc_menu.on_single_selection = [&desc_menu, &target](const MenuEntry& sel)
714 {
715 target = coord_def(-1, -1);
716 // HACK: quantity == 1: monsters, quantity == 2: items
717 const int quant = sel.quantity;
718 if (quant == 1)
719 {
720 // Get selected monster.
721 monster_info* m = static_cast<monster_info* >(sel.data);
722
723 #ifdef USE_TILE
724 // Highlight selected monster on the screen.
725 const coord_def gc(m->pos);
726 tiles.place_cursor(CURSOR_TUTORIAL, gc);
727 const string &desc = get_terse_square_desc(gc);
728 tiles.clear_text_tags(TAG_TUTORIAL);
729 tiles.add_text_tag(TAG_TUTORIAL, desc, gc);
730 #endif
731
732 if (desc_menu.menu_action == InvMenu::ACT_EXAMINE)
733 {
734 // View database entry.
735 describe_monsters(*m);
736 redraw_screen();
737 update_screen();
738 clear_messages();
739 }
740 else // ACT_EXECUTE -> view/travel
741 target = m->pos;
742 }
743 else if (quant == 2)
744 {
745 // Get selected item.
746 const item_def* i = static_cast<item_def*>(sel.data);
747 if (desc_menu.menu_action == InvMenu::ACT_EXAMINE)
748 describe_item_popup(*i);
749 else // ACT_EXECUTE -> view/travel
750 target = i->pos;
751 }
752 else
753 {
754 const int num = quant - 3;
755 const int y = num % 100;
756 const int x = (num - y)/100;
757 coord_def c(x,y);
758
759 if (desc_menu.menu_action == InvMenu::ACT_EXAMINE)
760 describe_feature_wide(c);
761 else // ACT_EXECUTE -> view/travel
762 target = c;
763 }
764 return desc_menu.menu_action == InvMenu::ACT_EXAMINE;
765 };
766 desc_menu.show();
767 redraw_screen();
768 update_screen();
769
770
771 #ifndef USE_TILE_LOCAL
772 if (!list_items.empty())
773 {
774 // Unset show_glyph for other menus.
775 InvEntry me(list_items[0]);
776 me.set_show_glyph(false);
777 }
778 #endif
779 #ifdef USE_TILE
780 // Clear cursor placement.
781 tiles.place_cursor(CURSOR_TUTORIAL, NO_CURSOR);
782 tiles.clear_text_tags(TAG_TUTORIAL);
783 #endif
784 return target;
785 }
786
_get_nearby_items(vector<item_def> & list_items,bool need_path,int range,targeter * hitfunc)787 static void _get_nearby_items(vector<item_def> &list_items,
788 bool need_path, int range, targeter *hitfunc)
789 {
790 // Grab all items known (or thought) to be in the stashes in view.
791 for (vision_iterator ri(you); ri; ++ri)
792 {
793 if (!_is_target_in_range(*ri, range, hitfunc))
794 continue;
795
796 if (need_path && _blocked_ray(*ri))
797 continue;
798
799 const int oid = you.visible_igrd(*ri);
800 if (oid == NON_ITEM)
801 continue;
802
803 const vector<const item_def *> items = item_list_on_square(
804 you.visible_igrd(*ri));
805
806 for (const item_def * item : items)
807 list_items.push_back(*item);
808 }
809 }
810
_get_nearby_features(vector<coord_def> & list_features,bool need_path,int range,targeter * hitfunc)811 static void _get_nearby_features(vector<coord_def> &list_features,
812 bool need_path, int range, targeter *hitfunc)
813 {
814 vector <text_pattern> &filters = Options.monster_item_view_features;
815 for (vision_iterator ri(you); ri; ++ri)
816 {
817 if (!_is_target_in_range(*ri, range, hitfunc))
818 continue;
819
820 if (need_path && _blocked_ray(*ri))
821 continue;
822
823 // Do we want to aim at this because its the feature, not because
824 // of a monster. This is a bit of a hack since hitfunc->valid_aim
825 // is monster-aware and monster targets are listed in a different
826 // place.
827 if (hitfunc && !monster_at(*ri))
828 list_features.push_back(*ri);
829 // Not using a targeter, list features according to user preferences.
830 else if (!hitfunc)
831 {
832 if (!filters.empty())
833 {
834 for (const text_pattern &pattern : filters)
835 {
836 if (pattern.matches(feature_description(env.grid(*ri)))
837 || feat_stair_direction(env.grid(*ri)) != CMD_NO_CMD
838 && pattern.matches("stair")
839 || feat_is_trap(env.grid(*ri))
840 && pattern.matches("trap"))
841 {
842 list_features.push_back(*ri);
843 break;
844 }
845 }
846 }
847 }
848 }
849 }
850
851 // Lists monsters, items, and some interesting features in the player's view.
852 // TODO: Allow sorting of items lists.
full_describe_view()853 void full_describe_view()
854 {
855 vector<monster_info> list_mons;
856 vector<item_def> list_items;
857 vector<coord_def> list_features;
858 // Get monsters via the monster_info, sorted by difficulty.
859 get_monster_info(list_mons);
860 _get_nearby_items(list_items, false, get_los_radius(), nullptr);
861 _get_nearby_features(list_features, false, get_los_radius(), nullptr);
862
863 if (list_mons.empty() && list_items.empty() && list_features.empty())
864 {
865 mpr("No monsters, items or features are visible.");
866 return;
867 }
868
869 coord_def target = _full_describe_menu(list_mons, list_items,
870 list_features, "target/travel");
871
872 // need to do this after the menu has been closed on console,
873 // since do_look_around() runs its own loop
874 if (target != coord_def(-1, -1))
875 do_look_around(target);
876 }
877
do_look_around(const coord_def & whence)878 void do_look_around(const coord_def &whence)
879 {
880 dist lmove = _look_around_target(you.pos() + whence);
881 if (lmove.isValid && lmove.isTarget && !lmove.isCancel
882 && !crawl_state.arena_suspended)
883 {
884 start_travel(lmove.target);
885 }
886 }
887
get_look_position(coord_def * c)888 bool get_look_position(coord_def *c)
889 {
890 dist lmove = _look_around_target(you.pos());
891 if (lmove.isCancel)
892 return false;
893 *c = lmove.target;
894 return true;
895 }
896
_look_around_target(const coord_def & whence)897 static dist _look_around_target(const coord_def &whence)
898 {
899 dist lmove; // Will be initialised by direction().
900 direction_chooser_args args;
901 args.restricts = DIR_TARGET;
902 args.just_looking = true;
903 args.needs_path = false;
904 args.target_prefix = "Here";
905 args.default_place = whence - you.pos();
906 direction(lmove, args);
907 return lmove;
908 }
909
range_view_annotator(targeter * range)910 range_view_annotator::range_view_annotator(targeter *range)
911 {
912 if (range && Options.darken_beyond_range)
913 crawl_state.darken_range = range;
914 }
915
~range_view_annotator()916 range_view_annotator::~range_view_annotator()
917 {
918 crawl_state.darken_range = nullptr;
919 }
920
monster_view_annotator(vector<monster * > * monsters)921 monster_view_annotator::monster_view_annotator(vector<monster *> *monsters)
922 {
923 if ((Options.use_animations & UA_MONSTER_IN_SIGHT) && monsters->size())
924 {
925 crawl_state.flash_monsters = monsters;
926 viewwindow(false);
927 update_screen();
928 }
929 }
930
~monster_view_annotator()931 monster_view_annotator::~monster_view_annotator()
932 {
933 if ((Options.use_animations & UA_MONSTER_IN_SIGHT)
934 && crawl_state.flash_monsters)
935 {
936 crawl_state.flash_monsters = nullptr;
937 viewwindow(false);
938 update_screen();
939 }
940 }
941
move_is_ok() const942 bool direction_chooser::move_is_ok() const
943 {
944 if (unrestricted || !behaviour->targeted())
945 return true;
946 if (!moves.isCancel && moves.isTarget)
947 {
948 if (!cell_see_cell(you.pos(), target(), LOS_NO_TRANS))
949 {
950 if (hitfunc && hitfunc->can_affect_unseen())
951 return true; // is this too broad?
952 if (you.see_cell(target()))
953 mprf(MSGCH_EXAMINE_FILTER, "There's something in the way.");
954 else
955 mprf(MSGCH_EXAMINE_FILTER, "You can't see that place.");
956 return false;
957 }
958
959 if (looking_at_you())
960 {
961 if (!targets_objects() && targets_enemies())
962 {
963 if (self == confirm_prompt_type::cancel
964 || self == confirm_prompt_type::prompt
965 && Options.allow_self_target
966 == confirm_prompt_type::cancel)
967 {
968 if (moves.interactive)
969 mprf(MSGCH_EXAMINE_FILTER, "That would be overly suicidal.");
970 return false;
971 }
972 else if (self != confirm_prompt_type::none
973 && Options.allow_self_target
974 != confirm_prompt_type::none)
975 {
976 // if it needs to be asked, simply disallow it when
977 // calling in non-interactive mode
978 if (!moves.interactive)
979 return false;
980 return yesno("Really target yourself?", false, 'n',
981 true, true, false, nullptr, false);
982 }
983 }
984
985 if (self == confirm_prompt_type::cancel)
986 {
987 // avoid printing this message when autotargeting -- it doesn't
988 // make much sense
989 if (moves.interactive)
990 mprf(MSGCH_EXAMINE_FILTER, "Sorry, you can't target yourself.");
991 return false;
992 }
993 }
994 }
995
996 // Some odd cases
997 if (!moves.isValid && !moves.isCancel)
998 return yesno("Are you sure you want to fizzle?", false, 'n');
999
1000 return true;
1001 }
1002
1003 // Assuming the target is in view, is line-of-fire blocked?
_blocked_ray(const coord_def & where)1004 static bool _blocked_ray(const coord_def &where)
1005 {
1006 return !exists_ray(you.pos(), where, opc_solid_see);
1007 }
1008
1009 // Try to find an enemy monster to target
find_default_monster_target(coord_def & result) const1010 bool direction_chooser::find_default_monster_target(coord_def& result) const
1011 {
1012 // First try to pick our previous monster target.
1013 const monster* mons_target = _get_current_target();
1014 if (mons_target != nullptr
1015 && _want_target_monster(mons_target, mode, hitfunc)
1016 && in_range(mons_target->pos()))
1017 {
1018 result = mons_target->pos();
1019 return true;
1020 }
1021 // If the previous targetted position is at all useful, use it.
1022 if (!Options.simple_targeting && hitfunc && !prefer_farthest
1023 && _find_monster_expl(you.prev_grd_targ, mode, needs_path,
1024 range, hitfunc, AFF_YES, AFF_MULTIPLE))
1025 {
1026 result = you.prev_grd_targ;
1027 return true;
1028 }
1029 // The previous target is no good. Try to find one from scratch.
1030 bool success = false;
1031
1032 if (Options.simple_targeting)
1033 {
1034 success = _find_square_wrapper(result, 1,
1035 bind(_find_monster, placeholders::_1,
1036 mode, needs_path, range, hitfunc),
1037 hitfunc);
1038 }
1039 else
1040 {
1041 success = hitfunc && _find_square_wrapper(result, 1,
1042 bind(_find_monster_expl,
1043 placeholders::_1, mode,
1044 needs_path, range,
1045 hitfunc,
1046 // First try to bizap
1047 AFF_MULTIPLE, AFF_YES),
1048 hitfunc)
1049 || _find_square_wrapper(result, 1,
1050 bind(restricts == DIR_SHADOW_STEP ?
1051 _find_shadow_step_mons :
1052 _find_monster,
1053 placeholders::_1, mode,
1054 needs_path, range, hitfunc),
1055 hitfunc);
1056 }
1057
1058 // This is used for three things:
1059 // * For all LRD targetting
1060 // * To aim explosions so they try to miss you
1061 // * To hit monsters in LOS that are outside of normal range, but
1062 // inside explosion/cloud range
1063 if (!Options.simple_targeting && hitfunc
1064 && hitfunc->can_affect_outside_range()
1065 && (!hitfunc->set_aim(result)
1066 || hitfunc->is_affected(result) < AFF_YES
1067 || hitfunc->is_affected(you.pos()) > AFF_NO))
1068 {
1069 coord_def old_result;
1070 if (success)
1071 old_result = result;
1072 for (aff_type mon_aff : { AFF_YES, AFF_MAYBE })
1073 {
1074 for (aff_type allowed_self_aff : { AFF_NO, AFF_MAYBE, AFF_YES })
1075 {
1076 success = _find_square_wrapper(result, 1,
1077 bind(_find_monster_expl,
1078 placeholders::_1, mode,
1079 needs_path, range, hitfunc,
1080 mon_aff, allowed_self_aff),
1081 hitfunc);
1082 if (success)
1083 {
1084 // If we're hitting ourselves anyway, just target the
1085 // monster's position (this looks less strange).
1086 if (allowed_self_aff == AFF_YES && !old_result.origin())
1087 result = old_result;
1088 break;
1089 }
1090 }
1091 if (success)
1092 break;
1093 }
1094 }
1095 if (success)
1096 return true;
1097
1098 // If we couldn't, maybe it was because of line-of-fire issues.
1099 // Check if that's happening, and inform the user (because it's
1100 // pretty confusing.)
1101 if (needs_path
1102 && _find_square_wrapper(result, 1,
1103 bind(restricts == DIR_SHADOW_STEP ?
1104 _find_shadow_step_mons : _find_monster,
1105 placeholders::_1, mode, false,
1106 range, hitfunc),
1107 hitfunc))
1108 {
1109 // Special colouring in tutorial or hints mode.
1110 const bool need_hint = Hints.hints_events[HINT_TARGET_NO_FOE];
1111 // TODO: this seems to trigger when there are no monsters in range
1112 // of the hitfunc, regardless of what's in the way, and it shouldn't.
1113 mprf(need_hint ? MSGCH_TUTORIAL : MSGCH_PROMPT,
1114 "All monsters which could be auto-targeted are covered by "
1115 "a wall or statue which interrupts your line of fire, even "
1116 "though it doesn't interrupt your line of sight.");
1117
1118 if (need_hint)
1119 {
1120 mprf(MSGCH_TUTORIAL, "To return to the main mode, press <w>Escape</w>.");
1121 Hints.hints_events[HINT_TARGET_NO_FOE] = false;
1122 }
1123 }
1124 return false;
1125 }
1126
1127 // Find a good square to start targeting from.
find_default_target() const1128 coord_def direction_chooser::find_default_target() const
1129 {
1130 coord_def result = you.pos();
1131 bool success = false;
1132
1133 if (targets_objects())
1134 {
1135 // First, try to find a particularly relevant item (autopickup).
1136 // Barring that, just try anything.
1137 success = _find_square_wrapper(result, 1,
1138 bind(_find_autopickup_object,
1139 placeholders::_1,
1140 needs_path, range, hitfunc),
1141 hitfunc,
1142 LS_FLIPVH)
1143 || _find_square_wrapper(result, 1,
1144 bind(_find_object, placeholders::_1,
1145 needs_path, range, hitfunc),
1146 hitfunc,
1147 LS_FLIPVH);
1148 }
1149 else if ((mode != TARG_ANY && mode != TARG_FRIEND)
1150 || self == confirm_prompt_type::cancel)
1151 {
1152 success = find_default_monster_target(result);
1153 }
1154
1155 if (!success)
1156 result = you.pos();
1157
1158 return result;
1159 }
1160
target() const1161 const coord_def& direction_chooser::target() const
1162 {
1163 return moves.target;
1164 }
1165
set_target(const coord_def & new_target)1166 void direction_chooser::set_target(const coord_def& new_target)
1167 {
1168 moves.target = new_target;
1169 }
1170
1171 #ifdef USE_TILE
_tileidx_aff_type(aff_type aff)1172 static tileidx_t _tileidx_aff_type(aff_type aff)
1173 {
1174 if (aff < AFF_YES)
1175 return TILE_RAY_OUT_OF_RANGE;
1176 else if (aff == AFF_YES)
1177 return TILE_RAY;
1178 else if (aff == AFF_LANDING)
1179 return TILE_LANDING;
1180 else if (aff == AFF_MULTIPLE)
1181 return TILE_RAY_MULTI;
1182 else
1183 return 0;
1184 }
1185 #endif
1186
1187 #ifndef USE_TILE_LOCAL
_colour_aff_type(aff_type aff,bool target)1188 static colour_t _colour_aff_type(aff_type aff, bool target)
1189 {
1190 if (aff < 0)
1191 return DARKGREY;
1192 else if (aff < AFF_YES)
1193 return target ? RED : MAGENTA;
1194 else if (aff == AFF_YES)
1195 return target ? LIGHTRED : LIGHTMAGENTA;
1196 else if (aff == AFF_LANDING)
1197 return target ? LIGHTGREEN : GREEN;
1198 else if (aff == AFF_MULTIPLE)
1199 return target ? LIGHTCYAN : CYAN;
1200 else
1201 die("unhandled aff %d", aff);
1202 }
1203 #endif
1204
_draw_ray_cell(screen_cell_t & cell,coord_def p,bool on_target,aff_type aff)1205 static void _draw_ray_cell(screen_cell_t& cell, coord_def p, bool on_target,
1206 aff_type aff)
1207 {
1208 #ifdef USE_TILE
1209 UNUSED(on_target, p);
1210 cell.tile.dngn_overlay[cell.tile.num_dngn_overlay++] =
1211 _tileidx_aff_type(aff);
1212 #endif
1213 #ifndef USE_TILE_LOCAL
1214 const auto bcol = _colour_aff_type(aff, on_target);
1215 const auto mbcol = on_target ? bcol : bcol | COLFLAG_REVERSE;
1216 const auto cglyph = _get_ray_glyph(p, bcol, '*', mbcol);
1217 cell.glyph = cglyph.ch;
1218 cell.colour = cglyph.col;
1219 #endif
1220 }
1221
render(crawl_view_buffer & vbuf)1222 void direction_chooser_renderer::render(crawl_view_buffer& vbuf)
1223 {
1224 if (crawl_state.invisible_targeting)
1225 return;
1226 m_directn.draw_beam(vbuf);
1227 m_directn.highlight_summoner(vbuf);
1228 }
1229
draw_beam(crawl_view_buffer & vbuf)1230 void direction_chooser::draw_beam(crawl_view_buffer &vbuf)
1231 {
1232 if (!show_beam)
1233 return;
1234
1235 // Use the new API if implemented.
1236 if (hitfunc)
1237 {
1238 if (behaviour->targeted() && !hitfunc->set_aim(target()))
1239 return;
1240 const los_type los = hitfunc->can_affect_unseen()
1241 ? LOS_NONE : LOS_DEFAULT;
1242 for (radius_iterator ri(you.pos(), los); ri; ++ri)
1243 {
1244 aff_type aff = hitfunc->is_affected(*ri);
1245 if (aff
1246 && (!feat_is_solid(env.grid(*ri)) || hitfunc->can_affect_walls()))
1247 {
1248 auto& cell = vbuf(grid2view(*ri) - 1);
1249 _draw_ray_cell(cell, *ri, *ri == target(), aff);
1250 }
1251 }
1252
1253 return;
1254 }
1255
1256 // If we don't have a new beam to show, we're done.
1257 if (!have_beam)
1258 return;
1259
1260 // We shouldn't ever get a beam to an out-of-LOS target.
1261 ASSERT(you.see_cell(target()));
1262
1263 // Work with a copy in order not to mangle anything.
1264 ray_def ray = beam;
1265
1266 // Draw the new ray with magenta '*'s, not including your square
1267 // or the target square. Out-of-range cells get grey '*'s instead.
1268 for (; ray.pos() != target(); ray.advance())
1269 {
1270 const coord_def p = ray.pos();
1271 ASSERT(you.see_cell(p));
1272
1273 if (p == you.pos())
1274 continue;
1275
1276 const bool inrange = in_range(p);
1277 auto& cell = vbuf(grid2view(p) - 1);
1278 #ifdef USE_TILE
1279 cell.tile.dngn_overlay[cell.tile.num_dngn_overlay++] =
1280 inrange ? TILE_RAY : TILE_RAY_OUT_OF_RANGE;
1281 #endif
1282 #ifndef USE_TILE_LOCAL
1283 const auto bcol = inrange ? MAGENTA : DARKGREY;
1284 const auto cglyph = _get_ray_glyph(p, bcol, '*', bcol| COLFLAG_REVERSE);
1285 cell.glyph = cglyph.ch;
1286 cell.colour = cglyph.col;
1287 #endif
1288 }
1289 textcolour(LIGHTGREY);
1290
1291 // Only draw the ray over the target on tiles.
1292 #ifdef USE_TILE
1293 auto& cell = vbuf(grid2view(target()) - 1);
1294 cell.tile.dngn_overlay[cell.tile.num_dngn_overlay++] =
1295 in_range(ray.pos()) ? TILE_RAY : TILE_RAY_OUT_OF_RANGE;
1296 #endif
1297 }
1298
in_range(const coord_def & p) const1299 bool direction_chooser::in_range(const coord_def& p) const
1300 {
1301 if (!behaviour->targeted())
1302 return true;
1303 if (hitfunc)
1304 return hitfunc->valid_aim(p);
1305 return range < 0 || grid_distance(p, you.pos()) <= range;
1306 }
1307
1308 // Cycle to either the next (dir == 1) or previous (dir == -1) object
1309 // and update output accordingly if successful.
object_cycle(int dir)1310 void direction_chooser::object_cycle(int dir)
1311 {
1312 if (_find_square_wrapper(objfind_pos, dir,
1313 bind(_find_object, placeholders::_1, needs_path,
1314 range, hitfunc),
1315 hitfunc,
1316 dir > 0 ? LS_FLIPVH : LS_FLIPHV))
1317 {
1318 set_target(objfind_pos);
1319 }
1320 else
1321 flush_input_buffer(FLUSH_ON_FAILURE);
1322 }
1323
monster_cycle(int dir)1324 void direction_chooser::monster_cycle(int dir)
1325 {
1326 if (prefer_farthest)
1327 dir = -dir; // cycle from furthest to closest
1328 if (_find_square_wrapper(monsfind_pos, dir,
1329 bind(_find_monster, placeholders::_1, mode,
1330 needs_path, range, hitfunc),
1331 hitfunc))
1332 {
1333 set_target(monsfind_pos);
1334 }
1335 else
1336 flush_input_buffer(FLUSH_ON_FAILURE);
1337 }
1338
feature_cycle_forward(int feature)1339 void direction_chooser::feature_cycle_forward(int feature)
1340 {
1341 if (_find_square_wrapper(objfind_pos, 1,
1342 [feature](const coord_def& where)
1343 {
1344 return map_bounds(where)
1345 && (you.see_cell(where)
1346 || env.map_knowledge(where).seen())
1347 && is_feature(feature, where);
1348 },
1349 hitfunc,
1350 LS_FLIPVH))
1351 {
1352 set_target(objfind_pos);
1353 }
1354 else
1355 flush_input_buffer(FLUSH_ON_FAILURE);
1356 }
1357
update_previous_target() const1358 void direction_chooser::update_previous_target() const
1359 {
1360 you.prev_targ = MHITNOT;
1361 you.prev_grd_targ.reset();
1362
1363 // Maybe we should except just_looking here?
1364 const monster* m = monster_at(target());
1365 if (m && you.can_see(*m))
1366 you.prev_targ = m->mindex();
1367 else if (looking_at_you())
1368 you.prev_targ = MHITYOU;
1369 else
1370 you.prev_grd_targ = target();
1371 }
1372
select(bool allow_out_of_range,bool endpoint)1373 bool direction_chooser::select(bool allow_out_of_range, bool endpoint)
1374 {
1375 const monster* mons = monster_at(target());
1376
1377 if (restricts == DIR_SHADOW_STEP)
1378 {
1379 targeter_shadow_step &tgt =
1380 *static_cast<targeter_shadow_step*>(hitfunc);
1381 if (!tgt.has_additional_sites(target()))
1382 return false;
1383 }
1384
1385 // leap and shadow step never allow selecting from past the target point
1386 if ((restricts == DIR_LEAP
1387 || restricts == DIR_SHADOW_STEP
1388 || !allow_out_of_range)
1389 && !in_range(target()))
1390 {
1391 return false;
1392 }
1393 moves.isEndpoint = endpoint || (mons && _mon_exposed(mons));
1394 moves.isValid = true;
1395 moves.isTarget = true;
1396 update_previous_target();
1397 return true;
1398 }
1399
pickup_item()1400 bool direction_chooser::pickup_item()
1401 {
1402 item_def *ii = nullptr;
1403 if (in_bounds(target()))
1404 ii = env.map_knowledge(target()).item();
1405 if (!ii || !ii->is_valid(true))
1406 {
1407 mprf(MSGCH_EXAMINE_FILTER, "You can't see any item there.");
1408 return false;
1409 }
1410 ii->flags |= ISFLAG_THROWN; // make autoexplore greedy
1411
1412 // From this point, if there's no item, we'll fake one. False info means
1413 // it's out of bounds and taken, or a mimic.
1414 item_def *item = 0;
1415 unsigned short it = env.igrid(target());
1416 if (it != NON_ITEM)
1417 {
1418 item = &env.item[it];
1419 // Check if it appears to be the same item.
1420 if (!item->is_valid()
1421 || ii->base_type != item->base_type
1422 || ii->sub_type != item->sub_type
1423 // TODO: check for different unidentified items of the same base type
1424 && (!item_type_has_unidentified(item->base_type)
1425 || ii->sub_type == get_max_subtype(item->base_type))
1426 || ii->get_colour() != item->get_colour())
1427 {
1428 item = 0;
1429 }
1430 }
1431 if (item)
1432 item->flags |= ISFLAG_THROWN;
1433
1434 if (!just_looking) // firing/casting prompt
1435 {
1436 mprf(MSGCH_EXAMINE_FILTER, "Marked for pickup.");
1437 return false;
1438 }
1439
1440 moves.isValid = true;
1441 moves.isTarget = true;
1442 update_previous_target();
1443 return true;
1444 }
1445
handle_signals()1446 bool direction_chooser::handle_signals()
1447 {
1448 // If we've received a HUP signal then the user can't choose a
1449 // target.
1450 if (crawl_state.seen_hups)
1451 {
1452 moves.isValid = false;
1453 moves.isCancel = true;
1454 moves.cmd_result = CMD_NO_CMD;
1455
1456 mprf(MSGCH_ERROR, "Targeting interrupted by HUP signal.");
1457 return true;
1458 }
1459 return false;
1460 }
1461
1462 // Print out the initial prompt when targeting starts.
1463 // Prompts might get scrolled off if you have too few lines available;
1464 // we'll live with that.
show_initial_prompt()1465 void direction_chooser::show_initial_prompt()
1466 {
1467 if (crawl_state.invisible_targeting)
1468 return;
1469 behaviour->update_top_prompt(&top_prompt);
1470 describe_cell();
1471 const string err = behaviour ? behaviour->get_error() : "";
1472 if (!err.empty())
1473 mprf(MSGCH_PROMPT, "%s", err.c_str()); // can this push the prompt one line too tall?
1474 }
1475
print_target_description(bool & did_cloud) const1476 void direction_chooser::print_target_description(bool &did_cloud) const
1477 {
1478 if (targets_objects())
1479 print_target_object_description();
1480 else
1481 print_target_monster_description(did_cloud);
1482
1483 if (!in_range(target()))
1484 {
1485 mprf(MSGCH_EXAMINE_FILTER, "%s",
1486 hitfunc ? hitfunc->why_not.c_str() : "Out of range.");
1487 }
1488 }
1489
target_interesting_terrain_description() const1490 string direction_chooser::target_interesting_terrain_description() const
1491 {
1492 const dungeon_feature_type feature = env.grid(target());
1493
1494 // Only features which can make you lose the item are interesting.
1495 // FIXME: extract the naming logic from here and use
1496 // feat_has_solid_floor().
1497 switch (feature)
1498 {
1499 case DNGN_DEEP_WATER: return "water";
1500 case DNGN_LAVA: return "lava";
1501 default: return "";
1502 }
1503 }
1504
target_cloud_description() const1505 string direction_chooser::target_cloud_description() const
1506 {
1507 if (cloud_struct* cloud = cloud_at(target()))
1508 return cloud->cloud_name(true);
1509 else
1510 return "";
1511 }
1512
1513 template<typename C1, typename C2>
_append_container(C1 & container_base,const C2 & container_append)1514 static void _append_container(C1& container_base, const C2& container_append)
1515 {
1516 container_base.insert(container_base.end(),
1517 container_append.begin(), container_append.end());
1518 }
1519
target_sanctuary_description() const1520 string direction_chooser::target_sanctuary_description() const
1521 {
1522 return is_sanctuary(target()) ? "sanctuary" : "";
1523 }
1524
target_silence_description() const1525 string direction_chooser::target_silence_description() const
1526 {
1527 return silenced(target()) ? "silenced" : "";
1528 }
1529
_push_back_if_nonempty(const string & str,vector<string> * vec)1530 static void _push_back_if_nonempty(const string& str, vector<string>* vec)
1531 {
1532 if (!str.empty())
1533 vec->push_back(str);
1534 }
1535
print_target_monster_description(bool & did_cloud) const1536 void direction_chooser::print_target_monster_description(bool &did_cloud) const
1537 {
1538 // Do we see anything?
1539 const monster* mon = monster_at(target());
1540 if (!mon)
1541 return;
1542
1543 const bool visible = you.can_see(*mon);
1544 const bool exposed = _mon_exposed(mon);
1545 if (!visible && !exposed)
1546 return;
1547
1548 // OK, now we know that we have something to describe.
1549 vector<string> suffixes;
1550 string text;
1551 // Cell features go first.
1552 _append_container(suffixes, target_cell_description_suffixes());
1553 if (visible)
1554 {
1555 monster_info mi(mon);
1556 // Only describe the monster if you can actually see it.
1557 _append_container(suffixes, monster_description_suffixes(mi));
1558 text = get_monster_equipment_desc(mi);
1559 }
1560 else
1561 text = "Disturbance";
1562
1563 // Build the final description string.
1564 if (!suffixes.empty())
1565 {
1566 text += " ("
1567 + comma_separated_line(suffixes.begin(), suffixes.end(), ", ")
1568 + ")";
1569 }
1570
1571 mprf(MSGCH_PROMPT, "%s: <lightgrey>%s</lightgrey>",
1572 target_prefix ? target_prefix : !behaviour->targeted() ? "Look" : "Aim",
1573 text.c_str());
1574
1575 // If there's a cloud here, it's been described.
1576 did_cloud = true;
1577 }
1578
1579 // FIXME: this should really take a cell as argument.
target_cell_description_suffixes() const1580 vector<string> direction_chooser::target_cell_description_suffixes() const
1581 {
1582 vector<string> suffixes;
1583 // Things which describe the cell.
1584 _push_back_if_nonempty(target_cloud_description(), &suffixes);
1585 _push_back_if_nonempty(target_sanctuary_description(), &suffixes);
1586 _push_back_if_nonempty(target_silence_description(), &suffixes);
1587 _push_back_if_nonempty(target_interesting_terrain_description(), &suffixes);
1588
1589 return suffixes;
1590 }
1591
monster_description_suffixes(const monster_info & mi) const1592 vector<string> direction_chooser::monster_description_suffixes(
1593 const monster_info& mi) const
1594 {
1595 vector<string> suffixes;
1596
1597 _push_back_if_nonempty(mi.wounds_description(true), &suffixes);
1598 _push_back_if_nonempty(mi.constriction_description(), &suffixes);
1599 _append_container(suffixes, mi.attributes());
1600 _append_container(suffixes, _get_monster_desc_vector(mi));
1601 _append_container(suffixes, behaviour->get_monster_desc(mi));
1602
1603 return suffixes;
1604 }
1605
print_target_object_description() const1606 void direction_chooser::print_target_object_description() const
1607 {
1608 if (!you.see_cell(target()))
1609 return;
1610
1611 const item_def* item = top_item_at(target());
1612 if (!item)
1613 return;
1614
1615 // FIXME: remove the duplication with print_items_description().
1616 mprf(MSGCH_PROMPT, "%s: %s",
1617 target_prefix ? target_prefix : "Aim",
1618 menu_colour_item_name(*item, DESC_A).c_str());
1619 }
1620
print_items_description() const1621 void direction_chooser::print_items_description() const
1622 {
1623 if (!in_bounds(target()))
1624 return;
1625
1626 auto items = item_list_on_square(you.visible_igrd(target()));
1627
1628 if (items.empty())
1629 return;
1630
1631 if (items.size() == 1)
1632 {
1633 mprf(MSGCH_FLOOR_ITEMS, "<cyan>Item here:</cyan> %s.",
1634 menu_colour_item_name(*items[0], DESC_A).c_str());
1635 }
1636 else
1637 mprf(MSGCH_FLOOR_ITEMS, "<cyan>Items here: </cyan> %s.", item_message(items).c_str());
1638 }
1639
print_floor_description(bool boring_too) const1640 void direction_chooser::print_floor_description(bool boring_too) const
1641 {
1642 const dungeon_feature_type feat = env.grid(target());
1643 if (!boring_too && feat == DNGN_FLOOR)
1644 return;
1645
1646 #ifdef DEBUG_DIAGNOSTICS
1647 // [ds] Be more verbose in debug mode.
1648 if (you.wizard)
1649 _debug_describe_feature_at(target());
1650 else
1651 #endif
1652 mprf(MSGCH_EXAMINE_FILTER, "%s.",
1653 feature_description_at(target(), true).c_str());
1654 }
1655
reinitialize_move_flags()1656 void direction_chooser::reinitialize_move_flags()
1657 {
1658 moves.isValid = false;
1659 moves.isTarget = false;
1660 moves.isCancel = false;
1661 moves.isEndpoint = false;
1662 moves.choseRay = false;
1663 }
1664
1665 // Returns true if we've completed targeting.
select_compass_direction(const coord_def & delta)1666 bool direction_chooser::select_compass_direction(const coord_def& delta)
1667 {
1668 if (restricts != DIR_TARGET && restricts != DIR_SHADOW_STEP)
1669 {
1670 // A direction is allowed, and we've selected it.
1671 moves.delta = delta;
1672 // Needed for now...eventually shouldn't be necessary
1673 set_target(you.pos() + moves.delta);
1674 moves.isValid = true;
1675 moves.isTarget = false;
1676 have_beam = false;
1677 show_beam = false;
1678 moves.choseRay = false;
1679 return true;
1680 }
1681 else
1682 {
1683 // Direction not allowed, so just move in that direction.
1684 // Maybe make this a bigger jump?
1685 set_target(target() + delta * 3);
1686 return false;
1687 }
1688 }
1689
toggle_beam()1690 void direction_chooser::toggle_beam()
1691 {
1692 if (!needs_path)
1693 {
1694 mprf(MSGCH_EXAMINE_FILTER, "This spell doesn't need a beam path.");
1695 return;
1696 }
1697
1698 show_beam = !show_beam;
1699 need_viewport_redraw = true;
1700
1701 if (show_beam)
1702 {
1703 have_beam = find_ray(you.pos(), target(), beam,
1704 opc_solid_see, you.current_vision);
1705 }
1706 }
1707
select_previous_target()1708 bool direction_chooser::select_previous_target()
1709 {
1710 if (const monster* mon_target = _get_current_target())
1711 {
1712 // We have all the information we need.
1713 moves.isValid = true;
1714 moves.isTarget = true;
1715 set_target(mon_target->pos());
1716 if (!just_looking)
1717 have_beam = false;
1718
1719 return !just_looking;
1720 }
1721 else
1722 {
1723 mprf(MSGCH_EXAMINE_FILTER, "Your target is gone.");
1724 flush_prev_message();
1725 return false;
1726 }
1727 }
1728
looking_at_you() const1729 bool direction_chooser::looking_at_you() const
1730 {
1731 return target() == you.pos();
1732 }
1733
handle_movement_key(command_type key_command,bool * loop_done)1734 void direction_chooser::handle_movement_key(command_type key_command,
1735 bool* loop_done)
1736 {
1737 const int compass_idx = _targeting_cmd_to_compass(key_command);
1738 if (compass_idx != -1)
1739 {
1740 const coord_def& delta = Compass[compass_idx];
1741 const bool unshifted = (shift_direction(key_command) != key_command);
1742 if (unshifted)
1743 set_target(target() + delta);
1744 else if (!allow_shift_dir)
1745 mpr("You can't do that.");
1746 else
1747 *loop_done = select_compass_direction(delta);
1748 }
1749 }
1750
handle_wizard_command(command_type key_command,bool * loop_done)1751 void direction_chooser::handle_wizard_command(command_type key_command,
1752 bool* loop_done)
1753 {
1754 #ifdef WIZARD
1755 if (!you.wizard)
1756 return;
1757
1758 monster* const m = monster_at(target());
1759 string marker_result = "";
1760
1761 // These commands do something even if there's no monster there.
1762 switch (key_command)
1763 {
1764 case CMD_TARGET_WIZARD_MOVE:
1765 wizard_move_player_or_monster(target());
1766 *loop_done = true;
1767 return;
1768
1769 case CMD_TARGET_WIZARD_MISCAST:
1770 if (m)
1771 debug_miscast(m->mindex());
1772 else if (looking_at_you())
1773 debug_miscast(NON_MONSTER);
1774 return;
1775
1776 // Note that this is a wizard-only command.
1777 case CMD_TARGET_CYCLE_BEAM:
1778 show_beam = true;
1779 have_beam = find_ray(you.pos(), target(), beam,
1780 opc_solid_see, you.current_vision, show_beam);
1781 need_viewport_redraw = true;
1782 return;
1783
1784 case CMD_TARGET_WIZARD_DEBUG_PORTAL:
1785 mprf(MSGCH_DIAGNOSTICS, "Trying to run portal debug at %d/%d...",
1786 target().x, target().y);
1787
1788 marker_result =
1789 env.markers.property_at(target(), MAT_ANY, "portal_debug");
1790
1791 mprf(MSGCH_DIAGNOSTICS, "Got result: %s!",
1792 marker_result.empty() ? "nothing" : marker_result.c_str());
1793
1794 return;
1795
1796 case CMD_TARGET_WIZARD_HURT_MONSTER:
1797 if (looking_at_you())
1798 {
1799 set_hp(1);
1800 print_stats();
1801 update_screen();
1802 }
1803 break;
1804
1805 case CMD_TARGET_WIZARD_CREATE_MIMIC:
1806 if (target() != you.pos())
1807 {
1808 wizard_create_feature(target());
1809 need_viewport_redraw = true;
1810 }
1811 return;
1812
1813 default:
1814 break;
1815 }
1816
1817 // Everything below here doesn't work if there's no monster.
1818 if (!m)
1819 return;
1820
1821 const int mid = m->mindex();
1822
1823 switch (key_command)
1824 {
1825 case CMD_TARGET_WIZARD_PATHFIND: debug_pathfind(mid); break;
1826 case CMD_TARGET_WIZARD_DEBUG_MONSTER: debug_stethoscope(mid); break;
1827 case CMD_TARGET_WIZARD_MAKE_SHOUT: debug_make_monster_shout(m); break;
1828 case CMD_TARGET_WIZARD_MAKE_FRIENDLY:
1829 _wizard_make_friendly(m);
1830 need_text_redraw = true;
1831 break;
1832
1833 case CMD_TARGET_WIZARD_GIVE_ITEM: wizard_give_monster_item(m); break;
1834 case CMD_TARGET_WIZARD_POLYMORPH: wizard_polymorph_monster(m); break;
1835
1836 case CMD_TARGET_WIZARD_GAIN_LEVEL:
1837 wizard_gain_monster_level(m);
1838 break;
1839
1840 case CMD_TARGET_WIZARD_BLESS_MONSTER:
1841 wizard_apply_monster_blessing(m);
1842 break;
1843
1844 case CMD_TARGET_WIZARD_MAKE_SUMMONED:
1845 wizard_make_monster_summoned(m);
1846 break;
1847
1848 case CMD_TARGET_WIZARD_HEAL_MONSTER:
1849 if (m->hit_points < m->max_hit_points)
1850 {
1851 m->hit_points = m->max_hit_points;
1852 need_all_redraw = true;
1853 }
1854 break;
1855
1856 case CMD_TARGET_WIZARD_HURT_MONSTER:
1857 m->hit_points = 1;
1858 mpr("Brought monster down to 1 HP.");
1859 flush_prev_message();
1860 break;
1861
1862 case CMD_TARGET_WIZARD_BANISH_MONSTER:
1863 m->banish(&you, "", 0, true);
1864 break;
1865
1866 case CMD_TARGET_WIZARD_KILL_MONSTER:
1867 monster_die(*m, KILL_YOU, NON_MONSTER);
1868 break;
1869
1870 default:
1871 return;
1872 }
1873 redraw_screen();
1874 update_screen();
1875 #endif
1876 }
1877
do_redraws()1878 void direction_chooser::do_redraws()
1879 {
1880 if (crawl_state.invisible_targeting)
1881 return;
1882
1883 // Check if our targeting behaviour forces a redraw.
1884 if (behaviour->should_redraw())
1885 {
1886 need_all_redraw = true;
1887 behaviour->clear_redraw();
1888 }
1889
1890 if (need_all_redraw)
1891 {
1892 need_viewport_redraw = true;
1893 need_text_redraw = true;
1894 need_cursor_redraw = true;
1895 need_all_redraw = false;
1896 }
1897
1898 if (need_viewport_redraw)
1899 {
1900 viewwindow(false, false, nullptr, &renderer);
1901 need_viewport_redraw = false;
1902 }
1903
1904 if (need_text_redraw)
1905 {
1906 msgwin_clear_temporary();
1907 describe_cell();
1908 need_text_redraw = false;
1909 }
1910
1911 if (need_cursor_redraw || Options.use_fake_cursor)
1912 {
1913 cursorxy(crawl_view.grid2screen(target()));
1914 #ifdef USE_TILE_WEB
1915 // cursorxy doesn't place the cursor in Webtiles, we do it manually here
1916 // This is by design, since we don't want to use the mouse cursor for
1917 // the overview map.
1918 tiles.place_cursor(CURSOR_MOUSE, target());
1919 #endif
1920 need_cursor_redraw = false;
1921 }
1922 }
1923
find_summoner()1924 coord_def direction_chooser::find_summoner()
1925 {
1926 const monster* mon = monster_at(target());
1927
1928 if (mon && mon->is_summoned()
1929 // Don't leak information about rakshasa mirrored illusions.
1930 && !mon->has_ench(ENCH_PHANTOM_MIRROR)
1931 // Don't leak information about invisible or out-of-los summons.
1932 && you.can_see(*mon))
1933 {
1934 const monster *summ = monster_by_mid(mon->summoner);
1935 // Don't leak information about invisible summoners.
1936 if (summ && you.can_see(*summ))
1937 return summ->pos();
1938 }
1939 return INVALID_COORD;
1940 }
1941
highlight_summoner(crawl_view_buffer & vbuf)1942 void direction_chooser::highlight_summoner(crawl_view_buffer &vbuf)
1943 {
1944 const coord_def summ_loc = find_summoner();
1945
1946 if (summ_loc == INVALID_COORD || !you.see_cell(summ_loc))
1947 return;
1948
1949 auto& cell = vbuf(grid2view(summ_loc) - 1);
1950 #ifdef USE_TILE
1951 cell.tile.is_highlighted_summoner = true;
1952 #endif
1953 #if defined(USE_TILE_WEB) || !defined(USE_TILE)
1954 cell.colour = CYAN | COLFLAG_REVERSE;
1955 #endif
1956 }
1957
tiles_update_target()1958 bool direction_chooser::tiles_update_target()
1959 {
1960 #ifdef USE_TILE
1961 const coord_def& gc = tiles.get_cursor();
1962 if (gc != NO_CURSOR && map_bounds(gc))
1963 {
1964 set_target(gc);
1965 return true;
1966 }
1967 #endif
1968 return false;
1969 }
1970
move_to_you()1971 void direction_chooser::move_to_you()
1972 {
1973 moves.isValid = true;
1974 moves.isTarget = true;
1975 set_target(you.pos());
1976 moves.delta.reset();
1977 }
1978
full_describe()1979 void direction_chooser::full_describe()
1980 {
1981 vector <monster_info> list_mons;
1982 vector<item_def> list_items;
1983 vector<coord_def> list_features;
1984
1985 vector <monster *> nearby_mons = get_nearby_monsters(true, false, false,
1986 false, true, true,
1987 range);
1988 for (auto m : nearby_mons)
1989 if (_want_target_monster(m, mode, hitfunc))
1990 list_mons.push_back(monster_info(m));
1991
1992 if (targets_objects() || just_looking)
1993 _get_nearby_items(list_items, needs_path, range, hitfunc);
1994
1995 if (hitfunc && hitfunc->can_affect_walls() || just_looking)
1996 _get_nearby_features(list_features, needs_path, range, hitfunc);
1997
1998 if (list_mons.empty() && list_items.empty() && list_features.empty())
1999 {
2000 mprf(MSGCH_EXAMINE_FILTER, "There are no valid targets to list.");
2001 flush_prev_message();
2002 return;
2003 }
2004
2005 const coord_def choice =
2006 _full_describe_menu(list_mons, list_items, list_features,
2007 just_looking ? "target/travel" : "target");
2008
2009 if (choice != coord_def(-1, -1))
2010 set_target(choice);
2011 need_all_redraw = true;
2012 }
2013
describe_target()2014 void direction_chooser::describe_target()
2015 {
2016 if (!map_bounds(target()) || !env.map_knowledge(target()).known())
2017 return;
2018 full_describe_square(target(), false);
2019 need_all_redraw = true;
2020 }
2021
show_help()2022 void direction_chooser::show_help()
2023 {
2024 show_targeting_help();
2025 redraw_screen();
2026 update_screen();
2027 clear_messages(true);
2028 need_all_redraw = true;
2029 }
2030
process_command(command_type command)2031 bool direction_chooser::process_command(command_type command)
2032 {
2033 bool loop_done = false;
2034
2035 // Move flags are volitile, reset them to defaults before each command
2036 reinitialize_move_flags();
2037
2038 switch (command)
2039 {
2040 case CMD_TARGET_SHOW_PROMPT: describe_cell(); break;
2041
2042 case CMD_TARGET_TOGGLE_BEAM:
2043 if (!just_looking)
2044 toggle_beam();
2045 break;
2046
2047 case CMD_TARGET_EXCLUDE:
2048 if (!just_looking)
2049 break;
2050
2051 if (!is_map_persistent())
2052 mpr("You cannot set exclusions on this level.");
2053 else
2054 {
2055 const bool was_excluded = is_exclude_root(target());
2056 cycle_exclude_radius(target());
2057 need_viewport_redraw = true;
2058 const bool is_excluded = is_exclude_root(target());
2059 if (!was_excluded && is_excluded)
2060 mpr("Placed new exclusion.");
2061 else if (was_excluded && !is_excluded)
2062 mpr("Removed exclusion.");
2063 else
2064 mpr("Reduced exclusion size to a single square.");
2065 }
2066
2067 need_cursor_redraw = true;
2068 break;
2069
2070 case CMD_TARGET_FIND_YOU: move_to_you(); break;
2071 case CMD_TARGET_FIND_TRAP: feature_cycle_forward('^'); break;
2072 case CMD_TARGET_FIND_PORTAL: feature_cycle_forward('\\'); break;
2073 case CMD_TARGET_FIND_ALTAR: feature_cycle_forward('_'); break;
2074 case CMD_TARGET_FIND_UPSTAIR: feature_cycle_forward('<'); break;
2075 case CMD_TARGET_FIND_DOWNSTAIR: feature_cycle_forward('>'); break;
2076
2077 case CMD_TARGET_MAYBE_PREV_TARGET:
2078 loop_done = looking_at_you() ? select_previous_target()
2079 : select(false, false);
2080 break;
2081
2082 case CMD_TARGET_PREV_TARGET: loop_done = select_previous_target(); break;
2083
2084 // some modifiers to the basic selection command
2085 case CMD_TARGET_SELECT: loop_done = select(false, false); break;
2086 case CMD_TARGET_SELECT_FORCE: loop_done = select(true, false); break;
2087 case CMD_TARGET_SELECT_ENDPOINT: loop_done = select(false, true); break;
2088 case CMD_TARGET_SELECT_FORCE_ENDPOINT: loop_done = select(true,true); break;
2089
2090 #ifdef USE_TILE
2091 case CMD_TARGET_MOUSE_SELECT:
2092 if (tiles_update_target())
2093 loop_done = select(false, false);
2094 break;
2095
2096 case CMD_TARGET_MOUSE_MOVE: tiles_update_target(); break;
2097 #endif
2098
2099 case CMD_TARGET_GET: loop_done = pickup_item(); break;
2100
2101 case CMD_TARGET_CYCLE_BACK:
2102 if (!targets_objects())
2103 monster_cycle(-1);
2104 else
2105 object_cycle(-1);
2106 break;
2107
2108 case CMD_TARGET_OBJ_CYCLE_BACK:
2109 object_cycle(-1);
2110 break;
2111
2112 case CMD_TARGET_CYCLE_FORWARD:
2113 if (!targets_objects())
2114 monster_cycle(1);
2115 else
2116 object_cycle(1);
2117 break;
2118
2119 case CMD_TARGET_OBJ_CYCLE_FORWARD:
2120 object_cycle(1);
2121 break;
2122
2123 case CMD_TARGET_CANCEL:
2124 loop_done = true;
2125 moves.isCancel = true;
2126 break;
2127
2128 case CMD_TARGET_FULL_DESCRIBE: full_describe(); break;
2129 case CMD_TARGET_DESCRIBE: describe_target(); break;
2130 case CMD_TARGET_HELP: show_help(); break;
2131
2132 default:
2133 if (moves.fire_context
2134 && moves.fire_context->targeter_handles_key(command))
2135 {
2136 // because of the somewhat convoluted way in which action selection
2137 // is handled, some commands can only be handled if the direction
2138 // chooser has been called via a quiver::action. Otherwise, we
2139 // ignore these commands.
2140 moves.isValid = false;
2141 moves.isCancel = true;
2142 moves.cmd_result = static_cast<int>(command);
2143 loop_done = true;
2144 break;
2145 }
2146 // Some blocks of keys with similar handling.
2147 handle_movement_key(command, &loop_done);
2148 handle_wizard_command(command, &loop_done);
2149 break;
2150 }
2151
2152 return loop_done;
2153 }
2154
finalize_moves()2155 void direction_chooser::finalize_moves()
2156 {
2157 moves.choseRay = have_beam;
2158 moves.ray = beam;
2159
2160 // We need this for directional explosions, otherwise they'll explode one
2161 // square away from the player.
2162 _extend_move_to_edge(moves);
2163
2164 #ifdef USE_TILE
2165 tiles.place_cursor(CURSOR_MOUSE, NO_CURSOR);
2166 #endif
2167 }
2168
2169 class UIDirectionChooserView
2170 #ifdef USE_TILE_LOCAL
2171 : public ui::Widget
2172 #else
2173 : public ui::OverlayWidget
2174 #endif
2175 {
2176 public:
UIDirectionChooserView(direction_chooser & dc)2177 UIDirectionChooserView(direction_chooser& dc) :
2178 m_dc(dc), old_target(dc.target())
2179 {
2180 }
~UIDirectionChooserView()2181 ~UIDirectionChooserView() {}
2182
_render()2183 void _render() override
2184 {
2185 if (crawl_state.invisible_targeting)
2186 return;
2187
2188 #ifndef USE_TILE_LOCAL
2189 // This call ensures that the hud will get redrawn in console any time
2190 // need_all_redraw is set. Minimally, this needs to happen on the
2191 // initial call, as well as after any popups on top of this widget.
2192 if (m_dc.need_all_redraw)
2193 redraw_screen(false);
2194 #endif
2195
2196 #ifdef USE_TILE_LOCAL
2197 // We always have to redraw the viewport, because ui::redraw() will call
2198 // redraw_screen in case the window has been resized.
2199 m_dc.need_viewport_redraw = true;
2200 #endif
2201 m_dc.do_redraws();
2202
2203 #ifdef USE_TILE_LOCAL
2204 tiles.render_current_regions();
2205 glmanager->reset_transform();
2206 #endif
2207 }
2208
_allocate_region()2209 void _allocate_region() override
2210 {
2211 m_dc.need_all_redraw = true;
2212 _expose();
2213 }
2214
on_event(const ui::Event & ev)2215 bool on_event(const ui::Event& ev) override
2216 {
2217 if (ev.type() == ui::Event::Type::KeyDown)
2218 {
2219 auto key = static_cast<const ui::KeyEvent&>(ev).key();
2220 key = unmangle_direction_keys(key, KMC_TARGETING, false);
2221
2222 const auto command = m_dc.behaviour->get_command(key);
2223 // XX a bit ugly to do this here..
2224 if (m_dc.behaviour->needs_path != MB_MAYBE)
2225 {
2226 m_dc.needs_path = tobool(m_dc.behaviour->needs_path, true);
2227 m_dc.show_beam = !m_dc.just_looking && m_dc.needs_path;
2228 // XX code duplication
2229 m_dc.have_beam = m_dc.show_beam
2230 && find_ray(you.pos(), m_dc.target(), m_dc.beam,
2231 opc_solid_see, you.current_vision);
2232 m_dc.need_text_redraw = true;
2233 m_dc.need_viewport_redraw = true;
2234 m_dc.need_cursor_redraw = true;
2235 }
2236
2237 string top_prompt = m_dc.top_prompt;
2238 m_dc.behaviour->update_top_prompt(&top_prompt);
2239
2240 if (m_dc.top_prompt != top_prompt)
2241 {
2242 _expose();
2243 m_dc.top_prompt = top_prompt;
2244 }
2245
2246 process_command(command);
2247
2248 // Flush the input buffer before the next command.
2249 if (!crawl_state.is_replaying_keys())
2250 flush_input_buffer(FLUSH_BEFORE_COMMAND);
2251 return true;
2252 }
2253
2254 #ifdef USE_TILE_LOCAL
2255 if (ev.type() == ui::Event::Type::MouseMove
2256 || ev.type() == ui::Event::Type::MouseDown)
2257 {
2258 auto wm_event = to_wm_event(static_cast<const ui::MouseEvent&>(ev));
2259 tiles.handle_mouse(wm_event);
2260 process_command(ev.type() == ui::Event::Type::MouseMove ?
2261 CMD_TARGET_MOUSE_MOVE :
2262 CMD_TARGET_MOUSE_SELECT);
2263 return true;
2264 }
2265 #endif
2266
2267 return false;
2268 }
2269
process_command(command_type cmd)2270 void process_command(command_type cmd)
2271 {
2272 // the chooser needs a cursor, but we need to hide it here
2273 cursor_control cc(false);
2274 bool loop_done = m_dc.process_command(cmd);
2275
2276 // Don't allow going out of bounds.
2277 if (!crawl_view.in_viewport_g(m_dc.target()))
2278 m_dc.set_target(old_target);
2279
2280 if (loop_done)
2281 {
2282 m_is_alive = false;
2283
2284 if (!m_dc.just_looking && !m_dc.move_is_ok())
2285 {
2286 m_dc.moves.isCancel = true;
2287 m_dc.moves.isValid = false;
2288 }
2289 return;
2290 }
2291
2292 // Redraw whatever is necessary.
2293 if (old_target != m_dc.target())
2294 {
2295 m_dc.have_beam = m_dc.show_beam
2296 && find_ray(you.pos(), m_dc.target(), m_dc.beam,
2297 opc_solid_see, you.current_vision);
2298 m_dc.need_text_redraw = true;
2299 m_dc.need_viewport_redraw = true;
2300 m_dc.need_cursor_redraw = true;
2301 }
2302
2303 if (m_dc.need_viewport_redraw || m_dc.need_cursor_redraw
2304 || m_dc.need_text_redraw || m_dc.need_all_redraw)
2305 {
2306 _expose();
2307 }
2308
2309 old_target = m_dc.target();
2310 }
2311
is_alive()2312 bool is_alive()
2313 {
2314 return m_is_alive;
2315 }
2316
2317 private:
2318 direction_chooser& m_dc;
2319 coord_def old_target;
2320 bool m_is_alive = true;
2321 };
2322
update_validity()2323 void direction_chooser::update_validity()
2324 {
2325 if (!select(false, moves.isEndpoint) || !move_is_ok())
2326 {
2327 moves.isCancel = true;
2328 moves.isValid = false;
2329 return;
2330 }
2331 // select() should handle setting bools appropriately on success
2332 }
2333
noninteractive()2334 bool direction_chooser::noninteractive()
2335 {
2336 // if target is unset, this will find previous or closest target; if
2337 // target is set this will adjust targeting depending on custom
2338 // behavior
2339 if (moves.find_target)
2340 set_target(find_default_target());
2341
2342 update_validity();
2343 finalize_moves();
2344 moves.cmd_result = moves.isValid && !moves.isCancel ? CMD_FIRE : CMD_NO_CMD;
2345 return moves.cmd_result == CMD_FIRE;
2346 }
2347
choose_direction()2348 bool direction_chooser::choose_direction()
2349 {
2350 #ifdef USE_TILE
2351 ui::cutoff_point ui_cutoff_point;
2352 #endif
2353
2354 #ifndef USE_TILE_LOCAL
2355 cursor_control ccon(!Options.use_fake_cursor);
2356 #endif
2357 #ifdef DGAMELAUNCH
2358 suppress_dgl_clrscr no_blinking;
2359 #endif
2360
2361 mouse_control mc(needs_path && !just_looking ? MOUSE_MODE_TARGET_PATH
2362 : MOUSE_MODE_TARGET);
2363 targeter_smite legacy_range(&you, range, 0, 0, true);
2364 range_view_annotator rva(hitfunc ? hitfunc :
2365 (range >= 0) ? &legacy_range : nullptr);
2366
2367 // init
2368 moves.delta.reset();
2369
2370 // Find a default target.
2371 set_target(!default_place.origin() ? default_place
2372 : find_default_target());
2373
2374 objfind_pos = monsfind_pos = target();
2375 if (prefer_farthest && moves.target != you.pos())
2376 monster_cycle(1);
2377
2378 // If requested, show the beam on startup.
2379 if (show_beam)
2380 {
2381 have_beam = find_ray(you.pos(), target(), beam,
2382 opc_solid_see, you.current_vision);
2383 need_viewport_redraw = have_beam;
2384 }
2385 if (hitfunc)
2386 need_viewport_redraw = true;
2387
2388 clear_messages();
2389 msgwin_temporary_mode tmp;
2390
2391 unwind_bool save_more(crawl_state.show_more_prompt, false);
2392 show_initial_prompt();
2393 need_text_redraw = false;
2394
2395 auto directn_view = make_shared<UIDirectionChooserView>(*this);
2396
2397 #ifdef USE_TILE_LOCAL
2398 unwind_bool inhibit_rendering(ui::should_render_current_regions, false);
2399 #endif
2400
2401 // TODO: ideally crawl_state.invisible_targeting would suppress the redraws
2402 // associated with these ui calls, but I'm not sure of a clean way to make
2403 // that work
2404 ui::push_layout(directn_view, KMC_TARGETING);
2405 directn_view->_queue_allocation();
2406 while (directn_view->is_alive() && !handle_signals())
2407 ui::pump_events();
2408 ui::pop_layout();
2409
2410 finalize_moves();
2411 if (moves.isValid && !moves.isCancel)
2412 moves.cmd_result = CMD_FIRE;
2413 return moves.isValid;
2414 }
2415
get_terse_square_desc(const coord_def & gc)2416 string get_terse_square_desc(const coord_def &gc)
2417 {
2418 string desc = "";
2419 const char *unseen_desc = "[unseen terrain]";
2420
2421 if (gc == you.pos())
2422 desc = you.your_name;
2423 else if (!map_bounds(gc))
2424 desc = unseen_desc;
2425 else if (!you.see_cell(gc))
2426 {
2427 if (env.map_knowledge(gc).seen())
2428 {
2429 desc = "[" + feature_description_at(gc, false, DESC_PLAIN)
2430 + "]";
2431 }
2432 else
2433 desc = unseen_desc;
2434 }
2435 else if (monster_at(gc) && you.can_see(*monster_at(gc)))
2436 desc = monster_at(gc)->full_name(DESC_PLAIN);
2437 else if (you.visible_igrd(gc) != NON_ITEM)
2438 {
2439 if (env.item[you.visible_igrd(gc)].defined())
2440 desc = env.item[you.visible_igrd(gc)].name(DESC_PLAIN);
2441 }
2442 else
2443 desc = feature_description_at(gc, false, DESC_PLAIN);
2444
2445 return desc;
2446 }
2447
terse_describe_square(const coord_def & c,bool in_range)2448 void terse_describe_square(const coord_def &c, bool in_range)
2449 {
2450 if (!you.see_cell(c))
2451 _describe_oos_square(c);
2452 else if (in_bounds(c))
2453 _describe_cell(c, in_range);
2454 }
2455
2456 // Get description of the "top" thing in a square; for mouseover text.
get_square_desc(const coord_def & c,describe_info & inf)2457 void get_square_desc(const coord_def &c, describe_info &inf)
2458 {
2459 const dungeon_feature_type feat = env.map_knowledge(c).feat();
2460 const cloud_type cloud = env.map_knowledge(c).cloud();
2461
2462 if (const monster_info *mi = env.map_knowledge(c).monsterinfo())
2463 {
2464 // First priority: monsters.
2465 string desc = uppercase_first(get_monster_equipment_desc(*mi))
2466 + ".\n";
2467 const string wounds = mi->wounds_description_sentence();
2468 if (!wounds.empty())
2469 desc += uppercase_first(wounds) + "\n";
2470 const string constrictions = mi->constriction_description();
2471 if (!constrictions.empty())
2472 desc += "It is " + constrictions + ".\n";
2473 desc += _get_monster_desc(*mi);
2474
2475 inf.title = desc;
2476
2477 bool temp = false;
2478 get_monster_db_desc(*mi, inf, temp);
2479 }
2480 else if (const item_def *obj = env.map_knowledge(c).item())
2481 {
2482 // Second priority: objects.
2483 get_item_desc(*obj, inf);
2484 }
2485 else if (feat != DNGN_UNSEEN && feat != DNGN_FLOOR
2486 && !feat_is_wall(feat) && !feat_is_tree(feat))
2487 {
2488 // Third priority: features.
2489 get_feature_desc(c, inf);
2490 }
2491 else // Fourth priority: clouds.
2492 inf.body << get_cloud_desc(cloud);
2493 }
2494
2495 // Show a description of the only thing on a square, or a selection menu with
2496 // visible things on the square if there are many. For x-v and similar contexts.
2497 // Used for both in- and out-of-los cells.
full_describe_square(const coord_def & c,bool cleanup)2498 void full_describe_square(const coord_def &c, bool cleanup)
2499 {
2500 vector<monster_info> list_mons;
2501 vector<item_def> list_items;
2502 vector<coord_def> list_features;
2503 int quantity = 0;
2504
2505 const monster_info *mi = env.map_knowledge(c).monsterinfo();
2506 item_def *obj = env.map_knowledge(c).item();
2507 const dungeon_feature_type feat = env.map_knowledge(c).feat();
2508
2509 if (mi)
2510 {
2511 list_mons.emplace_back(*mi);
2512 ++quantity;
2513 }
2514 if (obj)
2515 {
2516 list_items = item_list_in_stash(c);
2517 quantity += list_items.size();
2518 }
2519 // I'm not sure if features should be included. But it seems reasonable to
2520 // at least include what full_describe_view shows
2521 if (feat_stair_direction(feat) != CMD_NO_CMD || feat_is_trap(feat))
2522 {
2523 list_features.push_back(c);
2524 ++quantity;
2525 }
2526
2527 if (quantity > 1)
2528 {
2529 _full_describe_menu(list_mons, list_items, list_features, "", true,
2530 you.see_cell(c) ? "What do you want to examine?"
2531 : "What do you want to remember?");
2532 }
2533 else if (quantity == 1)
2534 {
2535 if (mi)
2536 describe_monsters(*mi);
2537 else if (list_items.size())
2538 describe_item(*obj);
2539 else
2540 describe_feature_wide(c);
2541 }
2542 else
2543 describe_feature_wide(c);
2544
2545 if (cleanup)
2546 {
2547 redraw_screen();
2548 update_screen();
2549 clear_messages();
2550 }
2551 }
2552
_extend_move_to_edge(dist & moves)2553 static void _extend_move_to_edge(dist &moves)
2554 {
2555 if (moves.delta.origin())
2556 return;
2557
2558 // Now the tricky bit - extend the target x,y out to map edge.
2559 int mx = 0, my = 0;
2560
2561 if (moves.delta.x > 0)
2562 mx = (GXM - 1) - you.pos().x;
2563 if (moves.delta.x < 0)
2564 mx = you.pos().x;
2565
2566 if (moves.delta.y > 0)
2567 my = (GYM - 1) - you.pos().y;
2568 if (moves.delta.y < 0)
2569 my = you.pos().y;
2570
2571 if (mx != 0 && my != 0)
2572 mx = my = min(mx, my);
2573
2574 moves.target.x = you.pos().x + moves.delta.x * mx;
2575 moves.target.y = you.pos().y + moves.delta.y * my;
2576 }
2577
2578 // Attempts to describe a square that's not in line-of-sight. If
2579 // there's a stash on the square, announces the top item and number
2580 // of items, otherwise, if there's a stair that's in the travel
2581 // cache and noted in the Dungeon (O)verview, names the stair.
_describe_oos_square(const coord_def & where)2582 static void _describe_oos_square(const coord_def& where)
2583 {
2584 mprf(MSGCH_EXAMINE_FILTER, "You can't see that place.");
2585
2586 if (!in_bounds(where) || !env.map_knowledge(where).seen())
2587 {
2588 #ifdef DEBUG_DIAGNOSTICS
2589 if (!in_bounds(where))
2590 dprf("(out of bounds)");
2591 else
2592 dprf("(map: %x)", env.map_knowledge(where).flags);
2593 #endif
2594 return;
2595 }
2596
2597 describe_stash(where);
2598 _describe_oos_feature(where);
2599 #ifdef DEBUG_DIAGNOSTICS
2600 _debug_describe_feature_at(where);
2601 #endif
2602 }
2603
_mons_is_valid_target(const monster * mon,targ_mode_type mode,int range)2604 static bool _mons_is_valid_target(const monster* mon, targ_mode_type mode,
2605 int range)
2606 {
2607 // Monsters that are no threat to you don't count as monsters.
2608 if (!mons_is_threatening(*mon) && !mons_class_is_test(mon->type))
2609 return false;
2610
2611 // Don't target submerged monsters.
2612 if (mode != TARG_HOSTILE_SUBMERGED && mon->submerged())
2613 return false;
2614
2615 // Don't usually target unseen monsters...
2616 if (!mon->visible_to(&you))
2617 {
2618 // ...unless it creates a "disturbance in the water".
2619 // Since you can't see the monster, assume it's not a friend.
2620 return mode != TARG_FRIEND
2621 && _mon_exposed(mon)
2622 && i_feel_safe(false, false, true, true, range);
2623 }
2624
2625 return true;
2626 }
2627
_want_target_monster(const monster * mon,targ_mode_type mode,targeter * hitfunc)2628 static bool _want_target_monster(const monster *mon, targ_mode_type mode,
2629 targeter* hitfunc)
2630 {
2631 if (hitfunc && !hitfunc->affects_monster(monster_info(mon)))
2632 return false;
2633 switch (mode)
2634 {
2635 case TARG_ANY:
2636 return true;
2637 case TARG_HOSTILE:
2638 case TARG_HOSTILE_SUBMERGED:
2639 return mons_attitude(*mon) == ATT_HOSTILE;
2640 case TARG_FRIEND:
2641 return mon->friendly();
2642 case TARG_INJURED_FRIEND:
2643 if (mon->friendly() && mons_get_damage_level(*mon) > MDAM_OKAY)
2644 return true;
2645 return !mon->wont_attack() && !mon->neutral()
2646 && unpacifiable_reason(*mon).empty();
2647 case TARG_BEOGH_GIFTABLE:
2648 return beogh_can_gift_items_to(mon);
2649 case TARG_MOVABLE_OBJECT:
2650 return false;
2651 case TARG_MOBILE_MONSTER:
2652 return !(mons_is_tentacle_or_tentacle_segment(mon->type)
2653 || mon->is_stationary());
2654 case TARG_NUM_MODES:
2655 break;
2656 // intentionally no default
2657 }
2658 die("Unknown targeting mode!");
2659 }
2660
_tobool(maybe_bool mb)2661 static bool _tobool(maybe_bool mb)
2662 {
2663 ASSERT(mb != MB_MAYBE);
2664 return mb == MB_TRUE;
2665 }
2666
_find_monster(const coord_def & where,targ_mode_type mode,bool need_path,int range,targeter * hitfunc)2667 static bool _find_monster(const coord_def& where, targ_mode_type mode,
2668 bool need_path, int range, targeter *hitfunc)
2669 {
2670 {
2671 coord_def dp = grid2player(where);
2672 // We could pass more info here.
2673 maybe_bool x = clua.callmbooleanfn("ch_target_monster", "dd",
2674 dp.x, dp.y);
2675 if (x != MB_MAYBE)
2676 return _tobool(x);
2677 }
2678
2679 // Target the player for friendly and general spells.
2680 if ((mode == TARG_FRIEND || mode == TARG_ANY) && where == you.pos())
2681 return true;
2682
2683 // Don't target out of range
2684 if (!_is_target_in_range(where, range, hitfunc))
2685 return false;
2686
2687 const monster* mon = monster_at(where);
2688
2689 // No monster or outside LOS.
2690 if (!mon || !cell_see_cell(you.pos(), where, LOS_DEFAULT))
2691 return false;
2692
2693 // Monster in LOS but only via glass walls, so no direct path.
2694 if (!you.see_cell_no_trans(where))
2695 return false;
2696
2697 if (!_mons_is_valid_target(mon, mode, range))
2698 return false;
2699
2700 if (need_path && _blocked_ray(mon->pos()))
2701 return false;
2702
2703 return _want_target_monster(mon, mode, hitfunc);
2704 }
2705
_find_shadow_step_mons(const coord_def & where,targ_mode_type mode,bool need_path,int range,targeter * hitfunc)2706 static bool _find_shadow_step_mons(const coord_def& where, targ_mode_type mode,
2707 bool need_path, int range,
2708 targeter *hitfunc)
2709 {
2710 {
2711 coord_def dp = grid2player(where);
2712 // We could pass more info here.
2713 maybe_bool x = clua.callmbooleanfn("ch_target_shadow_step", "dd",
2714 dp.x, dp.y);
2715 if (x != MB_MAYBE)
2716 return _tobool(x);
2717 }
2718
2719 // Need a monster to attack; this checks that the monster is a valid target.
2720 if (!_find_monster(where, mode, need_path, range, hitfunc))
2721 return false;
2722 // Can't step on yourself
2723 if (where == you.pos())
2724 return false;
2725
2726 targeter_shadow_step &tgt = *static_cast<targeter_shadow_step*>(hitfunc);
2727 return tgt.has_additional_sites(where);
2728 }
2729
_find_monster_expl(const coord_def & where,targ_mode_type mode,bool need_path,int range,targeter * hitfunc,aff_type mon_aff,aff_type allowed_self_aff)2730 static bool _find_monster_expl(const coord_def& where, targ_mode_type mode,
2731 bool need_path, int range, targeter *hitfunc,
2732 aff_type mon_aff, aff_type allowed_self_aff)
2733 {
2734 ASSERT(hitfunc);
2735
2736 {
2737 coord_def dp = grid2player(where);
2738 // We could pass more info here.
2739 maybe_bool x = clua.callmbooleanfn("ch_target_monster_expl", "dd",
2740 dp.x, dp.y);
2741 if (x != MB_MAYBE)
2742 return _tobool(x);
2743 }
2744
2745 if (!hitfunc->valid_aim(where))
2746 return false;
2747
2748 // Target is blocked by something
2749 if (need_path && _blocked_ray(where))
2750 return false;
2751
2752 if (hitfunc->set_aim(where))
2753 {
2754 if (hitfunc->is_affected(you.pos()) > allowed_self_aff)
2755 return false;
2756 for (monster_near_iterator mi(&you); mi; ++mi)
2757 {
2758 if (hitfunc->is_affected(mi->pos()) == mon_aff
2759 && _mons_is_valid_target(*mi, mode, range)
2760 && _want_target_monster(*mi, mode, hitfunc))
2761 {
2762 return true;
2763 }
2764 }
2765 }
2766 return false;
2767 }
2768
_item_at(const coord_def & where)2769 static const item_def* _item_at(const coord_def &where)
2770 {
2771 // XXX: are we ever interacting with unseen items, anyway?
2772 return you.see_cell(where)
2773 ? top_item_at(where)
2774 : env.map_knowledge(where).item();
2775 }
2776
_find_autopickup_object(const coord_def & where,bool need_path,int range,targeter * hitfunc)2777 static bool _find_autopickup_object(const coord_def& where, bool need_path,
2778 int range, targeter *hitfunc)
2779 {
2780 if (!_find_object(where, need_path, range, hitfunc))
2781 return false;
2782
2783 const item_def * const item = _item_at(where);
2784 ASSERT(item);
2785 return item_needs_autopickup(*item);
2786 }
2787
_find_object(const coord_def & where,bool need_path,int range,targeter * hitfunc)2788 static bool _find_object(const coord_def& where, bool need_path, int range,
2789 targeter *hitfunc)
2790 {
2791 // Don't target out of range.
2792 if (!_is_target_in_range(where, range, hitfunc))
2793 return false;
2794
2795 if (need_path && (!you.see_cell(where) || _blocked_ray(where)))
2796 return false;
2797
2798 const item_def * const item = _item_at(where);
2799 return item && !item_is_stationary(*item);
2800 }
2801
_next_los(int dir,int los,bool wrap)2802 static int _next_los(int dir, int los, bool wrap)
2803 {
2804 if (los == LS_ANY)
2805 return wrap? los : LS_NONE;
2806
2807 bool vis = los & LS_VISIBLE;
2808 bool hidden = los & LS_HIDDEN;
2809 bool flipvh = los & LS_FLIPVH;
2810 bool fliphv = los & LS_FLIPHV;
2811
2812 if (!vis && !hidden)
2813 vis = true;
2814
2815 if (wrap)
2816 {
2817 if (!flipvh && !fliphv)
2818 return los;
2819
2820 // We have to invert flipvh and fliphv if we're wrapping. Here's
2821 // why:
2822 //
2823 // * Say the cursor is on the last item in LOS, there are no
2824 // items outside LOS, and wrap == true. flipvh is true.
2825 // * We set wrap false and flip from visible to hidden, but there
2826 // are no hidden items. So now we need to flip back to visible
2827 // so we can go back to the first item in LOS. Unless we set
2828 // fliphv, we can't flip from hidden to visible.
2829 //
2830 los = flipvh ? LS_FLIPHV : LS_FLIPVH;
2831 }
2832 else
2833 {
2834 if (!flipvh && !fliphv)
2835 return LS_NONE;
2836
2837 if (flipvh && vis != (dir == 1))
2838 return LS_NONE;
2839
2840 if (fliphv && vis == (dir == 1))
2841 return LS_NONE;
2842 }
2843
2844 return (los & ~LS_VISMASK) | (vis ? LS_HIDDEN : LS_VISIBLE);
2845 }
2846
2847 //---------------------------------------------------------------
2848 //
2849 // find_square
2850 //
2851 // Finds the next monster/object/whatever (moving in a spiral
2852 // outwards from the player, so closer targets are chosen first;
2853 // starts to player's left) and puts its coordinates in mfp.
2854 // Returns 1 if it found something, zero otherwise. If direction
2855 // is -1, goes backwards.
2856 //
2857 //---------------------------------------------------------------
_find_square(coord_def & mfp,int direction,target_checker find_targ,targeter * hitfunc,bool wrap,int los)2858 static bool _find_square(coord_def &mfp, int direction,
2859 target_checker find_targ, targeter *hitfunc,
2860 bool wrap, int los)
2861 {
2862 int temp_xps = mfp.x;
2863 int temp_yps = mfp.y;
2864 int x_change = 0;
2865 int y_change = 0;
2866
2867 bool onlyVis = false, onlyHidden = false;
2868
2869 int i, j;
2870
2871 if (los == LS_NONE)
2872 return false;
2873
2874 if (los == LS_FLIPVH || los == LS_FLIPHV)
2875 {
2876 if (in_los_bounds_v(mfp))
2877 {
2878 // We've been told to flip between visible/hidden, so we
2879 // need to find what we're currently on.
2880 const bool vis = you.see_cell(view2grid(mfp));
2881
2882 if (wrap && (vis != (los == LS_FLIPVH)) == (direction == 1))
2883 {
2884 // We've already flipped over into the other direction,
2885 // so correct the flip direction if we're wrapping.
2886 los = (los == LS_FLIPHV ? LS_FLIPVH : LS_FLIPHV);
2887 }
2888
2889 los = (los & ~LS_VISMASK) | (vis ? LS_VISIBLE : LS_HIDDEN);
2890 }
2891 else
2892 {
2893 if (wrap)
2894 los = LS_HIDDEN | (direction > 0 ? LS_FLIPHV : LS_FLIPVH);
2895 else
2896 los |= LS_HIDDEN;
2897 }
2898 }
2899
2900 onlyVis = (los & LS_VISIBLE);
2901 onlyHidden = (los & LS_HIDDEN);
2902
2903 int radius = 0;
2904 if (crawl_view.viewsz.x > crawl_view.viewsz.y)
2905 radius = crawl_view.viewsz.x - LOS_RADIUS - 1;
2906 else
2907 radius = crawl_view.viewsz.y - LOS_RADIUS - 1;
2908
2909 const coord_def vyou = grid2view(you.pos());
2910
2911 const int minx = vyou.x - radius, maxx = vyou.x + radius,
2912 miny = vyou.y - radius, maxy = vyou.y + radius,
2913 ctrx = vyou.x, ctry = vyou.y;
2914
2915 while (temp_xps >= minx - 1 && temp_xps <= maxx
2916 && temp_yps <= maxy && temp_yps >= miny - 1)
2917 {
2918 if (direction == 1 && temp_xps == minx && temp_yps == maxy)
2919 {
2920 mfp = vyou;
2921 if (find_targ(you.pos()))
2922 return true;
2923 return _find_square(mfp, direction,
2924 find_targ, hitfunc,
2925 false, _next_los(direction, los, wrap));
2926 }
2927 if (direction == -1 && temp_xps == ctrx && temp_yps == ctry)
2928 {
2929 mfp = coord_def(minx, maxy);
2930 return _find_square(mfp, direction,
2931 find_targ, hitfunc,
2932 false, _next_los(direction, los, wrap));
2933 }
2934
2935 if (direction == 1)
2936 {
2937 if (temp_xps == minx - 1)
2938 {
2939 x_change = 0;
2940 y_change = -1;
2941 }
2942 else if (temp_xps == ctrx && temp_yps == ctry)
2943 {
2944 x_change = -1;
2945 y_change = 0;
2946 }
2947 else if (abs(temp_xps - ctrx) <= abs(temp_yps - ctry))
2948 {
2949 if (temp_xps - ctrx >= 0 && temp_yps - ctry <= 0)
2950 {
2951 if (abs(temp_xps - ctrx) > abs(temp_yps - ctry + 1))
2952 {
2953 x_change = 0;
2954 y_change = -1;
2955 if (temp_xps - ctrx > 0)
2956 y_change = 1;
2957 goto finished_spiralling;
2958 }
2959 }
2960 x_change = -1;
2961 if (temp_yps - ctry < 0)
2962 x_change = 1;
2963 y_change = 0;
2964 }
2965 else
2966 {
2967 x_change = 0;
2968 y_change = -1;
2969 if (temp_xps - ctrx > 0)
2970 y_change = 1;
2971 }
2972 } // end if (direction == 1)
2973 else
2974 {
2975 // This part checks all eight surrounding squares to find the
2976 // one that leads on to the present square.
2977 for (i = -1; i < 2; ++i)
2978 for (j = -1; j < 2; ++j)
2979 {
2980 if (i == 0 && j == 0)
2981 continue;
2982
2983 if (temp_xps + i == minx - 1)
2984 {
2985 x_change = 0;
2986 y_change = -1;
2987 }
2988 else if (temp_xps + i - ctrx == 0
2989 && temp_yps + j - ctry == 0)
2990 {
2991 x_change = -1;
2992 y_change = 0;
2993 }
2994 else if (abs(temp_xps + i - ctrx) <= abs(temp_yps + j - ctry))
2995 {
2996 const int xi = temp_xps + i - ctrx;
2997 const int yj = temp_yps + j - ctry;
2998
2999 if (xi >= 0 && yj <= 0
3000 && abs(xi) > abs(yj + 1))
3001 {
3002 x_change = 0;
3003 y_change = -1;
3004 if (xi > 0)
3005 y_change = 1;
3006 goto finished_spiralling;
3007 }
3008
3009 x_change = -1;
3010 if (yj < 0)
3011 x_change = 1;
3012 y_change = 0;
3013 }
3014 else
3015 {
3016 x_change = 0;
3017 y_change = -1;
3018 if (temp_xps + i - ctrx > 0)
3019 y_change = 1;
3020 }
3021
3022 if (temp_xps + i + x_change == temp_xps
3023 && temp_yps + j + y_change == temp_yps)
3024 {
3025 goto finished_spiralling;
3026 }
3027 }
3028 } // end else
3029
3030 finished_spiralling:
3031 x_change *= direction;
3032 y_change *= direction;
3033
3034 temp_xps += x_change;
3035 if (temp_yps + y_change <= maxy) // it can wrap, unfortunately
3036 temp_yps += y_change;
3037
3038 const int targ_x = you.pos().x + temp_xps - ctrx;
3039 const int targ_y = you.pos().y + temp_yps - ctry;
3040 const coord_def targ(targ_x, targ_y);
3041
3042 if (!crawl_view.in_viewport_g(targ))
3043 continue;
3044
3045 if (!map_bounds(targ))
3046 continue;
3047
3048 if ((onlyVis || onlyHidden) && onlyVis != you.see_cell(targ))
3049 continue;
3050
3051 if (find_targ(targ))
3052 {
3053 mfp.set(temp_xps, temp_yps);
3054 return true;
3055 }
3056 }
3057
3058 mfp = (direction > 0 ? coord_def(ctrx, ctry) : coord_def(minx, maxy));
3059 return _find_square(mfp, direction,
3060 find_targ, hitfunc,
3061 false, _next_los(direction, los, wrap));
3062 }
3063
3064 // XXX Unbelievably hacky. And to think that my goal was to clean up the code.
3065 // Identical to _find_square, except that mfp is in grid coordinates
3066 // rather than view coordinates.
_find_square_wrapper(coord_def & mfp,int direction,target_checker find_targ,targeter * hitfunc,LOSSelect los)3067 static bool _find_square_wrapper(coord_def &mfp, int direction,
3068 target_checker find_targ, targeter *hitfunc,
3069 LOSSelect los)
3070 {
3071 mfp = grid2view(mfp);
3072 const bool r = _find_square(mfp, direction, find_targ, hitfunc, true, los);
3073 mfp = view2grid(mfp);
3074 return r;
3075 }
3076
_describe_oos_feature(const coord_def & where)3077 static void _describe_oos_feature(const coord_def& where)
3078 {
3079 if (!env.map_knowledge(where).seen())
3080 return;
3081
3082 string desc = feature_description(env.map_knowledge(where).feat()) + ".";
3083
3084 if (!desc.empty())
3085 mprf(MSGCH_EXAMINE_FILTER, "[%s]", desc.c_str());
3086 }
3087
3088 // Returns a vector of features matching the given pattern.
features_by_desc(const base_pattern & pattern)3089 vector<dungeon_feature_type> features_by_desc(const base_pattern &pattern)
3090 {
3091 vector<dungeon_feature_type> features;
3092
3093 if (pattern.valid())
3094 {
3095 for (int i = 0; i < NUM_FEATURES; ++i)
3096 {
3097 string fdesc =
3098 feature_description(static_cast<dungeon_feature_type>(i)) + ".";
3099
3100 if (pattern.matches(fdesc))
3101 features.push_back(dungeon_feature_type(i));
3102 }
3103 }
3104 return features;
3105 }
3106
describe_floor()3107 void describe_floor()
3108 {
3109 dungeon_feature_type grid = env.map_knowledge(you.pos()).feat();
3110
3111 const char* prefix = "There is ";
3112 string feat;
3113
3114 switch (grid)
3115 {
3116 case DNGN_FLOOR:
3117 return;
3118
3119 case DNGN_ENTER_SHOP:
3120 prefix = "There is an entrance to ";
3121 break;
3122
3123 default:
3124 break;
3125 }
3126
3127 feat = feature_description_at(you.pos(), true, DESC_A);
3128 if (feat.empty())
3129 return;
3130
3131 msg_channel_type channel = MSGCH_EXAMINE;
3132
3133 // Messages for water/lava are too spammy use a status light instead.
3134 if (feat_is_water(grid) || feat_is_lava(grid))
3135 return;
3136
3137 mprf(channel, "%s%s here.", prefix, feat.c_str());
3138 if (grid == DNGN_ENTER_GAUNTLET)
3139 mprf(MSGCH_EXAMINE, "Beware, the minotaur awaits!");
3140 }
3141
_base_feature_desc(dungeon_feature_type grid,trap_type trap)3142 static string _base_feature_desc(dungeon_feature_type grid, trap_type trap)
3143 {
3144 if (feat_is_trap(grid) && trap != NUM_TRAPS)
3145 return full_trap_name(trap);
3146
3147 if (grid == DNGN_ROCK_WALL && player_in_branch(BRANCH_PANDEMONIUM))
3148 return "wall of the weird stuff which makes up Pandemonium";
3149 else if (!is_valid_feature_type(grid))
3150 return "";
3151 else
3152 return get_feature_def(grid).name;
3153
3154 }
3155
feature_description(dungeon_feature_type grid,trap_type trap,const string & cover_desc,description_level_type dtype)3156 string feature_description(dungeon_feature_type grid, trap_type trap,
3157 const string & cover_desc,
3158 description_level_type dtype)
3159 {
3160 string desc = _base_feature_desc(grid, trap);
3161 desc += cover_desc;
3162
3163 if (grid == DNGN_FLOOR && dtype == DESC_A)
3164 dtype = DESC_THE;
3165
3166 bool ignore_case = false;
3167 if (grid == DNGN_TRAP_ZOT)
3168 ignore_case = true;
3169
3170 return thing_do_grammar(dtype, desc, ignore_case);
3171 }
3172
raw_feature_description(const coord_def & where)3173 string raw_feature_description(const coord_def &where)
3174 {
3175 dungeon_feature_type feat = env.grid(where);
3176
3177 int mapi = env.level_map_ids(where);
3178 if (mapi != INVALID_MAP_INDEX)
3179 {
3180 const auto &renames = env.level_vaults[mapi]->map.feat_renames;
3181 if (const string *rename = map_find(renames, feat))
3182 return *rename;
3183 }
3184
3185 return _base_feature_desc(feat, get_trap_type(where));
3186 }
3187
3188 #ifndef DEBUG_DIAGNOSTICS
3189 // Is a feature interesting enough to 'v'iew it, even if a player normally
3190 // doesn't care about descriptions, i.e. does the description hold important
3191 // information? (Yes, this is entirely subjective. --jpeg)
_interesting_feature(dungeon_feature_type feat)3192 static bool _interesting_feature(dungeon_feature_type feat)
3193 {
3194 return get_feature_def(feat).flags & FFT_EXAMINE_HINT;
3195 }
3196 #endif
3197
feature_description_at(const coord_def & where,bool covering,description_level_type dtype)3198 string feature_description_at(const coord_def& where, bool covering,
3199 description_level_type dtype)
3200 {
3201 dungeon_feature_type grid = env.map_knowledge(where).feat();
3202 trap_type trap = env.map_knowledge(where).trap();
3203
3204 string marker_desc = env.markers.property_at(where, MAT_ANY,
3205 "feature_description");
3206
3207 string covering_description = "";
3208
3209 if (covering && you.see_cell(where))
3210 {
3211 if (feat_is_tree(grid) && env.forest_awoken_until)
3212 covering_description += ", awoken";
3213
3214 if (is_icecovered(where))
3215 covering_description = ", covered with ice";
3216
3217 if (is_temp_terrain(where))
3218 covering_description = ", summoned";
3219
3220 if (is_bloodcovered(where))
3221 covering_description += ", spattered with blood";
3222 }
3223
3224 // FIXME: remove desc markers completely; only Zin walls are left.
3225 // They suffer, among other problems, from an information leak.
3226 if (!marker_desc.empty())
3227 {
3228 marker_desc += covering_description;
3229
3230 return thing_do_grammar(dtype, marker_desc);
3231 }
3232
3233 if (feat_is_door(grid))
3234 {
3235 const string door_desc_prefix =
3236 env.markers.property_at(where, MAT_ANY,
3237 "door_description_prefix");
3238 const string door_desc_suffix =
3239 env.markers.property_at(where, MAT_ANY,
3240 "door_description_suffix");
3241 const string door_desc_noun =
3242 env.markers.property_at(where, MAT_ANY,
3243 "door_description_noun");
3244 const string door_desc_adj =
3245 env.markers.property_at(where, MAT_ANY,
3246 "door_description_adjective");
3247 const string door_desc_veto =
3248 env.markers.property_at(where, MAT_ANY,
3249 "door_description_veto");
3250
3251 set<coord_def> all_door;
3252 find_connected_identical(where, all_door);
3253 const char *adj, *noun;
3254 get_door_description(all_door.size(), &adj, &noun);
3255
3256 string desc;
3257 if (!door_desc_adj.empty())
3258 desc += door_desc_adj;
3259 else
3260 desc += adj;
3261
3262 if (door_desc_veto.empty() || door_desc_veto != "veto")
3263 {
3264 if (grid == DNGN_OPEN_DOOR)
3265 desc += "open ";
3266 else if (grid == DNGN_CLOSED_CLEAR_DOOR)
3267 desc += "closed translucent ";
3268 else if (grid == DNGN_OPEN_CLEAR_DOOR)
3269 desc += "open translucent ";
3270 else if (grid == DNGN_RUNED_DOOR)
3271 desc += "runed ";
3272 else if (grid == DNGN_RUNED_CLEAR_DOOR)
3273 desc += "runed translucent ";
3274 else if (grid == DNGN_SEALED_DOOR)
3275 desc += "sealed ";
3276 else if (grid == DNGN_SEALED_CLEAR_DOOR)
3277 desc += "sealed translucent ";
3278 else
3279 desc += "closed ";
3280 }
3281
3282 desc += door_desc_prefix;
3283
3284 if (!door_desc_noun.empty())
3285 desc += door_desc_noun;
3286 else
3287 desc += noun;
3288
3289 desc += door_desc_suffix;
3290
3291 desc += covering_description;
3292
3293 return thing_do_grammar(dtype, desc);
3294 }
3295
3296 bool ignore_case = false;
3297 if (grid == DNGN_TRAP_ZOT)
3298 ignore_case = true;
3299
3300 switch (grid)
3301 {
3302 #if TAG_MAJOR_VERSION == 34
3303 case DNGN_TRAP_MECHANICAL:
3304 return feature_description(grid, trap, covering_description, dtype);
3305
3306 case DNGN_ENTER_PORTAL_VAULT:
3307 // Should have been handled at the top of the function.
3308 return thing_do_grammar(dtype, "UNAMED PORTAL VAULT ENTRY");
3309 #endif
3310 case DNGN_ENTER_SHOP:
3311 return shop_name(*shop_at(where));
3312
3313 case DNGN_FLOOR:
3314 if (dtype == DESC_A)
3315 dtype = DESC_THE;
3316 // fallthrough
3317 default:
3318 const string featdesc = grid == env.grid(where)
3319 ? raw_feature_description(where)
3320 : _base_feature_desc(grid, trap);
3321 return thing_do_grammar(dtype, featdesc + covering_description,
3322 ignore_case);
3323 }
3324 }
3325
_describe_monster_weapon(const monster_info & mi,bool ident)3326 static string _describe_monster_weapon(const monster_info& mi, bool ident)
3327 {
3328 string desc = "";
3329 string name1, name2;
3330 const item_def *weap = mi.inv[MSLOT_WEAPON].get();
3331 const item_def *alt = mi.inv[MSLOT_ALT_WEAPON].get();
3332
3333 if (weap && (!ident || item_type_known(*weap)))
3334 name1 = weap->name(DESC_A, false, false, true, false);
3335 if (alt && (!ident || item_type_known(*alt)) && mi.wields_two_weapons())
3336 name2 = alt->name(DESC_A, false, false, true, false);
3337
3338 if (name1.empty() && !name2.empty())
3339 name1.swap(name2);
3340
3341 if (name1 == name2 && weap && !name1.empty())
3342 {
3343 item_def dup = *weap;
3344 ++dup.quantity;
3345 name1 = dup.name(DESC_A, false, false, true, true);
3346 name2.clear();
3347 }
3348
3349 if (mi.props.exists(SPECIAL_WEAPON_KEY))
3350 {
3351 name1 = article_a(ghost_brand_name(
3352 (brand_type) mi.props[SPECIAL_WEAPON_KEY].get_int(), mi.type));
3353 }
3354
3355 if (name1.empty())
3356 return desc;
3357
3358 if (mi.type == MONS_PANDEMONIUM_LORD)
3359 desc += " armed with ";
3360 else if (mons_class_is_animated_weapon(mi.type))
3361 desc += " ";
3362 else
3363 desc += " wielding ";
3364 desc += name1;
3365
3366 if (!name2.empty())
3367 {
3368 desc += " and ";
3369 desc += name2;
3370 }
3371
3372 return desc;
3373 }
3374
3375 #ifdef DEBUG_DIAGNOSTICS
_stair_destination_description(const coord_def & pos)3376 static string _stair_destination_description(const coord_def &pos)
3377 {
3378 if (LevelInfo *linf = travel_cache.find_level_info(level_id::current()))
3379 {
3380 const stair_info *si = linf->get_stair(pos);
3381 if (si)
3382 return " " + si->describe();
3383 else if (feat_is_stair(env.grid(pos)))
3384 return " (unknown stair)";
3385 }
3386 return "";
3387 }
3388 #endif
3389
_mon_enchantments_string(const monster_info & mi)3390 static string _mon_enchantments_string(const monster_info& mi)
3391 {
3392 const vector<string> enchant_descriptors = mi.attributes();
3393
3394 if (!enchant_descriptors.empty())
3395 {
3396 return uppercase_first(mi.pronoun(PRONOUN_SUBJECTIVE))
3397 + " "
3398 + conjugate_verb("are", mi.pronoun_plurality())
3399 + " "
3400 + comma_separated_line(enchant_descriptors.begin(),
3401 enchant_descriptors.end())
3402 + ".";
3403 }
3404 else
3405 return "";
3406 }
3407
_get_monster_behaviour_vector(const monster_info & mi)3408 static vector<string> _get_monster_behaviour_vector(const monster_info& mi)
3409 {
3410 vector<string> descs;
3411
3412 if ((mi.is(MB_SLEEPING) || mi.is(MB_DORMANT)))
3413 {
3414 if (mi.is(MB_CONFUSED))
3415 descs.emplace_back("sleepwalking");
3416 else if (mons_class_flag(mi.type, M_CONFUSED))
3417 descs.emplace_back("drifting");
3418 }
3419 else if (mi.attitude == ATT_HOSTILE && (mi.is(MB_UNAWARE) || mi.is(MB_WANDERING)))
3420 descs.emplace_back("hasn't noticed you");
3421
3422 return descs;
3423 }
3424
3425 // FIXME: this duplicates _get_monster_desc(). Unite them.
_get_monster_desc_vector(const monster_info & mi)3426 static vector<string> _get_monster_desc_vector(const monster_info& mi)
3427 {
3428 vector<string> descs;
3429
3430 _append_container(descs, _get_monster_behaviour_vector(mi));
3431
3432 if (you.duration[DUR_CONFUSING_TOUCH])
3433 {
3434 const int pow = you.props["confusing touch power"].get_int();
3435 descs.emplace_back(make_stringf("chance to confuse on hit: %d%%",
3436 hex_success_chance(mi.willpower(),
3437 pow, 100)));
3438 }
3439 else if (you.form == transformation::fungus
3440 && !mons_is_unbreathing(mi.type))
3441 {
3442 descs.emplace_back(make_stringf("chance to confuse on hit: %d%%",
3443 melee_confuse_chance(mi.hd)));
3444 }
3445
3446 if (mi.attitude == ATT_FRIENDLY)
3447 descs.emplace_back("friendly");
3448 else if (mi.attitude == ATT_GOOD_NEUTRAL)
3449 descs.emplace_back("peaceful");
3450 else if (mi.attitude != ATT_HOSTILE && !mi.is(MB_INSANE))
3451 {
3452 // don't differentiate between permanent or not
3453 descs.emplace_back("indifferent");
3454 }
3455
3456 if (mi.is(MB_HALOED))
3457 descs.emplace_back("haloed");
3458
3459 if (mi.is(MB_UMBRAED))
3460 descs.emplace_back("umbra");
3461
3462 if (mi.fire_blocker)
3463 {
3464 descs.push_back("fire blocked by " // FIXME, renamed features
3465 + feature_description(mi.fire_blocker, NUM_TRAPS, "",
3466 DESC_A));
3467 }
3468
3469 return descs;
3470 }
3471
3472 // Returns the description string for a given monster, including attitude
3473 // and enchantments but not equipment or wounds.
_get_monster_desc(const monster_info & mi)3474 static string _get_monster_desc(const monster_info& mi)
3475 {
3476 string text = "";
3477 string pronoun = uppercase_first(mi.pronoun(PRONOUN_SUBJECTIVE));
3478
3479 if (mi.is(MB_MESMERIZING))
3480 {
3481 text += string("You are mesmerised by ")
3482 + mi.pronoun(PRONOUN_POSSESSIVE) + " song.\n";
3483 }
3484
3485 if (mi.is(MB_SLEEPING) || mi.is(MB_DORMANT))
3486 {
3487 text += pronoun + " "
3488 + conjugate_verb("appear", mi.pronoun_plurality()) + " to be "
3489 + (mi.is(MB_CONFUSED) ? "sleepwalking" : "resting") + ".\n";
3490 }
3491 // Applies to both friendlies and hostiles
3492 else if (mi.is(MB_FLEEING))
3493 {
3494 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3495 + " fleeing.\n";
3496 }
3497 // hostile with target != you
3498 else if (mi.attitude == ATT_HOSTILE
3499 && (mi.is(MB_UNAWARE) || mi.is(MB_WANDERING)))
3500 {
3501 text += pronoun + " " + conjugate_verb("have", mi.pronoun_plurality())
3502 + " not noticed you.\n";
3503 }
3504
3505 if (mi.attitude == ATT_FRIENDLY)
3506 {
3507 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3508 + " friendly.\n";
3509 }
3510 else if (mi.attitude == ATT_GOOD_NEUTRAL)
3511 {
3512 text += pronoun + " " + conjugate_verb("seem", mi.pronoun_plurality())
3513 + " to be peaceful towards you.\n";
3514 }
3515 else if (mi.attitude != ATT_HOSTILE && !mi.is(MB_INSANE))
3516 {
3517 // don't differentiate between permanent or not
3518 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3519 + " indifferent to you.\n";
3520 }
3521
3522 if (mi.is(MB_SUMMONED) || mi.is(MB_PERM_SUMMON))
3523 {
3524 text += pronoun + " " + conjugate_verb("have", mi.pronoun_plurality())
3525 + " been summoned";
3526 if (mi.is(MB_SUMMONED_CAPPED))
3527 {
3528 text += ", and " + conjugate_verb("are", mi.pronoun_plurality())
3529 + " expiring";
3530 }
3531 else if (mi.is(MB_PERM_SUMMON))
3532 text += " but will not time out";
3533 text += ".\n";
3534 }
3535
3536 if (mi.is(MB_HALOED))
3537 {
3538 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3539 + " illuminated by a divine halo.\n";
3540 }
3541
3542 if (mi.is(MB_UMBRAED))
3543 {
3544 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3545 + " wreathed by an umbra.\n";
3546 }
3547
3548 if (mi.intel() <= I_BRAINLESS)
3549 {
3550 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3551 + " mindless.\n";
3552 }
3553
3554 if (mi.is(MB_CHAOTIC))
3555 {
3556 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3557 + " chaotic.\n";
3558 }
3559
3560 if (mi.is(MB_POSSESSABLE))
3561 {
3562 text += string(mi.pronoun(PRONOUN_POSSESSIVE))
3563 + " soul is ripe for the taking.\n";
3564 }
3565 else if (mi.is(MB_ENSLAVED))
3566 {
3567 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3568 + " a disembodied soul.\n";
3569 }
3570
3571 if (mi.is(MB_MIRROR_DAMAGE))
3572 {
3573 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3574 + " reflecting injuries back at attackers.\n";
3575 }
3576
3577 if (mi.is(MB_INNER_FLAME))
3578 {
3579 text += pronoun + " " + conjugate_verb("are", mi.pronoun_plurality())
3580 + " filled with an inner flame.\n";
3581 }
3582
3583 if (mi.fire_blocker)
3584 {
3585 text += string("Your line of fire to ") + mi.pronoun(PRONOUN_OBJECTIVE)
3586 + " is blocked by " // FIXME: renamed features
3587 + feature_description(mi.fire_blocker, NUM_TRAPS, "",
3588 DESC_A)
3589 + ".\n";
3590 }
3591
3592 text += _mon_enchantments_string(mi);
3593 if (!text.empty() && text.back() == '\n')
3594 text.pop_back();
3595 return text;
3596 }
3597
_describe_monster(const monster_info & mi)3598 static void _describe_monster(const monster_info& mi)
3599 {
3600 // First print type and equipment.
3601 string text = uppercase_first(get_monster_equipment_desc(mi)) + ".";
3602 const string wounds_desc = mi.wounds_description_sentence();
3603 if (!wounds_desc.empty())
3604 text += " " + uppercase_first(wounds_desc);
3605 const string constriction_desc = mi.constriction_description();
3606 if (!constriction_desc.empty())
3607 text += " It is" + constriction_desc + ".";
3608 mprf(MSGCH_EXAMINE, "%s", text.c_str());
3609
3610 // Print the rest of the description.
3611 text = _get_monster_desc(mi);
3612 if (!text.empty())
3613 mprf(MSGCH_EXAMINE, "%s", text.c_str());
3614 }
3615
3616 // This method is called in two cases:
3617 // a) Monsters coming into view: "An ogre comes into view. It is wielding ..."
3618 // b) Monster description via 'x': "An ogre, wielding a club, and wearing ..."
get_monster_equipment_desc(const monster_info & mi,mons_equip_desc_level_type level,description_level_type mondtype,bool print_attitude)3619 string get_monster_equipment_desc(const monster_info& mi,
3620 mons_equip_desc_level_type level,
3621 description_level_type mondtype,
3622 bool print_attitude)
3623 {
3624 string desc = "";
3625 if (mondtype != DESC_NONE)
3626 {
3627 if (print_attitude && mons_is_pghost(mi.type))
3628 desc = get_ghost_description(mi);
3629 else
3630 desc = mi.full_name(mondtype);
3631
3632 if (print_attitude)
3633 {
3634 vector<string> attributes;
3635 if (mi.attitude == ATT_FRIENDLY)
3636 attributes.emplace_back("friendly");
3637 else if (mi.attitude == ATT_GOOD_NEUTRAL)
3638 attributes.emplace_back("peaceful");
3639 else if (mi.attitude != ATT_HOSTILE && !mi.is(MB_INSANE))
3640 attributes.emplace_back("neutral");
3641 _append_container(attributes, mi.attributes());
3642
3643 string str = comma_separated_line(attributes.begin(),
3644 attributes.end());
3645
3646 if (mons_class_is_animated_weapon(mi.type)
3647 || mi.type == MONS_PANDEMONIUM_LORD
3648 || mi.type == MONS_PLAYER_GHOST)
3649 {
3650 if (!str.empty())
3651 str += " ";
3652
3653 // animated armour is has "animated" in its name already,
3654 // spectral weapons have "spectral".
3655 if (mi.type == MONS_DANCING_WEAPON)
3656 str += "dancing weapon";
3657 else if (mi.type == MONS_PANDEMONIUM_LORD)
3658 str += "pandemonium lord";
3659 else if (mi.type == MONS_PLAYER_GHOST)
3660 str += "ghost";
3661 }
3662 if (!str.empty())
3663 desc += " (" + str + ")";
3664 }
3665 }
3666
3667 string weap = _describe_monster_weapon(mi, level == DESC_IDENTIFIED);
3668
3669 // Print the rest of the equipment only for full descriptions.
3670 if (level == DESC_WEAPON || level == DESC_WEAPON_WARNING)
3671 return desc + weap;
3672
3673 item_def* mon_arm = mi.inv[MSLOT_ARMOUR].get();
3674 item_def* mon_shd = mi.inv[MSLOT_SHIELD].get();
3675 item_def* mon_qvr = mi.inv[MSLOT_MISSILE].get();
3676 item_def* mon_alt = mi.inv[MSLOT_ALT_WEAPON].get();
3677 item_def* mon_wnd = mi.inv[MSLOT_WAND].get();
3678 item_def* mon_rng = mi.inv[MSLOT_JEWELLERY].get();
3679
3680 #define uninteresting(x) (x && !item_is_branded(*x) && !is_artefact(*x))
3681 // For "comes into view" msgs, only care about branded stuff and artefacts
3682 if (level == DESC_IDENTIFIED)
3683 {
3684 if (uninteresting(mon_arm))
3685 mon_arm = nullptr;
3686 if (uninteresting(mon_shd))
3687 mon_shd = nullptr;
3688 if (uninteresting(mon_qvr))
3689 mon_qvr = nullptr;
3690 if (uninteresting(mon_rng))
3691 mon_rng = nullptr;
3692 if (uninteresting(mon_alt) && mon_alt->base_type != OBJ_WANDS)
3693 mon_alt = nullptr;
3694 }
3695 #undef uninteresting
3696
3697 // _describe_monster_weapon already took care of this
3698 if (mi.wields_two_weapons())
3699 mon_alt = 0;
3700
3701 const bool mon_has_wand = mon_wnd;
3702 const bool mon_carry = mon_alt || mon_has_wand;
3703
3704 vector<string> item_descriptions;
3705
3706 // Dancing weapons have all their weapon information in their full_name, so
3707 // we don't need to add another weapon description here (see Mantis 11887).
3708 if (!weap.empty() && !mons_class_is_animated_weapon(mi.type))
3709 item_descriptions.push_back(weap.substr(1)); // strip leading space
3710
3711 // as with dancing weapons, don't claim animated armours 'wear' their armour
3712 if (mon_arm && mi.type != MONS_ANIMATED_ARMOUR)
3713 {
3714 const string armour_desc = make_stringf("wearing %s",
3715 mon_arm->name(DESC_A).c_str());
3716 item_descriptions.push_back(armour_desc);
3717 }
3718
3719 if (mon_shd)
3720 {
3721 const string shield_desc = make_stringf("wearing %s",
3722 mon_shd->name(DESC_A).c_str());
3723 item_descriptions.push_back(shield_desc);
3724 }
3725
3726 if (mon_rng)
3727 {
3728 const string rng_desc = make_stringf("wearing %s",
3729 mon_rng->name(DESC_A).c_str());
3730 item_descriptions.push_back(rng_desc);
3731 }
3732
3733 if (mon_qvr)
3734 {
3735 const bool net = mon_qvr->sub_type == MI_THROWING_NET;
3736 const string qvr_desc = net ? mon_qvr->name(DESC_A)
3737 : pluralise(mon_qvr->name(DESC_PLAIN));
3738 item_descriptions.push_back(make_stringf("quivering %s", qvr_desc.c_str()));
3739 }
3740
3741 if (mon_carry)
3742 {
3743 string carried_desc = "carrying ";
3744
3745 if (mon_alt)
3746 {
3747 carried_desc += mon_alt->name(DESC_A);
3748 if (mon_has_wand)
3749 carried_desc += " and ";
3750 }
3751
3752 if (mon_has_wand)
3753 carried_desc += mon_wnd->name(DESC_A);
3754
3755 item_descriptions.push_back(carried_desc);
3756 }
3757
3758 const string item_description = comma_separated_line(
3759 item_descriptions.begin(),
3760 item_descriptions.end());
3761
3762 if (!item_description.empty() && !desc.empty())
3763 desc += ", ";
3764 return desc + item_description;
3765 }
3766
_print_cloud_desc(const coord_def where)3767 static bool _print_cloud_desc(const coord_def where)
3768 {
3769 vector<string> areas;
3770 if (is_sanctuary(where))
3771 areas.emplace_back("lies inside a sanctuary");
3772 if (silenced(where))
3773 areas.emplace_back("is shrouded in silence");
3774 if (haloed(where) && !umbraed(where))
3775 areas.emplace_back("is lit by a halo");
3776 if (umbraed(where) && !haloed(where))
3777 areas.emplace_back("is wreathed by an umbra");
3778 if (liquefied(where))
3779 areas.emplace_back("is liquefied");
3780 if (orb_haloed(where) || quad_haloed(where))
3781 areas.emplace_back("is covered in magical glow");
3782 if (disjunction_haloed(where))
3783 areas.emplace_back("is bathed in translocational energy");
3784 if (!areas.empty())
3785 {
3786 mprf("This square %s.",
3787 comma_separated_line(areas.begin(), areas.end()).c_str());
3788 }
3789
3790 if (cloud_struct* cloud = cloud_at(where))
3791 {
3792 mprf(MSGCH_EXAMINE, "There is a cloud of %s here.",
3793 cloud->cloud_name(true).c_str());
3794 return true;
3795 }
3796
3797 return false;
3798 }
3799
_print_item_desc(const coord_def where)3800 static bool _print_item_desc(const coord_def where)
3801 {
3802 int targ_item = you.visible_igrd(where);
3803
3804 if (targ_item == NON_ITEM)
3805 return false;
3806
3807 string name = menu_colour_item_name(env.item[targ_item], DESC_A);
3808 mprf(MSGCH_FLOOR_ITEMS, "You see %s here.", name.c_str());
3809
3810 if (env.item[ targ_item ].link != NON_ITEM)
3811 mprf(MSGCH_FLOOR_ITEMS, "There is something else lying underneath.");
3812
3813 return true;
3814 }
3815
3816 #ifdef DEBUG_DIAGNOSTICS
_debug_describe_feature_at(const coord_def & where)3817 static void _debug_describe_feature_at(const coord_def &where)
3818 {
3819 const string feature_desc = feature_description_at(where, true);
3820 string marker;
3821 if (map_marker *mark = env.markers.find(where, MAT_ANY))
3822 {
3823 string desc = mark->debug_describe();
3824 if (desc.empty())
3825 desc = "?";
3826 marker = " (" + desc + ")";
3827 }
3828 const string traveldest = _stair_destination_description(where);
3829 string height_desc;
3830 if (env.heightmap)
3831 height_desc = make_stringf(" (height: %d)", (*env.heightmap)(where));
3832 const dungeon_feature_type feat = env.grid(where);
3833
3834 string vault;
3835 const int map_index = env.level_map_ids(where);
3836 if (map_index != INVALID_MAP_INDEX)
3837 {
3838 const vault_placement &vp(*env.level_vaults[map_index]);
3839 const coord_def br = vp.pos + vp.size - 1;
3840 vault = make_stringf(" [Vault: %s (%d,%d)-(%d,%d) (%dx%d)]",
3841 vp.map_name_at(where).c_str(),
3842 vp.pos.x, vp.pos.y,
3843 br.x, br.y,
3844 vp.size.x, vp.size.y);
3845 }
3846
3847 char32_t ch = get_cell_glyph(where).ch;
3848 dprf("(%d,%d): %s - %s. (%d/%s)%s%s%s%s map: %x",
3849 where.x, where.y,
3850 ch == '<' ? "<<" : stringize_glyph(ch).c_str(),
3851 feature_desc.c_str(),
3852 feat,
3853 dungeon_feature_name(feat),
3854 marker.c_str(),
3855 traveldest.c_str(),
3856 height_desc.c_str(),
3857 vault.c_str(),
3858 env.map_knowledge(where).flags);
3859 }
3860 #endif
3861
3862 // Describe a cell, guaranteed to be in view.
_describe_cell(const coord_def & where,bool in_range)3863 static void _describe_cell(const coord_def& where, bool in_range)
3864 {
3865 #ifndef DEBUG_DIAGNOSTICS
3866 bool monster_described = false;
3867 #endif
3868
3869 if (where == you.pos() && !crawl_state.arena_suspended)
3870 mprf(MSGCH_EXAMINE_FILTER, "You.");
3871
3872 if (const monster* mon = monster_at(where))
3873 {
3874 #ifdef DEBUG_DIAGNOSTICS
3875 if (!mon->visible_to(&you))
3876 {
3877 mprf(MSGCH_DIAGNOSTICS, "There is a non-visible %smonster here.",
3878 _mon_exposed_in_water(mon) ? "exposed by water " :
3879 _mon_exposed_in_cloud(mon) ? "exposed by cloud " : "");
3880 }
3881 #else
3882 if (!mon->visible_to(&you))
3883 {
3884 if (_mon_exposed_in_water(mon))
3885 mprf(MSGCH_EXAMINE_FILTER, "There is a strange disturbance in the water here.");
3886 else if (_mon_exposed_in_cloud(mon))
3887 mprf(MSGCH_EXAMINE_FILTER, "There is a strange disturbance in the cloud here.");
3888
3889 goto look_clouds;
3890 }
3891 #endif
3892
3893 monster_info mi(mon);
3894 _describe_monster(mi);
3895
3896 if (!in_range)
3897 {
3898 mprf(MSGCH_EXAMINE_FILTER, "%s %s out of range.",
3899 mon->pronoun(PRONOUN_SUBJECTIVE).c_str(),
3900 conjugate_verb("are", mi.pronoun_plurality()).c_str());
3901 }
3902 #ifndef DEBUG_DIAGNOSTICS
3903 monster_described = true;
3904 #endif
3905
3906 #if defined(DEBUG_DIAGNOSTICS) && defined(WIZARD)
3907 debug_stethoscope(env.mgrid(where));
3908 #endif
3909 if (crawl_state.game_is_hints() && hints_monster_interesting(mon))
3910 {
3911 const char *msg;
3912 #ifdef USE_TILE_LOCAL
3913 msg = "(<w>Right-click</w> for more information.)";
3914 #else
3915 msg = "(Press <w>v</w> for more information.)";
3916 #endif
3917 mpr(msg);
3918 }
3919 }
3920
3921 #ifdef DEBUG_DIAGNOSTICS
3922 _print_cloud_desc(where);
3923 _print_item_desc(where);
3924 _debug_describe_feature_at(where);
3925 #else
3926 // removing warning
3927 look_clouds:
3928
3929 bool cloud_described = _print_cloud_desc(where);
3930 bool item_described = _print_item_desc(where);
3931
3932 string feature_desc = feature_description_at(where, true);
3933 const bool bloody = is_bloodcovered(where);
3934 if (crawl_state.game_is_hints() && hints_pos_interesting(where.x, where.y))
3935 {
3936 #ifdef USE_TILE_LOCAL
3937 feature_desc += " (<w>Right-click</w> for more information.)";
3938 #else
3939 feature_desc += " (Press <w>v</w> for more information.)";
3940 #endif
3941 mpr(feature_desc);
3942 }
3943 else
3944 {
3945 dungeon_feature_type feat = env.grid(where);
3946
3947 if (_interesting_feature(feat))
3948 {
3949 #ifdef USE_TILE_LOCAL
3950 feature_desc += " (Right-click for more information.)";
3951 #else
3952 feature_desc += " (Press 'v' for more information.)";
3953 #endif
3954 }
3955
3956 // Suppress "Floor." if there's something on that square that we've
3957 // already described.
3958 if (feat == DNGN_FLOOR && !bloody
3959 && (monster_described || item_described || cloud_described))
3960 {
3961 return;
3962 }
3963
3964 msg_channel_type channel = MSGCH_EXAMINE;
3965 if (feat == DNGN_FLOOR || feat_is_water(feat))
3966 channel = MSGCH_EXAMINE_FILTER;
3967
3968 mprf(channel, "%s", feature_desc.c_str());
3969 }
3970 #endif
3971 }
3972
3973 ///////////////////////////////////////////////////////////////////////////
3974 // targeting_behaviour
3975
targeting_behaviour(bool look_around)3976 targeting_behaviour::targeting_behaviour(bool look_around)
3977 : just_looking(look_around), needs_path(MB_MAYBE)
3978 {
3979 }
3980
~targeting_behaviour()3981 targeting_behaviour::~targeting_behaviour()
3982 {
3983 }
3984
get_command(int key)3985 command_type targeting_behaviour::get_command(int key)
3986 {
3987 command_type cmd = key_to_command(key, KMC_TARGETING);
3988 if (cmd >= CMD_MIN_TARGET && cmd < CMD_TARGET_PREV_TARGET)
3989 return cmd;
3990
3991 // XXX: hack
3992 if (cmd == CMD_TARGET_SELECT && key == ' ' && just_looking)
3993 cmd = CMD_TARGET_CANCEL;
3994
3995 return cmd;
3996 }
3997
get_monster_desc(const monster_info & mi)3998 vector<string> targeting_behaviour::get_monster_desc(const monster_info& mi)
3999 {
4000 vector<string> descs;
4001 if (get_desc_func)
4002 _append_container(descs, (get_desc_func)(mi));
4003 return descs;
4004 }
4005