1 /**
2 * @file
3 * @brief Misc function used to render the dungeon.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "view.h"
9
10 #include <algorithm>
11 #include <cmath>
12 #include <cstring>
13 #include <memory>
14 #include <sstream>
15
16 #include "act-iter.h"
17 #include "artefact.h"
18 #include "cio.h"
19 #include "cloud.h"
20 #include "clua.h"
21 #include "colour.h"
22 #include "coord.h"
23 #include "coordit.h"
24 #include "database.h"
25 #include "delay.h"
26 #include "dgn-overview.h"
27 #include "directn.h"
28 #include "english.h"
29 #include "env.h"
30 #include "tile-env.h"
31 #include "exclude.h"
32 #include "feature.h"
33 #include "files.h"
34 #include "fprop.h"
35 #include "god-conduct.h"
36 #include "god-passive.h"
37 #include "god-wrath.h"
38 #include "hints.h"
39 #include "items.h"
40 #include "item-name.h" // item_type_known
41 #include "item-prop.h" // get_weapon_brand
42 #include "libutil.h"
43 #include "macro.h"
44 #include "map-knowledge.h"
45 #include "message.h"
46 #include "misc.h"
47 #include "mon-abil.h" // boris_covet_orb
48 #include "mon-behv.h"
49 #include "mon-death.h"
50 #include "mon-poly.h"
51 #include "mon-tentacle.h"
52 #include "mon-util.h"
53 #include "nearby-danger.h"
54 #include "notes.h"
55 #include "options.h"
56 #include "output.h"
57 #include "player.h"
58 #include "random.h"
59 #include "religion.h"
60 #include "shout.h"
61 #include "show.h"
62 #include "showsymb.h"
63 #include "state.h"
64 #include "stringutil.h"
65 #include "tag-version.h"
66 #include "target.h"
67 #include "terrain.h"
68 #include "tilemcache.h"
69 #ifdef USE_TILE
70 #include "tile-flags.h"
71 #include "tilepick.h"
72 #include "tilepick-p.h"
73 #include "tileview.h"
74 #endif
75 #include "tiles-build-specific.h"
76 #include "traps.h"
77 #include "travel.h"
78 #include "unicode.h"
79 #include "unwind.h"
80 #include "viewchar.h"
81 #include "viewmap.h"
82 #include "xom.h"
83
84 static layers_type _layers = LAYERS_ALL;
85 static layers_type _layers_saved = LAYERS_NONE;
86
87 crawl_view_geometry crawl_view;
88
handle_seen_interrupt(monster * mons,vector<string> * msgs_buf)89 bool handle_seen_interrupt(monster* mons, vector<string>* msgs_buf)
90 {
91 activity_interrupt_data aid(mons);
92 if (mons->seen_context)
93 aid.context = mons->seen_context;
94 // XXX: Hack to make the 'seen' monster spec flag work.
95 else if (testbits(mons->flags, MF_WAS_IN_VIEW)
96 || testbits(mons->flags, MF_SEEN))
97 {
98 aid.context = SC_ALREADY_SEEN;
99 }
100 else
101 aid.context = SC_NEWLY_SEEN;
102
103 if (!mons_is_safe(mons))
104 {
105 return interrupt_activity(activity_interrupt::see_monster,
106 aid, msgs_buf);
107 }
108
109 return false;
110 }
111
flush_comes_into_view()112 void flush_comes_into_view()
113 {
114 if (!you.turn_is_over
115 || (!you_are_delayed() && !crawl_state.is_repeating_cmd()))
116 {
117 return;
118 }
119
120 monster* mon = crawl_state.which_mon_acting();
121
122 if (!mon || !mon->alive() || (mon->flags & MF_WAS_IN_VIEW)
123 || !you.can_see(*mon))
124 {
125 return;
126 }
127
128 handle_seen_interrupt(mon);
129 }
130
seen_monsters_react(int stealth)131 void seen_monsters_react(int stealth)
132 {
133 if (you.duration[DUR_TIME_STEP] || crawl_state.game_is_arena())
134 return;
135
136 for (monster_near_iterator mi(you.pos()); mi; ++mi)
137 {
138 if ((mi->asleep() || mons_is_wandering(**mi))
139 && check_awaken(*mi, stealth))
140 {
141 behaviour_event(*mi, ME_ALERT, &you, you.pos(), false);
142
143 // That might have caused a pacified monster to leave the level.
144 if (!(*mi)->alive())
145 continue;
146
147 monster_consider_shouting(**mi);
148 }
149
150 if (!mi->visible_to(&you))
151 continue;
152
153 if (!mi->has_ench(ENCH_INSANE) && mi->can_see(you))
154 {
155 // Trigger Duvessa & Dowan upgrades
156 if (mi->props.exists(ELVEN_ENERGIZE_KEY))
157 {
158 mi->props.erase(ELVEN_ENERGIZE_KEY);
159 elven_twin_energize(*mi);
160 }
161 else if (mi->type == MONS_BORIS && player_has_orb()
162 && !mi->props.exists(BORIS_ORB_KEY))
163 {
164 mi->props[BORIS_ORB_KEY] = true;
165 boris_covet_orb(*mi);
166 }
167 #if TAG_MAJOR_VERSION == 34
168 else if (mi->props.exists(OLD_DUVESSA_ENERGIZE_KEY))
169 {
170 mi->props.erase(OLD_DUVESSA_ENERGIZE_KEY);
171 elven_twin_energize(*mi);
172 }
173 else if (mi->props.exists(OLD_DOWAN_ENERGIZE_KEY))
174 {
175 mi->props.erase(OLD_DOWAN_ENERGIZE_KEY);
176 elven_twin_energize(*mi);
177 }
178 #endif
179 }
180 }
181 }
182
_desc_mons_type_map(map<monster_type,int> types)183 static string _desc_mons_type_map(map<monster_type, int> types)
184 {
185 string message;
186 unsigned int count = 1;
187 for (const auto &entry : types)
188 {
189 string name;
190 description_level_type desc;
191 if (entry.second == 1)
192 desc = DESC_A;
193 else
194 desc = DESC_PLAIN;
195
196 name = mons_type_name(entry.first, desc);
197 if (entry.second > 1)
198 {
199 name = make_stringf("%d %s", entry.second,
200 pluralise_monster(name).c_str());
201 }
202
203 message += name;
204 if (count == types.size() - 1)
205 message += " and ";
206 else if (count < types.size())
207 message += ", ";
208 ++count;
209 }
210 return message;
211 }
212
_mons_genus_keep_uniques(monster_type mc)213 static monster_type _mons_genus_keep_uniques(monster_type mc)
214 {
215 return mons_is_unique(mc) ? mc : mons_genus(mc);
216 }
217
218 /**
219 * Monster list simplification
220 *
221 * When too many monsters come into view at once, we group the ones with the
222 * same genus, starting with the most represented genus.
223 *
224 * @param types monster types and the number of monster for each type.
225 * @param genera monster genera and the number of monster for each genus.
226 */
_genus_factoring(map<monster_type,int> & types,map<monster_type,int> & genera)227 static void _genus_factoring(map<monster_type, int> &types,
228 map<monster_type, int> &genera)
229 {
230 monster_type genus = MONS_NO_MONSTER;
231 int num = 0;
232 // Find the most represented genus.
233 for (const auto &entry : genera)
234 if (entry.second > num)
235 {
236 genus = entry.first;
237 num = entry.second;
238 }
239
240 // The most represented genus has a single member.
241 // No more factoring is possible, we're done.
242 if (num == 1)
243 {
244 genera.clear();
245 return;
246 }
247
248 genera.erase(genus);
249 auto it = types.begin();
250 do
251 {
252 if (_mons_genus_keep_uniques(it->first) != genus)
253 {
254 ++it;
255 continue;
256 }
257
258 // This genus has a single monster type. Can't factor.
259 if (it->second == num)
260 return;
261
262 types.erase(it++);
263
264 } while (it != types.end());
265
266 types[genus] = num;
267 }
268
_is_weapon_worth_listing(const unique_ptr<item_def> & wpn)269 static bool _is_weapon_worth_listing(const unique_ptr<item_def> &wpn)
270 {
271 return wpn && (wpn->base_type == OBJ_STAVES
272 || is_unrandom_artefact(*wpn.get())
273 || get_weapon_brand(*wpn.get()) != SPWPN_NORMAL);
274 }
275
_is_item_worth_listing(const unique_ptr<item_def> & item)276 static bool _is_item_worth_listing(const unique_ptr<item_def> &item)
277 {
278 return item && (item_is_branded(*item.get())
279 || is_artefact(*item.get()));
280 }
281
_is_mon_equipment_worth_listing(const monster_info & mi)282 static bool _is_mon_equipment_worth_listing(const monster_info &mi)
283 {
284
285 if (_is_weapon_worth_listing(mi.inv[MSLOT_WEAPON]))
286 return true;
287 const unique_ptr<item_def> &alt_weap = mi.inv[MSLOT_ALT_WEAPON];
288 if (mi.wields_two_weapons() && _is_weapon_worth_listing(alt_weap))
289 return true;
290 // can a wand be in the alt weapon slot? get_monster_equipment_desc seems to
291 // think so, so we'll check
292 if (alt_weap && alt_weap->base_type == OBJ_WANDS)
293 return true;
294 if (mi.inv[MSLOT_WAND])
295 return true;
296
297 return _is_item_worth_listing(mi.inv[MSLOT_SHIELD])
298 || _is_item_worth_listing(mi.inv[MSLOT_ARMOUR])
299 || _is_item_worth_listing(mi.inv[MSLOT_JEWELLERY])
300 || _is_item_worth_listing(mi.inv[MSLOT_MISSILE]);
301 }
302
303 /// Return a warning for the player about newly-seen monsters, as appropriate.
_monster_headsup(const vector<monster * > & monsters,const map<monster_type,int> & types,bool divine)304 static string _monster_headsup(const vector<monster*> &monsters,
305 const map<monster_type, int> &types,
306 bool divine)
307 {
308 string warning_msg = "";
309 for (const monster* mon : monsters)
310 {
311 monster_info mi(mon);
312 const bool zin_ided = mon->props.exists("zin_id");
313 const bool has_interesting_equipment
314 = _is_mon_equipment_worth_listing(mi);
315 if ((divine && !zin_ided)
316 || (!divine && !has_interesting_equipment))
317 {
318 continue;
319 }
320
321 if (!divine && monsters.size() == 1)
322 continue; // don't give redundant warnings for enemies
323
324 if (warning_msg.size())
325 warning_msg += " ";
326
327 string monname;
328 if (monsters.size() == 1)
329 monname = mon->pronoun(PRONOUN_SUBJECTIVE);
330 else if (mon->type == MONS_DANCING_WEAPON)
331 monname = "There";
332 else if (types.at(mon->type) == 1)
333 monname = mon->full_name(DESC_THE);
334 else
335 monname = mon->full_name(DESC_A);
336 warning_msg += uppercase_first(monname);
337
338 warning_msg += " ";
339 if (monsters.size() == 1)
340 warning_msg += conjugate_verb("are", mon->pronoun_plurality());
341 else
342 warning_msg += "is";
343
344 mons_equip_desc_level_type level = mon->type != MONS_DANCING_WEAPON
345 ? DESC_IDENTIFIED : DESC_WEAPON_WARNING;
346
347 if (!divine)
348 {
349 if (mon->type != MONS_DANCING_WEAPON)
350 warning_msg += " ";
351 warning_msg += get_monster_equipment_desc(mi, level, DESC_NONE);
352 warning_msg += ".";
353 continue;
354 }
355
356 if (you_worship(GOD_ZIN))
357 {
358 warning_msg += " a foul ";
359 if (mon->has_ench(ENCH_GLOWING_SHAPESHIFTER))
360 warning_msg += "glowing ";
361 warning_msg += "shapeshifter";
362 }
363 else
364 {
365 // TODO: deduplicate
366 if (mon->type != MONS_DANCING_WEAPON)
367 warning_msg += " ";
368 warning_msg += get_monster_equipment_desc(mi, level, DESC_NONE);
369 }
370 warning_msg += ".";
371 }
372
373 return warning_msg;
374 }
375
376 /// Let Ash/Zin warn the player about newly-seen monsters, as appropriate.
_divine_headsup(const vector<monster * > & monsters,const map<monster_type,int> & types)377 static void _divine_headsup(const vector<monster*> &monsters,
378 const map<monster_type, int> &types)
379 {
380 const string warnings = _monster_headsup(monsters, types, true);
381 if (!warnings.size())
382 return;
383
384 const string warning_msg = " warns you: " + warnings;
385 simple_god_message(warning_msg.c_str());
386 #ifndef USE_TILE_LOCAL
387 // XXX: should this really be here...?
388 if (you_worship(GOD_ZIN))
389 update_monster_pane();
390 #endif
391 }
392
_secular_headsup(const vector<monster * > & monsters,const map<monster_type,int> & types)393 static void _secular_headsup(const vector<monster*> &monsters,
394 const map<monster_type, int> &types)
395 {
396 const string warnings = _monster_headsup(monsters, types, false);
397 if (!warnings.size())
398 return;
399 mprf(MSGCH_MONSTER_WARNING, "%s", warnings.c_str());
400 }
401
_count_monster_types(const vector<monster * > & monsters,const unsigned int max_types=UINT_MAX)402 static map<monster_type, int> _count_monster_types(const vector<monster*>& monsters,
403 const unsigned int max_types = UINT_MAX)
404 {
405 map<monster_type, int> types;
406 map<monster_type, int> genera; // This is the plural for genus!
407 for (const monster *mon : monsters)
408 {
409 const monster_type type = mon->type;
410 types[type]++;
411 genera[_mons_genus_keep_uniques(type)]++;
412 }
413
414 while (types.size() > max_types && !genera.empty())
415 _genus_factoring(types, genera);
416
417 return types;
418 }
419
420 /**
421 * Return a string listing monsters in a human readable form.
422 * E.g. "a hydra and 2 liches".
423 *
424 * @param monsters A list of monsters that just became visible.
425 */
describe_monsters_condensed(const vector<monster * > & monsters)426 string describe_monsters_condensed(const vector<monster*>& monsters)
427 {
428 return _desc_mons_type_map(_count_monster_types(monsters, 4));
429 }
430
431 /**
432 * Handle printing "foo comes into view" messages for newly appeared monsters.
433 * Also let Ash/Zin warn the player about newly-seen monsters, as appropriate.
434 *
435 * @param msgs A list of individual 'comes into view' messages; e.g.
436 * "the goblin comes into view.", "Mara opens the gate."
437 * @param monsters A list of monsters that just became visible.
438 */
_handle_comes_into_view(const vector<string> & msgs,const vector<monster * > monsters)439 static void _handle_comes_into_view(const vector<string> &msgs,
440 const vector<monster*> monsters)
441 {
442 if (monsters.size() == 1)
443 mprf(MSGCH_MONSTER_WARNING, "%s", msgs[0].c_str());
444 else
445 mprf(MSGCH_MONSTER_WARNING, "%s come into view.",
446 describe_monsters_condensed(monsters).c_str());
447
448 const auto& types = _count_monster_types(monsters);
449 _divine_headsup(monsters, types);
450 _secular_headsup(monsters, types);
451 }
452
453 /// If the player has the shout mutation, maybe shout at newly-seen monsters.
_maybe_trigger_shoutitis(const vector<monster * > monsters)454 static void _maybe_trigger_shoutitis(const vector<monster*> monsters)
455 {
456 if (!you.get_mutation_level(MUT_SCREAM))
457 return;
458
459 for (const monster* mon : monsters)
460 {
461 if (!mons_is_tentacle_or_tentacle_segment(mon->type)
462 && !mons_is_conjured(mon->type)
463 && x_chance_in_y(you.get_mutation_level(MUT_SCREAM) * 6, 100))
464 {
465 yell(mon);
466 return;
467 }
468 }
469 }
470
471 /// Let Gozag's wrath buff newly-seen hostile monsters, maybe.
_maybe_gozag_incite(vector<monster * > monsters)472 static void _maybe_gozag_incite(vector<monster*> monsters)
473 {
474 if (!player_under_penance(GOD_GOZAG))
475 return;
476
477 counted_monster_list mon_count;
478 vector<monster *> incited;
479 for (monster* mon : monsters)
480 {
481 // XXX: some of this is probably redundant with interrupt_activity
482 if (!mon->see_cell(you.pos()) // xray_vision
483 || mon->wont_attack()
484 || mon->is_stationary()
485 || mons_is_object(mon->type)
486 || mons_is_tentacle_or_tentacle_segment(mon->type))
487 {
488 continue;
489 }
490
491 if (coinflip()
492 && mon->get_experience_level() >= random2(you.experience_level))
493 {
494 mon_count.add(mon);
495 incited.push_back(mon);
496 }
497 }
498
499 if (incited.empty())
500 return;
501
502 string msg = make_stringf("%s incites %s against you.",
503 god_name(GOD_GOZAG).c_str(),
504 mon_count.describe().c_str());
505 if (strwidth(msg) >= get_number_of_cols() - 2)
506 {
507 msg = make_stringf("%s incites your enemies against you.",
508 god_name(GOD_GOZAG).c_str());
509 }
510 mprf(MSGCH_GOD, GOD_GOZAG, "%s", msg.c_str());
511
512 for (monster *mon : incited)
513 gozag_incite(mon);
514 }
515
update_monsters_in_view()516 void update_monsters_in_view()
517 {
518 int num_hostile = 0;
519 vector<string> msgs;
520 vector<monster*> monsters;
521
522 for (monster_iterator mi; mi; ++mi)
523 {
524 if (you.see_cell(mi->pos()))
525 {
526 if (mi->attitude == ATT_HOSTILE)
527 num_hostile++;
528
529 if (mi->visible_to(&you))
530 {
531 if (handle_seen_interrupt(*mi, &msgs))
532 monsters.push_back(*mi);
533 seen_monster(*mi);
534 }
535 else
536 mi->flags &= ~MF_WAS_IN_VIEW;
537 }
538 else if (!you.turn_is_over)
539 {
540 if (mi->flags & MF_WAS_IN_VIEW)
541 {
542 // Reset client id so the player doesn't know (for sure) he
543 // has seen this monster before when it reappears.
544 mi->reset_client_id();
545 }
546
547 mi->flags &= ~MF_WAS_IN_VIEW;
548
549 // If the monster hasn't been seen by the time that the player
550 // gets control back then seen_context is out of date.
551 mi->seen_context = SC_NONE;
552 }
553 }
554
555 // Summoners may have lost their summons upon seeing us and converting
556 // leaving invalid monsters in this vector.
557 monsters.erase(
558 std::remove_if(monsters.begin(), monsters.end(),
559 [](const monster * m) { return !m->alive(); }),
560 monsters.end());
561
562 if (!msgs.empty())
563 {
564 _handle_comes_into_view(msgs, monsters);
565 // XXX: does interrupt_activity() add 'comes into view' messages to
566 // 'msgs' in ALL cases we want shoutitis/gozag wrath to trigger?
567 _maybe_trigger_shoutitis(monsters);
568 _maybe_gozag_incite(monsters);
569 }
570
571 // Xom thinks it's hilarious the way the player picks up an ever
572 // growing entourage of monsters while running through the Abyss.
573 // To approximate this, if the number of hostile monsters in view
574 // is greater than it ever was for this particular trip to the
575 // Abyss, Xom is stimulated in proportion to the number of
576 // hostile monsters. Thus if the entourage doesn't grow, then
577 // Xom becomes bored.
578 if (player_in_branch(BRANCH_ABYSS)
579 && you.attribute[ATTR_ABYSS_ENTOURAGE] < num_hostile)
580 {
581 you.attribute[ATTR_ABYSS_ENTOURAGE] = num_hostile;
582 xom_is_stimulated(12 * num_hostile);
583 }
584 }
585
586
587 // We logically associate a difficulty parameter with each tile on each level,
588 // to make deterministic passive mapping work. It is deterministic so that the
589 // reveal order doesn't, for example, change on reload.
590 //
591 // This function returns the difficulty parameters for each tile on the current
592 // level, whose difficulty is less than a certain amount.
593 //
594 // Random difficulties are used in the few cases where we want repeated maps
595 // to give different results; scrolls and cards, since they are a finite
596 // resource.
_tile_difficulties(bool random)597 static const FixedArray<uint8_t, GXM, GYM>& _tile_difficulties(bool random)
598 {
599 // We will often be called with the same level parameter and cutoff, so
600 // cache this (DS with passive mapping autoexploring could be 5000 calls
601 // in a second or so).
602 static FixedArray<uint8_t, GXM, GYM> cache;
603 static int cache_seed = -1;
604
605 if (random)
606 {
607 cache_seed = -1;
608 for (int y = Y_BOUND_1; y <= Y_BOUND_2; ++y)
609 for (int x = X_BOUND_1; x <= X_BOUND_2; ++x)
610 cache[x][y] = random2(100);
611 return cache;
612 }
613
614 // must not produce the magic value (-1)
615 int seed = ((static_cast<int>(you.where_are_you) << 8) + you.depth)
616 ^ (you.game_seed & 0x7fffffff);
617
618 if (seed == cache_seed)
619 return cache;
620
621 cache_seed = seed;
622
623 for (int y = Y_BOUND_1; y <= Y_BOUND_2; ++y)
624 for (int x = X_BOUND_1; x <= X_BOUND_2; ++x)
625 cache[x][y] = hash_with_seed(100, seed, y * GXM + x);
626
627 return cache;
628 }
629
_feat_default_map_colour(dungeon_feature_type feat)630 static colour_t _feat_default_map_colour(dungeon_feature_type feat)
631 {
632 if (player_in_branch(BRANCH_SEWER) && feat_is_water(feat))
633 return feat == DNGN_DEEP_WATER ? GREEN : LIGHTGREEN;
634 return BLACK;
635 }
636
637 // Returns true if it succeeded.
magic_mapping(int map_radius,int proportion,bool suppress_msg,bool force,bool deterministic,coord_def origin)638 bool magic_mapping(int map_radius, int proportion, bool suppress_msg,
639 bool force, bool deterministic,
640 coord_def origin)
641 {
642 if (!force && !is_map_persistent())
643 {
644 if (!suppress_msg)
645 canned_msg(MSG_DISORIENTED);
646
647 return false;
648 }
649
650 const bool wizard_map = (you.wizard && map_radius == 1000);
651
652 if (map_radius < 5)
653 map_radius = 5;
654
655 // now gradually weaker with distance:
656 const int pfar = map_radius * 7 / 10;
657 const int very_far = map_radius * 9 / 10;
658
659 bool did_map = false;
660 int num_altars = 0;
661 int num_shops_portals = 0;
662
663 const FixedArray<uint8_t, GXM, GYM>& difficulty =
664 _tile_difficulties(!deterministic);
665
666 for (radius_iterator ri(in_bounds(origin) ? origin : you.pos(),
667 map_radius, C_SQUARE);
668 ri; ++ri)
669 {
670 coord_def pos = *ri;
671 if (!wizard_map)
672 {
673 int threshold = proportion;
674
675 const int dist = grid_distance(you.pos(), pos);
676
677 if (dist > very_far)
678 threshold = threshold / 3;
679 else if (dist > pfar)
680 threshold = threshold * 2 / 3;
681
682 if (difficulty(pos) > threshold)
683 continue;
684 }
685
686 map_cell& knowledge = env.map_knowledge(pos);
687
688 if (knowledge.changed())
689 {
690 // If the player has already seen the square, update map
691 // knowledge with the new terrain. Otherwise clear what we had
692 // before.
693 if (knowledge.seen())
694 {
695 dungeon_feature_type newfeat = env.grid(pos);
696 trap_type tr = feat_is_trap(newfeat) ? get_trap_type(pos) : TRAP_UNASSIGNED;
697 knowledge.set_feature(newfeat, env.grid_colours(pos), tr);
698 }
699 else
700 knowledge.clear();
701 }
702
703 // Don't assume that DNGN_UNSEEN cells ever count as mapped.
704 // Because of a bug at one point in map forgetting, cells could
705 // spuriously get marked as mapped even when they were completely
706 // unseen.
707 const bool already_mapped = knowledge.mapped()
708 && knowledge.feat() != DNGN_UNSEEN;
709
710 if (!wizard_map && (knowledge.seen() || already_mapped))
711 continue;
712
713 const dungeon_feature_type feat = env.grid(pos);
714
715 bool open = true;
716
717 if (feat_is_solid(feat) && !feat_is_closed_door(feat))
718 {
719 open = false;
720 for (adjacent_iterator ai(pos); ai; ++ai)
721 {
722 if (map_bounds(*ai) && (!feat_is_opaque(env.grid(*ai))
723 || feat_is_closed_door(env.grid(*ai))))
724 {
725 open = true;
726 break;
727 }
728 }
729 }
730
731 if (open)
732 {
733 if (wizard_map)
734 {
735 knowledge.set_feature(feat, _feat_default_map_colour(feat),
736 feat_is_trap(env.grid(pos)) ? get_trap_type(pos)
737 : TRAP_UNASSIGNED);
738 }
739 else if (!knowledge.feat())
740 {
741 auto base_feat = magic_map_base_feat(feat);
742 auto colour = _feat_default_map_colour(base_feat);
743 auto trap = feat_is_trap(env.grid(pos)) ? get_trap_type(pos)
744 : TRAP_UNASSIGNED;
745 knowledge.set_feature(base_feat, colour, trap);
746 }
747 if (emphasise(pos))
748 knowledge.flags |= MAP_EMPHASIZE;
749
750 if (wizard_map)
751 {
752 if (is_notable_terrain(feat))
753 seen_notable_thing(feat, pos);
754
755 set_terrain_seen(pos);
756 #ifdef USE_TILE
757 tile_wizmap_terrain(pos);
758 #endif
759 }
760 else
761 {
762 set_terrain_mapped(pos);
763
764 if (get_cell_map_feature(knowledge) == MF_STAIR_BRANCH)
765 seen_notable_thing(feat, pos);
766
767 if (get_feature_dchar(feat) == DCHAR_ALTAR)
768 num_altars++;
769 else if (get_feature_dchar(feat) == DCHAR_ARCH)
770 num_shops_portals++;
771 }
772
773 did_map = true;
774 }
775 }
776
777 if (!suppress_msg)
778 {
779 if (did_map)
780 mpr("You feel aware of your surroundings.");
781 else
782 canned_msg(MSG_DISORIENTED);
783
784 vector<string> sensed;
785
786 if (num_altars > 0)
787 {
788 sensed.push_back(make_stringf("%d altar%s", num_altars,
789 num_altars > 1 ? "s" : ""));
790 }
791
792 if (num_shops_portals > 0)
793 {
794 const char* plur = num_shops_portals > 1 ? "s" : "";
795 sensed.push_back(make_stringf("%d shop%s/portal%s",
796 num_shops_portals, plur, plur));
797 }
798
799 if (!sensed.empty())
800 mpr_comma_separated_list("You sensed ", sensed);
801 }
802
803 return did_map;
804 }
805
fully_map_level()806 void fully_map_level()
807 {
808 for (rectangle_iterator ri(1); ri; ++ri)
809 {
810 bool ok = false;
811 for (adjacent_iterator ai(*ri, false); ai; ++ai)
812 if (!feat_is_opaque(env.grid(*ai)))
813 ok = true;
814 if (!ok)
815 continue;
816 env.map_knowledge(*ri).set_feature(env.grid(*ri), 0,
817 feat_is_trap(env.grid(*ri)) ? get_trap_type(*ri) : TRAP_UNASSIGNED);
818 set_terrain_seen(*ri);
819 #ifdef USE_TILE
820 tile_wizmap_terrain(*ri);
821 #endif
822 if (env.igrid(*ri) != NON_ITEM)
823 env.map_knowledge(*ri).set_detected_item();
824 env.pgrid(*ri) |= FPROP_SEEN_OR_NOEXP;
825 }
826 }
827
mon_enemies_around(const monster * mons)828 bool mon_enemies_around(const monster* mons)
829 {
830 // If the monster has a foe, return true.
831 if (mons->foe != MHITNOT && mons->foe != MHITYOU)
832 return true;
833
834 if (crawl_state.game_is_arena())
835 {
836 // If the arena-mode code in _handle_behaviour() hasn't set a foe then
837 // we don't have one.
838 return false;
839 }
840 else if (mons->wont_attack())
841 {
842 // Additionally, if an ally is nearby and *you* have a foe,
843 // consider it as the ally's enemy too.
844 return you.can_see(*mons) && there_are_monsters_nearby(true);
845 }
846 else
847 {
848 // For hostile monster* you* are the main enemy.
849 return mons->can_see(you);
850 }
851 }
852
853 // Returns a string containing a representation of the map. Leading and
854 // trailing spaces are trimmed from each line. Leading and trailing empty
855 // lines are also snipped.
screenshot()856 string screenshot()
857 {
858 vector<string> lines(crawl_view.viewsz.y);
859 unsigned int lsp = GXM;
860 for (int y = 0; y < crawl_view.viewsz.y; y++)
861 {
862 string line;
863 for (int x = 0; x < crawl_view.viewsz.x; x++)
864 {
865 // in grid coords
866 const coord_def gc = view2grid(crawl_view.viewp +
867 coord_def(x, y));
868 char32_t ch =
869 (!map_bounds(gc)) ? ' ' :
870 (gc == you.pos()) ? mons_char(you.symbol)
871 : get_cell_glyph(gc).ch;
872 line += stringize_glyph(ch);
873 }
874 // right-trim the line
875 for (int x = line.length() - 1; x >= 0; x--)
876 if (line[x] == ' ')
877 line.erase(x);
878 else
879 break;
880 // see how much it can be left-trimmed
881 for (unsigned int x = 0; x < line.length(); x++)
882 if (line[x] != ' ')
883 {
884 if (lsp > x)
885 lsp = x;
886 break;
887 }
888 lines[y] = line;
889 }
890
891 for (string &line : lines)
892 line.erase(0, lsp); // actually trim from the left
893 while (!lines.empty() && lines.back().empty())
894 lines.pop_back(); // then from the bottom
895
896 ostringstream ss;
897 unsigned int y = 0;
898 for (y = 0; y < lines.size() && lines[y].empty(); y++)
899 ; // ... and from the top
900 for (; y < lines.size(); y++)
901 ss << lines[y] << "\n";
902 return ss.str();
903 }
904
viewmap_flash_colour()905 int viewmap_flash_colour()
906 {
907 return _layers & LAYERS_ALL && you.berserk() ? RED : BLACK;
908 }
909
910 // Updates one square of the view area. Should only be called for square
911 // in LOS.
view_update_at(const coord_def & pos)912 void view_update_at(const coord_def &pos)
913 {
914 if (pos == you.pos())
915 return;
916
917 show_update_at(pos);
918 #ifdef USE_TILE
919 tile_draw_map_cell(pos, true);
920 #endif
921 #ifdef USE_TILE_WEB
922 tiles.mark_for_redraw(pos);
923 #endif
924
925 #ifndef USE_TILE_LOCAL
926 if (!env.map_knowledge(pos).visible())
927 return;
928 cglyph_t g = get_cell_glyph(pos);
929
930 int flash_colour = you.flash_colour == BLACK
931 ? viewmap_flash_colour()
932 : you.flash_colour;
933 monster_type mons = env.map_knowledge(pos).monster();
934 int cell_colour =
935 flash_colour &&
936 (mons == MONS_NO_MONSTER || mons_class_is_firewood(mons))
937 ? real_colour(flash_colour)
938 : g.col;
939
940 const coord_def vp = grid2view(pos);
941 // Don't draw off-screen.
942 if (crawl_view.in_viewport_v(vp))
943 {
944 cgotoxy(vp.x, vp.y, GOTO_DNGN);
945 put_colour_ch(cell_colour, g.ch);
946 }
947
948 // Force colour back to normal, else clrscr() will flood screen
949 // with this colour on DOS.
950 textcolour(LIGHTGREY);
951
952 #endif
953 #ifdef USE_TILE_WEB
954 tiles.mark_for_redraw(pos);
955 #endif
956 }
957
view_update()958 bool view_update()
959 {
960 if (you.num_turns > you.last_view_update)
961 {
962 viewwindow();
963 update_screen();
964 return true;
965 }
966 return false;
967 }
968
flash_view(use_animation_type a,colour_t colour,targeter * where)969 void flash_view(use_animation_type a, colour_t colour, targeter *where)
970 {
971 if (crawl_state.need_save && Options.use_animations & a)
972 {
973 #ifndef USE_TILE_LOCAL
974 save_cursor_pos save;
975 #endif
976
977 you.flash_colour = colour;
978 you.flash_where = where;
979 viewwindow(false);
980 update_screen();
981 }
982 }
983
flash_view_delay(use_animation_type a,colour_t colour,int flash_delay,targeter * where)984 void flash_view_delay(use_animation_type a, colour_t colour, int flash_delay,
985 targeter *where)
986 {
987 if (crawl_state.need_save && Options.use_animations & a)
988 {
989 flash_view(a, colour, where);
990 scaled_delay(flash_delay);
991 flash_view(a, 0);
992 }
993 }
994
995 enum class update_flag
996 {
997 affect_excludes = (1 << 0),
998 added_exclude = (1 << 1),
999 };
1000 DEF_BITFIELD(update_flags, update_flag);
1001
1002 // Do various updates when the player sees a cell. Returns whether
1003 // exclusion LOS might have been affected.
player_view_update_at(const coord_def & gc)1004 static update_flags player_view_update_at(const coord_def &gc)
1005 {
1006 maybe_remove_autoexclusion(gc);
1007 update_flags ret;
1008
1009 // Set excludes in a radius of 1 around harmful clouds genereated
1010 // by neither monsters nor the player.
1011 const cloud_struct* cloud = cloud_at(gc);
1012 if (cloud && !crawl_state.game_is_arena())
1013 {
1014 const cloud_struct &cl = *cloud;
1015
1016 bool did_exclude = false;
1017 if (!cl.temporary() && is_damaging_cloud(cl.type, false))
1018 {
1019 int size = cl.exclusion_radius();
1020
1021 // Steam clouds are less dangerous than the other ones,
1022 // so don't exclude the neighbour cells.
1023 if (cl.type == CLOUD_STEAM && size == 1)
1024 size = 0;
1025
1026 bool was_exclusion = is_exclude_root(gc);
1027 set_exclude(gc, size, true, false, true);
1028 if (!did_exclude && !was_exclusion)
1029 ret |= update_flag::added_exclude;
1030 }
1031 }
1032
1033 // Print hints mode messages for features in LOS.
1034 if (crawl_state.game_is_hints())
1035 hints_observe_cell(gc);
1036
1037 if (env.map_knowledge(gc).changed() || !env.map_knowledge(gc).seen())
1038 ret |= update_flag::affect_excludes;
1039
1040 set_terrain_visible(gc);
1041
1042 if (!(env.pgrid(gc) & FPROP_SEEN_OR_NOEXP))
1043 {
1044 if (!crawl_state.game_is_arena()
1045 && cell_triggers_conduct(gc)
1046 && !player_in_branch(BRANCH_TEMPLE))
1047 {
1048 did_god_conduct(DID_EXPLORATION, 2500);
1049 const int density = env.density ? env.density : 2000;
1050 you.exploration += div_rand_round(1<<16, density);
1051 roll_trap_effects();
1052 }
1053 env.pgrid(gc) |= FPROP_SEEN_OR_NOEXP;
1054 }
1055
1056 #ifdef USE_TILE
1057 const coord_def ep = grid2show(gc);
1058
1059 // We remove any references to mcache when
1060 // writing to the background.
1061 tile_env.bk_fg(gc) = tile_env.fg(ep);
1062 tile_env.bk_bg(gc) = tile_env.bg(ep);
1063 tile_env.bk_cloud(gc) = tile_env.cloud(ep);
1064 #endif
1065
1066 return ret;
1067 }
1068
player_view_update()1069 static void player_view_update()
1070 {
1071 if (crawl_state.game_is_arena())
1072 {
1073 for (rectangle_iterator ri(crawl_view.vgrdc, LOS_MAX_RANGE); ri; ++ri)
1074 player_view_update_at(*ri);
1075 // no need to do excludes on the arena
1076 return;
1077 }
1078
1079 vector<coord_def> update_excludes;
1080 bool need_update = false;
1081
1082 for (vision_iterator ri(you); ri; ++ri)
1083 {
1084 update_flags flags = player_view_update_at(*ri);
1085 if (flags & update_flag::affect_excludes)
1086 update_excludes.push_back(*ri);
1087 if (flags & update_flag::added_exclude)
1088 need_update = true;
1089 }
1090 // Update exclusion LOS for possibly affected excludes.
1091 update_exclusion_los(update_excludes);
1092 // Catch up on deferred updates for cloud excludes.
1093 if (need_update)
1094 deferred_exclude_update();
1095 }
1096
_draw_out_of_bounds(screen_cell_t * cell)1097 static void _draw_out_of_bounds(screen_cell_t *cell)
1098 {
1099 #ifndef USE_TILE_LOCAL
1100 cell->glyph = ' ';
1101 cell->colour = DARKGREY;
1102 #endif
1103 #ifdef USE_TILE
1104 cell->tile.fg = 0;
1105 cell->tile.bg = tileidx_out_of_bounds(you.where_are_you);
1106 #endif
1107 }
1108
_draw_outside_los(screen_cell_t * cell,const coord_def & gc,const coord_def & ep)1109 static void _draw_outside_los(screen_cell_t *cell, const coord_def &gc,
1110 const coord_def &ep)
1111 {
1112 #ifndef USE_TILE_LOCAL
1113 // Outside the env.show area.
1114 cglyph_t g = get_cell_glyph(gc);
1115 cell->glyph = g.ch;
1116 cell->colour = g.col;
1117 #endif
1118
1119 #ifdef USE_TILE
1120 // this is just for out-of-los rays, but I don't see a more efficient way..
1121 if (in_bounds(gc))
1122 cell->tile.bg = tile_env.bg(ep);
1123
1124 tileidx_out_of_los(&cell->tile.fg, &cell->tile.bg, &cell->tile.cloud, gc);
1125 #else
1126 UNUSED(ep);
1127 #endif
1128 }
1129
_draw_player(screen_cell_t * cell,const coord_def & gc,const coord_def & ep,bool anim_updates)1130 static void _draw_player(screen_cell_t *cell,
1131 const coord_def &gc, const coord_def &ep,
1132 bool anim_updates)
1133 {
1134 #ifndef USE_TILE_LOCAL
1135 // Player overrides everything in cell.
1136 cell->glyph = mons_char(you.symbol);
1137 cell->colour = mons_class_colour(you.symbol);
1138 if (you.swimming())
1139 {
1140 if (env.grid(gc) == DNGN_DEEP_WATER)
1141 cell->colour = BLUE;
1142 else
1143 cell->colour = CYAN;
1144 }
1145 if (Options.use_fake_player_cursor)
1146 cell->colour |= COLFLAG_REVERSE;
1147
1148 cell->colour = real_colour(cell->colour);
1149 #endif
1150
1151 #ifdef USE_TILE
1152 cell->tile.fg = tile_env.fg(ep) = tileidx_player();
1153 cell->tile.bg = tile_env.bg(ep);
1154 cell->tile.cloud = tile_env.cloud(ep);
1155 if (anim_updates)
1156 tile_apply_animations(cell->tile.bg, &tile_env.flv(gc));
1157 #else
1158 UNUSED(ep, anim_updates);
1159 #endif
1160 }
1161
_draw_los(screen_cell_t * cell,const coord_def & gc,const coord_def & ep,bool anim_updates)1162 static void _draw_los(screen_cell_t *cell,
1163 const coord_def &gc, const coord_def &ep,
1164 bool anim_updates)
1165 {
1166 #ifndef USE_TILE_LOCAL
1167 cglyph_t g = get_cell_glyph(gc);
1168 cell->glyph = g.ch;
1169 cell->colour = g.col;
1170 #endif
1171
1172 #ifdef USE_TILE
1173 cell->tile.fg = tile_env.fg(ep);
1174 cell->tile.bg = tile_env.bg(ep);
1175 cell->tile.cloud = tile_env.cloud(ep);
1176 if (anim_updates)
1177 tile_apply_animations(cell->tile.bg, &tile_env.flv(gc));
1178 #else
1179 UNUSED(ep, anim_updates);
1180 #endif
1181 }
1182
1183 class shake_viewport_animation: public animation
1184 {
1185 public:
shake_viewport_animation()1186 shake_viewport_animation() { frames = 5; frame_delay = 40; }
1187
init_frame(int)1188 void init_frame(int /*frame*/) override
1189 {
1190 offset = coord_def();
1191 offset.x = random2(3) - 1;
1192 offset.y = random2(3) - 1;
1193 }
1194
cell_cb(const coord_def & pos,int &)1195 coord_def cell_cb(const coord_def &pos, int &/*colour*/) override
1196 {
1197 return pos + offset;
1198 }
1199
1200 private:
1201 coord_def offset;
1202 };
1203
1204 class banish_animation: public animation
1205 {
1206 public:
banish_animation()1207 banish_animation(): remaining(false) { }
1208
init_frame(int frame)1209 void init_frame(int frame) override
1210 {
1211 current_frame = frame;
1212
1213 if (!frame)
1214 {
1215 frames = 10;
1216 hidden.clear();
1217 remaining = true;
1218 }
1219
1220 if (remaining)
1221 frames = frame + 2;
1222 else
1223 frames = frame;
1224
1225 remaining = false;
1226 }
1227
cell_cb(const coord_def & pos,int &)1228 coord_def cell_cb(const coord_def &pos, int &/*colour*/) override
1229 {
1230 if (pos == you.pos())
1231 return pos;
1232
1233 if (bool *found = map_find(hidden, pos))
1234 {
1235 if (*found)
1236 return coord_def(-1, -1);
1237 }
1238
1239 if (!random2(10 - current_frame))
1240 {
1241 hidden.insert(make_pair(pos, true));
1242 return coord_def(-1, -1);
1243 }
1244
1245 remaining = true;
1246 return pos;
1247 }
1248
1249 bool remaining;
1250 map<coord_def, bool> hidden;
1251 int current_frame;
1252 };
1253
1254 class orb_animation: public animation
1255 {
1256 public:
init_frame(int frame)1257 void init_frame(int frame) override
1258 {
1259 current_frame = frame;
1260 range = current_frame > 5
1261 ? (10 - current_frame)
1262 : current_frame;
1263 frame_delay = 3 * (6 - range) * (6 - range);
1264 }
1265
cell_cb(const coord_def & pos,int & colour)1266 coord_def cell_cb(const coord_def &pos, int &colour) override
1267 {
1268 const coord_def diff = pos - you.pos();
1269 const int dist = diff.x * diff.x * 4 / 9 + diff.y * diff.y;
1270 const int min = range * range;
1271 const int max = (range + 2) * (range + 2);
1272 if (dist >= min && dist < max)
1273 if (is_tiles())
1274 colour = MAGENTA;
1275 else
1276 colour = DARKGREY;
1277 else
1278 colour = 0;
1279
1280 return pos;
1281 }
1282
1283 int range;
1284 int current_frame;
1285 };
1286
1287 static shake_viewport_animation shake_viewport;
1288 static banish_animation banish;
1289 static orb_animation orb;
1290
1291 static animation *animations[NUM_ANIMATIONS] = {
1292 &shake_viewport,
1293 &banish,
1294 &orb
1295 };
1296
run_animation(animation_type anim,use_animation_type type,bool cleanup)1297 void run_animation(animation_type anim, use_animation_type type, bool cleanup)
1298 {
1299 #ifdef USE_TILE_WEB
1300 // XXX this doesn't work in webtiles yet
1301 if (is_tiles())
1302 return;
1303 #endif
1304 if (Options.use_animations & type)
1305 {
1306 animation *a = animations[anim];
1307
1308 viewwindow();
1309 update_screen();
1310
1311 for (int i = 0; i < a->frames; ++i)
1312 {
1313 a->init_frame(i);
1314 viewwindow(false, false, a);
1315 update_screen();
1316 delay(a->frame_delay);
1317 }
1318
1319 if (cleanup)
1320 {
1321 viewwindow();
1322 update_screen();
1323 }
1324 }
1325 }
1326
1327 static bool _view_is_updating = false;
1328
1329 crawl_view_buffer view_dungeon(animation *a, bool anim_updates, view_renderer *renderer);
1330
_viewwindow_should_render()1331 static bool _viewwindow_should_render()
1332 {
1333 if (you.asleep())
1334 return false;
1335 if (mouse_control::current_mode() != MOUSE_MODE_NORMAL)
1336 return true;
1337 if (you.running && you.running.is_rest())
1338 return Options.rest_delay != -1;
1339 const bool run_dont_draw = you.running && Options.travel_delay < 0
1340 && (!you.running.is_explore() || Options.explore_delay < 0);
1341 return !run_dont_draw;
1342 }
1343
1344 /**
1345 * Draws the main window using the character set returned
1346 * by get_show_glyph().
1347 *
1348 * @param show_updates if true, env.show and dependent structures
1349 * are updated. Should be set if anything in
1350 * view has changed.
1351 * @param tiles_only if true, only the tile view will be updated. This
1352 * is only relevant for Webtiles.
1353 * @param a[in] the animation to be showing, if any.
1354 * @param renderer[in] A view renderer used to inject extra visual elements.
1355 */
viewwindow(bool show_updates,bool tiles_only,animation * a,view_renderer * renderer)1356 void viewwindow(bool show_updates, bool tiles_only, animation *a, view_renderer *renderer)
1357 {
1358 if (_view_is_updating)
1359 {
1360 // recursive calls to this function can lead to memory corruption or
1361 // crashes, depending on the circumstance, because some functions
1362 // called from here (e.g. show_init) will wipe out a whole bunch of
1363 // map data that will still be lurking around higher on the call stack
1364 // as references. Because the call paths are so complicated, it's hard
1365 // to find a principled / non-brute-force way of preventing recursion
1366 // here -- though it's still better to prevent it by other means if
1367 // possible.
1368 dprf("Recursive viewwindow call attempted!");
1369 return;
1370 }
1371
1372 {
1373 unwind_bool updating(_view_is_updating, true);
1374
1375 #ifndef USE_TILE_LOCAL
1376 save_cursor_pos save;
1377
1378 if (crawl_state.smallterm)
1379 {
1380 smallterm_warning();
1381 update_screen();
1382 return;
1383 }
1384 #endif
1385
1386 // The player could be at (0,0) if we are called during level-gen; this can
1387 // happen via mpr -> interrupt_activity -> stop_delay -> runrest::stop
1388 if (you.duration[DUR_TIME_STEP] || you.pos().origin())
1389 return;
1390
1391 // Update the animation of cells only once per turn.
1392 const bool anim_updates = (you.last_view_update != you.num_turns);
1393
1394 if (anim_updates)
1395 you.frame_no++;
1396
1397 #ifdef USE_TILE
1398 tiles.clear_text_tags(TAG_NAMED_MONSTER);
1399
1400 if (show_updates)
1401 mcache.clear_nonref();
1402 #endif
1403
1404 if (show_updates || _layers != LAYERS_ALL)
1405 {
1406 if (!is_map_persistent())
1407 ash_detect_portals(false);
1408
1409 // TODO: why on earth is this called from here? It seems like it
1410 // should be called directly on changing location, or something
1411 // like that...
1412 if (you.on_current_level)
1413 show_init(_layers);
1414
1415 #ifdef USE_TILE
1416 tile_draw_floor();
1417 tile_draw_map_cells();
1418 #endif
1419 view_clear_overlays();
1420 }
1421
1422 if (show_updates)
1423 player_view_update();
1424
1425 if (_viewwindow_should_render())
1426 {
1427 const auto vbuf = view_dungeon(a, anim_updates, renderer);
1428
1429 you.last_view_update = you.num_turns;
1430 #ifndef USE_TILE_LOCAL
1431 if (!tiles_only)
1432 {
1433 puttext(crawl_view.viewp.x, crawl_view.viewp.y, vbuf);
1434 update_monster_pane();
1435 }
1436 #else
1437 UNUSED(tiles_only);
1438 #endif
1439 #ifdef USE_TILE
1440 tiles.set_need_redraw(you.running ? Options.tile_runrest_rate : 0);
1441 tiles.load_dungeon(vbuf, crawl_view.vgrdc);
1442 tiles.update_tabs();
1443 #endif
1444
1445 // Leaving it this way because short flashes can occur in long ones,
1446 // and this simply works without requiring a stack.
1447 you.flash_colour = BLACK;
1448 you.flash_where = 0;
1449 }
1450
1451 // Reset env.show if we munged it.
1452 if (_layers != LAYERS_ALL)
1453 show_init();
1454 }
1455 }
1456
1457 #ifdef USE_TILE
1458 struct tile_overlay
1459 {
1460 coord_def gc;
1461 tileidx_t tile;
1462 };
1463 static vector<tile_overlay> tile_overlays;
1464 static unsigned int tile_overlay_i;
1465
view_add_tile_overlay(const coord_def & gc,tileidx_t tile)1466 void view_add_tile_overlay(const coord_def &gc, tileidx_t tile)
1467 {
1468 tile_overlays.push_back({gc, tile});
1469 }
1470 #endif
1471
1472 #ifndef USE_TILE_LOCAL
1473 struct glyph_overlay
1474 {
1475 coord_def gc;
1476 cglyph_t glyph;
1477 };
1478 static vector<glyph_overlay> glyph_overlays;
1479 static unsigned int glyph_overlay_i;
1480
view_add_glyph_overlay(const coord_def & gc,cglyph_t glyph)1481 void view_add_glyph_overlay(const coord_def &gc, cglyph_t glyph)
1482 {
1483 glyph_overlays.push_back({gc, glyph});
1484 }
1485 #endif
1486
view_clear_overlays()1487 void view_clear_overlays()
1488 {
1489 #ifdef USE_TILE
1490 tile_overlays.clear();
1491 #endif
1492 #ifndef USE_TILE_LOCAL
1493 glyph_overlays.clear();
1494 #endif
1495 }
1496
1497 /**
1498 * Comparison function for coord_defs that orders coords based on the ordering
1499 * used by rectangle_iterator.
1500 */
_coord_def_cmp(const coord_def & l,const coord_def & r)1501 static bool _coord_def_cmp(const coord_def& l, const coord_def& r)
1502 {
1503 return l.y < r.y || (l.y == r.y && l.x < r.x);
1504 }
1505
_sort_overlays()1506 static void _sort_overlays()
1507 {
1508 /* Stable sort is needed so that we don't swap draw order within cells. */
1509 #ifdef USE_TILE
1510 stable_sort(begin(tile_overlays), end(tile_overlays),
1511 [](const tile_overlay &left, const tile_overlay &right) {
1512 return _coord_def_cmp(left.gc, right.gc);
1513 });
1514 tile_overlay_i = 0;
1515 #endif
1516 #ifndef USE_TILE_LOCAL
1517 stable_sort(begin(glyph_overlays), end(glyph_overlays),
1518 [](const glyph_overlay &left, const glyph_overlay &right) {
1519 return _coord_def_cmp(left.gc, right.gc);
1520 });
1521 glyph_overlay_i = 0;
1522 #endif
1523 }
1524
add_overlays(const coord_def & gc,screen_cell_t * cell)1525 static void add_overlays(const coord_def& gc, screen_cell_t* cell)
1526 {
1527 #ifdef USE_TILE
1528 while (tile_overlay_i < tile_overlays.size()
1529 && _coord_def_cmp(tile_overlays[tile_overlay_i].gc, gc))
1530 {
1531 tile_overlay_i++;
1532 }
1533 while (tile_overlay_i < tile_overlays.size()
1534 && tile_overlays[tile_overlay_i].gc == gc)
1535 {
1536 const auto &overlay = tile_overlays[tile_overlay_i];
1537 if (cell->tile.num_dngn_overlay == 0
1538 || cell->tile.dngn_overlay[cell->tile.num_dngn_overlay - 1]
1539 != static_cast<int>(overlay.tile))
1540 {
1541 cell->tile.dngn_overlay[cell->tile.num_dngn_overlay++] = overlay.tile;
1542 }
1543 tile_overlay_i++;
1544 }
1545 #endif
1546 #ifndef USE_TILE_LOCAL
1547 while (glyph_overlay_i < glyph_overlays.size()
1548 && _coord_def_cmp(glyph_overlays[glyph_overlay_i].gc, gc))
1549 {
1550 glyph_overlay_i++;
1551 }
1552 while (glyph_overlay_i < glyph_overlays.size()
1553 && glyph_overlays[glyph_overlay_i].gc == gc)
1554 {
1555 const auto &overlay = glyph_overlays[glyph_overlay_i];
1556 cell->glyph = overlay.glyph.ch;
1557 cell->colour = overlay.glyph.col;
1558 glyph_overlay_i++;
1559 }
1560 #endif
1561 }
1562
1563 /**
1564 * Constructs the main dungeon view, rendering it into a new crawl_view_buffer.
1565 *
1566 * @param a[in] the animation to be showing, if any.
1567 * @return A new view buffer with the rendered content.
1568 */
view_dungeon(animation * a,bool anim_updates,view_renderer * renderer)1569 crawl_view_buffer view_dungeon(animation *a, bool anim_updates, view_renderer *renderer)
1570 {
1571 crawl_view_buffer vbuf(crawl_view.viewsz);
1572
1573 screen_cell_t *cell(vbuf);
1574
1575 cursor_control cs(false);
1576
1577 _sort_overlays();
1578
1579 int flash_colour = you.flash_colour;
1580 if (flash_colour == BLACK)
1581 flash_colour = viewmap_flash_colour();
1582
1583 const coord_def tl = coord_def(1, 1);
1584 const coord_def br = vbuf.size();
1585 for (rectangle_iterator ri(tl, br); ri; ++ri)
1586 {
1587 // in grid coords
1588 const coord_def gc = a
1589 ? a->cell_cb(view2grid(*ri), flash_colour)
1590 : view2grid(*ri);
1591
1592 if (you.flash_where && you.flash_where->is_affected(gc) <= 0)
1593 draw_cell(cell, gc, anim_updates, 0);
1594 else
1595 draw_cell(cell, gc, anim_updates, flash_colour);
1596
1597 cell++;
1598 }
1599
1600 if (renderer)
1601 renderer->render(vbuf);
1602
1603 return vbuf;
1604 }
1605
draw_cell(screen_cell_t * cell,const coord_def & gc,bool anim_updates,int flash_colour)1606 void draw_cell(screen_cell_t *cell, const coord_def &gc,
1607 bool anim_updates, int flash_colour)
1608 {
1609 #ifdef USE_TILE
1610 cell->tile.clear();
1611 #endif
1612 const coord_def ep = grid2show(gc);
1613
1614 if (!map_bounds(gc))
1615 _draw_out_of_bounds(cell);
1616 else if (!crawl_view.in_los_bounds_g(gc))
1617 _draw_outside_los(cell, gc, coord_def());
1618 else if (gc == you.pos() && you.on_current_level
1619 && _layers & LAYER_PLAYER
1620 && !crawl_state.game_is_arena()
1621 && !crawl_state.arena_suspended)
1622 {
1623 _draw_player(cell, gc, ep, anim_updates);
1624 }
1625 else if (you.see_cell(gc) && you.on_current_level)
1626 _draw_los(cell, gc, ep, anim_updates);
1627 else
1628 _draw_outside_los(cell, gc, ep); // in los bounds but not visible
1629
1630 #ifdef USE_TILE
1631 cell->tile.map_knowledge = map_bounds(gc) ? env.map_knowledge(gc) : map_cell();
1632 cell->flash_colour = BLACK;
1633 #endif
1634
1635 #ifndef USE_TILE_LOCAL
1636 // Don't hide important information by recolouring monsters.
1637 bool allow_mon_recolour = query_map_knowledge(true, gc, [](const map_cell& m) {
1638 return m.monster() == MONS_NO_MONSTER || mons_class_is_firewood(m.monster());
1639 });
1640 #endif
1641
1642 // Is this cell excluded from movement by mesmerise-related statuses?
1643 // MAP_WITHHELD is set in `show.cc:_update_feat_at`.
1644 bool mesmerise_excluded = (gc != you.pos() // for fungus form
1645 && map_bounds(gc)
1646 && you.on_current_level
1647 && (env.map_knowledge(gc).flags & MAP_WITHHELD)
1648 && !feat_is_solid(env.grid(gc)));
1649
1650 // Alter colour if flashing the characters vision.
1651 if (flash_colour)
1652 {
1653 #ifndef USE_TILE_LOCAL
1654 if (!you.see_cell(gc))
1655 cell->colour = DARKGREY;
1656 else if (gc != you.pos() && allow_mon_recolour)
1657 cell->colour = real_colour(flash_colour);
1658 #endif
1659 #ifdef USE_TILE
1660 if (you.see_cell(gc))
1661 cell->flash_colour = real_colour(flash_colour);
1662 #endif
1663 }
1664 else if (crawl_state.darken_range)
1665 {
1666 if ((crawl_state.darken_range->obeys_mesmerise && mesmerise_excluded)
1667 || (!crawl_state.darken_range->valid_aim(gc)))
1668 {
1669 #ifndef USE_TILE_LOCAL
1670 if (allow_mon_recolour)
1671 cell->colour = DARKGREY;
1672 #endif
1673 #ifdef USE_TILE
1674 if (you.see_cell(gc))
1675 cell->tile.bg |= TILE_FLAG_OOR;
1676 #endif
1677 }
1678 }
1679 else if (crawl_state.flash_monsters)
1680 {
1681 #ifndef USE_TILE_LOCAL
1682 bool found = gc == you.pos();
1683
1684 if (!found)
1685 for (auto mon : *crawl_state.flash_monsters)
1686 {
1687 if (gc == mon->pos())
1688 {
1689 found = true;
1690 break;
1691 }
1692 }
1693
1694 if (!found)
1695 cell->colour = DARKGREY;
1696 #endif
1697 }
1698 else if (mesmerise_excluded) // but no range limits in place
1699 {
1700 #ifndef USE_TILE_LOCAL
1701 if (allow_mon_recolour)
1702 cell->colour = DARKGREY;
1703 #endif
1704
1705 #ifdef USE_TILE
1706 // Only grey out tiles within LOS; out-of-LOS tiles are already
1707 // darkened.
1708 if (you.see_cell(gc))
1709 cell->tile.bg |= TILE_FLAG_OOR;
1710 #endif
1711 }
1712
1713 #ifdef USE_TILE
1714 tile_apply_properties(gc, cell->tile);
1715 #endif
1716
1717 #ifndef USE_TILE_LOCAL
1718 if ((_layers != LAYERS_ALL || Options.always_show_exclusions)
1719 && you.on_current_level
1720 && map_bounds(gc)
1721 && (_layers == LAYERS_NONE
1722 || gc != you.pos()
1723 && (env.map_knowledge(gc).monster() == MONS_NO_MONSTER
1724 || !you.see_cell(gc)))
1725 && travel_colour_override(gc))
1726 {
1727 if (is_exclude_root(gc))
1728 cell->colour = Options.tc_excluded;
1729 else if (is_excluded(gc))
1730 cell->colour = Options.tc_exclude_circle;
1731 }
1732 #endif
1733
1734 add_overlays(gc, cell);
1735 }
1736
1737 // Hide view layers. The player can toggle certain layers back on
1738 // and the resulting configuration will be remembered for the
1739 // remainder of the game session.
_config_layers_menu()1740 static void _config_layers_menu()
1741 {
1742 bool exit = false;
1743
1744 _layers = _layers_saved;
1745 crawl_state.viewport_weapons = !!(_layers & LAYER_MONSTER_WEAPONS);
1746 crawl_state.viewport_monster_hp = !!(_layers & LAYER_MONSTER_HEALTH);
1747
1748 msgwin_set_temporary(true);
1749 while (!exit)
1750 {
1751 viewwindow();
1752 update_screen();
1753 mprf(MSGCH_PROMPT, "Select layers to display:\n"
1754 "<%s>(m)onsters</%s>|"
1755 "<%s>(p)layer</%s>|"
1756 "<%s>(i)tems</%s>|"
1757 "<%s>(c)louds</%s>"
1758 #ifndef USE_TILE_LOCAL
1759 "|"
1760 "<%s>monster (w)eapons</%s>|"
1761 "<%s>monster (h)ealth</%s>"
1762 #endif
1763 ,
1764 _layers & LAYER_MONSTERS ? "lightgrey" : "darkgrey",
1765 _layers & LAYER_MONSTERS ? "lightgrey" : "darkgrey",
1766 _layers & LAYER_PLAYER ? "lightgrey" : "darkgrey",
1767 _layers & LAYER_PLAYER ? "lightgrey" : "darkgrey",
1768 _layers & LAYER_ITEMS ? "lightgrey" : "darkgrey",
1769 _layers & LAYER_ITEMS ? "lightgrey" : "darkgrey",
1770 _layers & LAYER_CLOUDS ? "lightgrey" : "darkgrey",
1771 _layers & LAYER_CLOUDS ? "lightgrey" : "darkgrey"
1772 #ifndef USE_TILE_LOCAL
1773 ,
1774 _layers & LAYER_MONSTER_WEAPONS ? "lightgrey" : "darkgrey",
1775 _layers & LAYER_MONSTER_WEAPONS ? "lightgrey" : "darkgrey",
1776 _layers & LAYER_MONSTER_HEALTH ? "lightgrey" : "darkgrey",
1777 _layers & LAYER_MONSTER_HEALTH ? "lightgrey" : "darkgrey"
1778 #endif
1779 );
1780 mprf(MSGCH_PROMPT, "Press 'a' to toggle all layers. "
1781 "Press any other key to exit.");
1782
1783 switch (get_ch())
1784 {
1785 case 'm': _layers_saved = _layers ^= LAYER_MONSTERS; break;
1786 case 'p': _layers_saved = _layers ^= LAYER_PLAYER; break;
1787 case 'i': _layers_saved = _layers ^= LAYER_ITEMS; break;
1788 case 'c': _layers_saved = _layers ^= LAYER_CLOUDS; break;
1789 #ifndef USE_TILE_LOCAL
1790 case 'w': _layers_saved = _layers ^= LAYER_MONSTER_WEAPONS;
1791 if (_layers & LAYER_MONSTER_WEAPONS)
1792 _layers_saved = _layers |= LAYER_MONSTERS;
1793 break;
1794 case 'h': _layers_saved = _layers ^= LAYER_MONSTER_HEALTH;
1795 if (_layers & LAYER_MONSTER_HEALTH)
1796 _layers_saved = _layers |= LAYER_MONSTERS;
1797 break;
1798 #endif
1799 case 'a': if (_layers)
1800 _layers_saved = _layers = LAYERS_NONE;
1801 else
1802 {
1803 #ifndef USE_TILE_LOCAL
1804 _layers_saved = _layers = LAYERS_ALL
1805 | LAYER_MONSTER_WEAPONS
1806 | LAYER_MONSTER_HEALTH;
1807 #else
1808 _layers_saved = _layers = LAYERS_ALL;
1809 #endif
1810 }
1811 break;
1812 default:
1813 _layers = LAYERS_ALL;
1814 crawl_state.viewport_weapons = !!(_layers & LAYER_MONSTER_WEAPONS);
1815 crawl_state.viewport_monster_hp = !!(_layers & LAYER_MONSTER_HEALTH);
1816 exit = true;
1817 break;
1818 }
1819
1820 crawl_state.viewport_weapons = !!(_layers & LAYER_MONSTER_WEAPONS);
1821 crawl_state.viewport_monster_hp = !!(_layers & LAYER_MONSTER_HEALTH);
1822
1823 msgwin_clear_temporary();
1824 }
1825 msgwin_set_temporary(false);
1826
1827 canned_msg(MSG_OK);
1828 }
1829
toggle_show_terrain()1830 void toggle_show_terrain()
1831 {
1832 if (_layers == LAYERS_ALL)
1833 _config_layers_menu();
1834 else
1835 reset_show_terrain();
1836 }
1837
reset_show_terrain()1838 void reset_show_terrain()
1839 {
1840 if (_layers != LAYERS_ALL)
1841 mprf(MSGCH_PROMPT, "Restoring view layers.");
1842
1843 _layers = LAYERS_ALL;
1844 crawl_state.viewport_weapons = !!(_layers & LAYER_MONSTER_WEAPONS);
1845 crawl_state.viewport_monster_hp = !!(_layers & LAYER_MONSTER_HEALTH);
1846 }
1847
1848 ////////////////////////////////////////////////////////////////////////////
1849 // Term resize handling (generic).
1850
handle_terminal_resize()1851 void handle_terminal_resize()
1852 {
1853 crawl_state.terminal_resized = false;
1854
1855 if (crawl_state.terminal_resize_handler)
1856 (*crawl_state.terminal_resize_handler)();
1857 else
1858 crawl_view.init_geometry();
1859
1860 redraw_screen();
1861 update_screen();
1862 }
1863