1 /**
2 * @file
3 * @brief Rendering of map_cell to glyph and colour.
4 *
5 * This only needs the information within one object of type map_cell.
6 **/
7
8 #include "AppHdr.h"
9
10 #include "showsymb.h"
11
12 #include "cloud.h"
13 #include "colour.h"
14 #include "env.h"
15 #include "item-name.h"
16 #include "libutil.h" // map_find
17 #include "options.h"
18 #include "religion.h"
19 #include "stash.h"
20 #include "state.h"
21 #include "stringutil.h"
22 #include "tag-version.h"
23 #include "terrain.h"
24 #include "travel.h"
25 #include "viewchar.h"
26 #include "mon-tentacle.h"
27 #include "tilepick.h"
28 #include "rltiles/tiledef-player.h"
29
_cell_feat_show_colour(const map_cell & cell,const coord_def & loc,bool coloured)30 static unsigned short _cell_feat_show_colour(const map_cell& cell,
31 const coord_def& loc,
32 bool coloured)
33 {
34 dungeon_feature_type feat = cell.feat();
35 unsigned short colour = BLACK;
36 const feature_def &fdef = get_feature_def(feat);
37
38 // These do not obey vault recolouring.
39 const bool no_vault_recolour = feat_has_dry_floor(feat)
40 && feat != DNGN_FLOOR
41 && !feat_is_open_door(feat);
42
43 // These aren't shown mossy/bloody/slimy in console.
44 const bool norecolour = feat_is_door(feat) || no_vault_recolour;
45
46 if (is_stair_exclusion(loc))
47 colour = Options.tc_excluded;
48 else if (!coloured)
49 {
50 if (cell.flags & MAP_EMPHASIZE)
51 colour = fdef.seen_em_colour();
52 else if (cell.flags & MAP_MAGIC_MAPPED_FLAG)
53 colour = fdef.unseen_colour();
54 else
55 colour = fdef.seen_colour();
56
57 if (colour)
58 return colour;
59 }
60 else if (!feat_is_solid(feat)
61 && (cell.flags & (MAP_SANCTUARY_1 | MAP_SANCTUARY_2)))
62 {
63 if (cell.flags & MAP_SANCTUARY_1)
64 colour = YELLOW;
65 else if (cell.flags & MAP_SANCTUARY_2)
66 {
67 if (!one_chance_in(4))
68 colour = WHITE; // 3/4
69 else if (!one_chance_in(3))
70 colour = LIGHTCYAN; // 1/6
71 else
72 colour = LIGHTGREY; // 1/12
73 }
74 }
75 else if (cell.flags & MAP_BLOODY && !norecolour)
76 colour = RED;
77 else if (cell.flags & MAP_CORRODING && feat == DNGN_FLOOR)
78 colour = LIGHTGREEN;
79 else if (cell.flags & MAP_ICY
80 && (feat_is_wall(feat) || feat == DNGN_FLOOR))
81 {
82 if (feat_is_wall(feat))
83 colour = ETC_ICE;
84 else
85 colour = LIGHTCYAN;
86 }
87 else if (cell.feat_colour() && !no_vault_recolour)
88 colour = cell.feat_colour();
89 else
90 {
91 colour = fdef.colour();
92
93 if (fdef.em_colour() && fdef.em_colour() != fdef.colour()
94 && cell.flags & MAP_EMPHASIZE)
95 {
96 colour = fdef.em_colour();
97 }
98 }
99
100 if (feat == DNGN_SHALLOW_WATER && player_in_branch(BRANCH_SHOALS))
101 colour = ETC_WAVES;
102
103 if (feat_is_tree(feat) && env.forest_awoken_until)
104 colour = ETC_AWOKEN_FOREST;
105
106 if (feat == DNGN_FLOOR)
107 {
108 if (cell.flags & MAP_LIQUEFIED)
109 colour = ETC_LIQUEFIED;
110 else if (cell.flags & MAP_DISJUNCT)
111 colour = ETC_DISJUNCTION;
112 else if (cell.flags & MAP_HALOED)
113 {
114 if (cell.flags & MAP_SILENCED && cell.flags & MAP_UMBRAED)
115 colour = CYAN; // Default for silence.
116 else if (cell.flags & MAP_SILENCED)
117 colour = LIGHTCYAN;
118 else if (cell.flags & MAP_UMBRAED)
119 colour = fdef.colour(); // Cancels out!
120 else
121 colour = YELLOW;
122 }
123 else if (cell.flags & MAP_UMBRAED)
124 {
125 if (cell.flags & MAP_SILENCED)
126 colour = BLUE; // Silence gets darker
127 else
128 colour = MAGENTA; // If no holy or silence
129 }
130 else if (cell.flags & MAP_SILENCED)
131 colour = CYAN; // Silence but no holy/unholy
132 else if (cell.flags & MAP_ORB_HALOED)
133 colour = ETC_ORB_GLOW;
134 else if (cell.flags & MAP_QUAD_HALOED)
135 colour = BLUE;
136 #if TAG_MAJOR_VERSION == 34
137 else if (cell.flags & MAP_HOT)
138 colour = ETC_FIRE;
139 #endif
140 }
141
142 return colour;
143 }
144
_show_mons_type(const monster_info & mi)145 static monster_type _show_mons_type(const monster_info& mi)
146 {
147 if (mi.type == MONS_SLIME_CREATURE && mi.slime_size > 1)
148 return MONS_MERGED_SLIME_CREATURE;
149 else if (mi.type == MONS_ZOMBIE)
150 {
151 return mons_zombie_size(mi.base_type) == Z_BIG ?
152 MONS_ZOMBIE_LARGE : MONS_ZOMBIE_SMALL;
153 }
154 else if (mi.type == MONS_SKELETON)
155 {
156 return mons_zombie_size(mi.base_type) == Z_BIG ?
157 MONS_SKELETON_LARGE : MONS_SKELETON_SMALL;
158 }
159 else if (mi.type == MONS_SIMULACRUM)
160 {
161 return mons_zombie_size(mi.base_type) == Z_BIG ?
162 MONS_SIMULACRUM_LARGE : MONS_SIMULACRUM_SMALL;
163 }
164 else if (mi.type == MONS_SENSED)
165 return mi.base_type;
166
167 return mi.type;
168 }
169
_get_mons_colour(const monster_info & mi)170 static int _get_mons_colour(const monster_info& mi)
171 {
172 // Show hp directly on the monster, except for irrelevant ones.
173 // Fedhas worshippers might be interested in their plants however.
174 if (crawl_state.viewport_monster_hp
175 && (you_worship(GOD_FEDHAS) || !mons_class_is_firewood(mi.type)))
176 {
177 return dam_colour(mi) | COLFLAG_ITEM_HEAP;
178 }
179
180 int col = mi.colour();
181
182 // We really shouldn't store unmodified colour. This hack compares
183 // effective type, but really, all redefinitions should work instantly,
184 // rather than for newly spawned monsters only.
185 monster_type stype = _show_mons_type(mi);
186 if (stype != mi.type && mi.type != MONS_SENSED)
187 col = mons_class_colour(stype);
188
189 if (mi.is(MB_ROLLING))
190 col = ETC_BONE;
191
192 if (mi.is(MB_BERSERK))
193 col = RED;
194
195 if (mi.is(MB_MIRROR_DAMAGE))
196 col = ETC_DEATH;
197
198 if (mi.is(MB_INNER_FLAME))
199 col = ETC_FIRE;
200
201 if (mi.attitude == ATT_FRIENDLY)
202 col |= COLFLAG_FRIENDLY_MONSTER;
203 else if (mi.attitude != ATT_HOSTILE)
204 col |= COLFLAG_NEUTRAL_MONSTER;
205 else if (Options.stab_brand != CHATTR_NORMAL
206 && mi.is(MB_STABBABLE))
207 {
208 col |= COLFLAG_WILLSTAB;
209 }
210 else if (Options.may_stab_brand != CHATTR_NORMAL
211 && mi.is(MB_DISTRACTED))
212 {
213 col |= COLFLAG_MAYSTAB;
214 }
215 else if (mons_class_is_stationary(mi.type))
216 {
217 if (Options.feature_item_brand != CHATTR_NORMAL
218 && feat_stair_direction(env.grid(mi.pos)) != CMD_NO_CMD)
219 {
220 col |= COLFLAG_FEATURE_ITEM;
221 }
222 else if (Options.heap_brand != CHATTR_NORMAL
223 && you.visible_igrd(mi.pos) != NON_ITEM
224 && !crawl_state.game_is_arena())
225 {
226 col |= COLFLAG_ITEM_HEAP;
227 }
228 }
229
230 // Backlit monsters are fuzzy and override colours, but not brands.
231 if (!crawl_state.game_is_arena()
232 && !you.can_see_invisible()
233 && mi.is(MB_INVISIBLE)
234 && mi.attitude != ATT_FRIENDLY)
235 {
236 col = (col & COLFLAG_MASK) | DARKGREY;
237 }
238
239 return col;
240 }
241
_get_item_override(const item_def & item)242 static cglyph_t _get_item_override(const item_def &item)
243 {
244 cglyph_t g;
245 g.ch = 0;
246 g.col = 0;
247
248 // Skip costly name calculations if not needed.
249 if (Options.item_glyph_overrides.empty())
250 return g;
251
252 // use qualname for gold so that pile quantity doesn't affect caching.
253 // Could extend this to missiles, but in that case I can actually imagine
254 // someone writing annotation code that relies on a count.
255 // TODO: the caching here avoids the regex penalty, but annotation and
256 // item.name themselves are quite heavy when called a lot, so some better
257 // caching might be in order. (Or more efficient calls to this function.)
258 string name = stash_annotate_item(STASH_LUA_SEARCH_ANNOTATE, &item)
259 + " {" + item_prefix(item, false) + "} "
260 + item.name(item.base_type == OBJ_GOLD ? DESC_QUALNAME : DESC_PLAIN);
261
262 {
263 // Check the cache...
264 if (cglyph_t *gly = map_find(Options.item_glyph_cache, name))
265 return *gly;
266 }
267
268 for (auto ir : Options.item_glyph_overrides)
269 {
270 text_pattern tpat(ir.first);
271 if (tpat.matches(name))
272 {
273 // You may have a rule that sets the glyph but not colour for
274 // axes, then another that sets colour only for artefacts
275 // (useless items, etc). Thus, apply only parts that apply.
276 if (ir.second.ch)
277 g.ch = ir.second.ch;
278 if (ir.second.col)
279 g.col = ir.second.col;
280 }
281 }
282
283 // Matching against a list of regexps can be costly, save up to 1000
284 // last matches.
285 if (Options.item_glyph_cache.size() >= 1000)
286 Options.item_glyph_cache.clear();
287 Options.item_glyph_cache[name] = g;
288
289 return g;
290 }
291
get_cell_show_class(const map_cell & cell,bool only_stationary_monsters)292 show_class get_cell_show_class(const map_cell& cell,
293 bool only_stationary_monsters)
294 {
295 if (cell.invisible_monster())
296 return SH_INVIS_EXPOSED;
297
298 if (cell.monster() != MONS_NO_MONSTER
299 && (!only_stationary_monsters
300 || mons_class_is_stationary(cell.monster())))
301 {
302 return SH_MONSTER;
303 }
304
305 if (cell.cloud() != CLOUD_NONE)
306 return SH_CLOUD;
307
308 const dungeon_feature_type feat = cell.feat();
309 if (feat && feat_is_solid(feat)
310 || feat_has_dry_floor(feat)
311 && feat != DNGN_FLOOR
312 && !feat_is_open_door(feat)
313 && feat != DNGN_ABANDONED_SHOP
314 && feat != DNGN_STONE_ARCH
315 && feat != DNGN_EXPIRED_PORTAL
316 && !feat_is_fountain(feat))
317 {
318 return SH_FEATURE;
319 }
320
321 if (cell.item())
322 return SH_ITEM;
323
324 if (cell.feat())
325 return SH_FEATURE;
326
327 return SH_NOTHING;
328 }
329
330 static const unsigned short ripple_table[] =
331 {
332 BLUE, // BLACK => BLUE (default)
333 BLUE, // BLUE => BLUE
334 GREEN, // GREEN => GREEN
335 CYAN, // CYAN => CYAN
336 RED, // RED => RED
337 MAGENTA, // MAGENTA => MAGENTA
338 BROWN, // BROWN => BROWN
339 DARKGREY, // LIGHTGREY => DARKGREY
340 DARKGREY, // DARKGREY => DARKGREY
341 BLUE, // LIGHTBLUE => BLUE
342 GREEN, // LIGHTGREEN => GREEN
343 BLUE, // LIGHTCYAN => BLUE
344 RED, // LIGHTRED => RED
345 MAGENTA, // LIGHTMAGENTA => MAGENTA
346 BROWN, // YELLOW => BROWN
347 LIGHTGREY, // WHITE => LIGHTGREY
348 };
349
_get_cell_glyph_with_class(const map_cell & cell,const coord_def & loc,const show_class cls,int colour_mode)350 static cglyph_t _get_cell_glyph_with_class(const map_cell& cell,
351 const coord_def& loc,
352 const show_class cls,
353 int colour_mode)
354 {
355 const bool coloured = colour_mode == 0 ? cell.visible() : (colour_mode > 0);
356 cglyph_t g;
357 show_type show;
358
359 g.ch = 0;
360 const cloud_type cell_cloud = cell.cloud();
361
362 switch (cls)
363 {
364 case SH_INVIS_EXPOSED:
365 ASSERT(cell.invisible_monster());
366
367 show.cls = SH_INVIS_EXPOSED;
368 if (cell_cloud != CLOUD_NONE)
369 g.col = cell.cloud_colour();
370 else
371 g.col = ripple_table[cell.feat_colour() & 0xf];
372 break;
373
374 case SH_MONSTER:
375 {
376 show = cell.monster();
377 const monster_info* mi = cell.monsterinfo();
378 ASSERT(mi);
379
380 if (cell.detected_monster())
381 {
382 ASSERT(mi->type == MONS_SENSED);
383 if (mons_is_sensed(mi->base_type))
384 g.col = mons_class_colour(mi->base_type);
385 else
386 g.col = Options.detected_monster_colour;
387 }
388 else if (!coloured)
389 g.col = Options.remembered_monster_colour;
390 else
391 g.col = _get_mons_colour(*mi);
392
393 monster_type stype = _show_mons_type(*mi);
394 if (show.mons == MONS_SENSED)
395 g.ch = mons_char(mi->base_type);
396 else
397 g.ch = mons_char(stype);
398
399 if (mons_is_tentacle_segment(stype))
400 {
401 switch (tileidx_tentacle(*mi))
402 {
403 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_N_SW:
404 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_N_SE:
405 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_S_NW:
406 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_S_NE:
407 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_N_S:
408 g.ch = dchar_glyph(DCHAR_DRAW_VERT);
409 break;
410 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_W_NE:
411 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_W_SE:
412 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_E_NW:
413 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_E_SW:
414 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_E_W:
415 g.ch = dchar_glyph(DCHAR_DRAW_HORIZ);
416 break;
417 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_NE_SW:
418 g.ch = dchar_glyph(DCHAR_DRAW_SLASH);
419 break;
420 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_NW_SE:
421 g.ch = dchar_glyph(DCHAR_DRAW_BACKSLASH);
422 break;
423 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_E_N:
424 g.ch = dchar_glyph(DCHAR_DRAW_BL);
425 break;
426 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_E_S:
427 g.ch = dchar_glyph(DCHAR_DRAW_TL);
428 break;
429 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_S_W:
430 g.ch = dchar_glyph(DCHAR_DRAW_TR);
431 break;
432 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_N_W:
433 g.ch = dchar_glyph(DCHAR_DRAW_BR);
434 break;
435 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_NE_NW:
436 g.ch = dchar_glyph(DCHAR_DRAW_DOWN);
437 break;
438 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_SE_SW:
439 g.ch = dchar_glyph(DCHAR_DRAW_UP);
440 break;
441 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_NW_SW:
442 g.ch = dchar_glyph(DCHAR_DRAW_RIGHT);
443 break;
444 case TILEP_MONS_KRAKEN_TENTACLE_SEGMENT_NE_SE:
445 g.ch = dchar_glyph(DCHAR_DRAW_LEFT);
446 break;
447 default: break;
448 }
449 }
450
451 // If we want to show weapons, overwrite all of that.
452 item_def* weapon = mi->inv[MSLOT_WEAPON].get();
453 if (crawl_state.viewport_weapons && weapon)
454 {
455 show = *weapon;
456 g = _get_item_override(*weapon);
457 if (!g.col)
458 g.col = weapon->get_colour();
459 }
460
461 break;
462 }
463
464 case SH_CLOUD:
465 ASSERT(cell_cloud);
466 show.cls = SH_CLOUD;
467 if (coloured)
468 g.col = cell.cloud_colour();
469 else
470 g.col = DARKGRAY;
471
472 if (cloud_type_tile_info(cell.cloudinfo()->type).variation
473 == CTVARY_DUR)
474 {
475 // duration is already clamped to 0-3
476 int dur = cell.cloudinfo()->duration;
477 switch (dur)
478 {
479 case 0:
480 g.ch = dchar_glyph(DCHAR_CLOUD_TERMINAL);
481 break;
482 case 1:
483 g.ch = dchar_glyph(DCHAR_CLOUD_FADING);
484 break;
485 case 2:
486 g.ch = dchar_glyph(DCHAR_CLOUD_WEAK);
487 break;
488 case 3:
489 g.ch = dchar_glyph(DCHAR_CLOUD);
490 break;
491 }
492 }
493 else
494 g.ch = dchar_glyph(DCHAR_CLOUD);
495 break;
496
497 case SH_FEATURE:
498 show = cell.feat();
499 ASSERT(show);
500
501 g.col = _cell_feat_show_colour(cell, loc, coloured);
502
503 if (cell.item())
504 {
505 if (Options.feature_item_brand
506 && (feat_is_critical(cell.feat())
507 || feat_is_solid(cell.feat())))
508 {
509 g.col |= COLFLAG_FEATURE_ITEM;
510 }
511 else if (Options.trap_item_brand && feat_is_trap(cell.feat()))
512 g.col |= COLFLAG_TRAP_ITEM;
513 }
514 break;
515
516 case SH_ITEM:
517 {
518 const item_def* eitem = cell.item();
519 ASSERT(eitem);
520 show = *eitem;
521
522 g = _get_item_override(*eitem);
523
524 if (feat_is_water(cell.feat()))
525 g.col = _cell_feat_show_colour(cell, loc, coloured);
526 else if (!g.col)
527 g.col = eitem->get_colour();
528
529 // monster(mimic)-owned items have link = NON_ITEM+1+midx
530 if (cell.flags & MAP_MORE_ITEMS)
531 g.col |= COLFLAG_ITEM_HEAP;
532 break;
533 }
534
535 case SH_NOTHING:
536 case NUM_SHOW_CLASSES:
537 // blackness
538 g.ch = ' ';
539 return g;
540 }
541
542 if (Options.show_travel_trail && travel_trail_index(loc) >= 0)
543 {
544 const feature_def& fd = get_feature_def(DNGN_TRAVEL_TRAIL);
545
546 if (fd.symbol())
547 g.ch = fd.symbol();
548 if (fd.colour() != COLOUR_UNDEF)
549 g.col = fd.colour();
550
551 g.col |= COLFLAG_REVERSE;
552 }
553
554 if (!g.ch)
555 {
556 const feature_def &fdef = get_feature_def(show);
557 g.ch = !cell.seen() || cell.flags & MAP_EMPHASIZE ? fdef.magic_symbol()
558 : fdef.symbol();
559 }
560
561 if (g.col)
562 g.col = real_colour(g.col, loc);
563
564 return g;
565 }
566
get_cell_glyph(const coord_def & loc,bool only_stationary_monsters,int colour_mode)567 cglyph_t get_cell_glyph(const coord_def& loc, bool only_stationary_monsters,
568 int colour_mode)
569 {
570 // note: this does NOT determine output of the player glyph;
571 // that's handled by itself in _draw_player() in view.cc
572 const map_cell& cell = env.map_knowledge(loc);
573 const show_class cell_show_class =
574 get_cell_show_class(cell, only_stationary_monsters);
575 return _get_cell_glyph_with_class(cell, loc, cell_show_class, colour_mode);
576 }
577
get_feat_symbol(dungeon_feature_type feat)578 char32_t get_feat_symbol(dungeon_feature_type feat)
579 {
580 return get_feature_def(feat).symbol();
581 }
582
get_item_symbol(show_item_type it)583 char32_t get_item_symbol(show_item_type it)
584 {
585 return get_feature_def(show_type(it)).symbol();
586 }
587
get_item_glyph(const item_def & item)588 cglyph_t get_item_glyph(const item_def& item)
589 {
590 cglyph_t g = _get_item_override(item);
591 if (!g.ch)
592 g.ch = get_feature_def(show_type(item)).symbol();
593 if (!g.col)
594 g.col = item.get_colour();
595 return g;
596 }
597
get_mons_glyph(const monster_info & mi)598 cglyph_t get_mons_glyph(const monster_info& mi)
599 {
600 monster_type stype = _show_mons_type(mi);
601 cglyph_t g;
602
603 g.ch = mons_char(stype);
604 g.col = _get_mons_colour(mi);
605 g.col = real_colour(g.col);
606 return g;
607 }
608
glyph_to_tagstr(const cglyph_t & g)609 string glyph_to_tagstr(const cglyph_t& g)
610 {
611 string col = colour_to_str(g.col);
612 string ch = stringize_glyph(g.ch);
613 if (g.ch == '<')
614 ch += "<";
615 return make_stringf("<%s>%s</%s>", col.c_str(), ch.c_str(), col.c_str());
616 }
617