1 /**
2 * @file
3 * @brief Functions used to print information about various game objects.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "describe.h"
9
10 #include <algorithm>
11 #include <cstdio>
12 #include <cmath>
13 #include <iomanip>
14 #include <numeric>
15 #include <set>
16 #include <sstream>
17 #include <string>
18
19 #include "ability.h"
20 #include "adjust.h"
21 #include "areas.h"
22 #include "art-enum.h"
23 #include "artefact.h"
24 #include "branch.h"
25 #include "cloud.h" // cloud_type_name
26 #include "clua.h"
27 #include "colour.h"
28 #include "database.h"
29 #include "dbg-util.h"
30 #include "decks.h"
31 #include "delay.h"
32 #include "describe-spells.h"
33 #include "directn.h"
34 #include "english.h"
35 #include "env.h"
36 #include "tile-env.h"
37 #include "evoke.h"
38 #include "fight.h"
39 #include "ghost.h"
40 #include "god-abil.h"
41 #include "god-item.h"
42 #include "hints.h"
43 #include "invent.h"
44 #include "item-prop.h"
45 #include "item-status-flag-type.h"
46 #include "items.h"
47 #include "item-use.h"
48 #include "jobs.h"
49 #include "lang-fake.h"
50 #include "libutil.h"
51 #include "macro.h"
52 #include "melee-attack.h" // describe_to_hit
53 #include "message.h"
54 #include "mon-behv.h"
55 #include "mon-cast.h" // mons_spell_range
56 #include "mon-death.h"
57 #include "mon-tentacle.h"
58 #include "output.h"
59 #include "potion.h"
60 #include "ranged-attack.h" // describe_to_hit
61 #include "religion.h"
62 #include "rltiles/tiledef-feat.h"
63 #include "skills.h"
64 #include "species.h"
65 #include "spl-cast.h"
66 #include "spl-book.h"
67 #include "spl-goditem.h"
68 #include "spl-miscast.h"
69 #include "spl-summoning.h"
70 #include "spl-util.h"
71 #include "spl-wpnench.h"
72 #include "stash.h"
73 #include "state.h"
74 #include "stringutil.h" // to_string on Cygwin
75 #include "tag-version.h"
76 #include "terrain.h"
77 #include "throw.h" // is_pproj_active for describe_to_hit
78 #include "tile-flags.h"
79 #include "tilepick.h"
80 #ifdef USE_TILE_LOCAL
81 #include "tilereg-crt.h"
82 #include "rltiles/tiledef-dngn.h"
83 #endif
84 #ifdef USE_TILE
85 #include "tileview.h"
86 #endif
87 #include "transform.h"
88 #include "travel.h"
89 #include "unicode.h"
90 #include "viewchar.h"
91
92 using namespace ui;
93
94
95 static void _print_bar(int value, int scale, string name,
96 ostringstream &result, int base_value = INT_MAX);
97
98 static void _describe_mons_to_hit(const monster_info& mi, ostringstream &result);
99
count_desc_lines(const string & _desc,const int width)100 int count_desc_lines(const string &_desc, const int width)
101 {
102 string desc = get_linebreak_string(_desc, width);
103 return count(begin(desc), end(desc), '\n');
104 }
105
show_description(const string & body,const tile_def * tile)106 int show_description(const string &body, const tile_def *tile)
107 {
108 describe_info inf;
109 inf.body << body;
110 return show_description(inf, tile);
111 }
112
show_description(const describe_info & inf,const tile_def * tile)113 int show_description(const describe_info &inf, const tile_def *tile)
114 {
115 auto vbox = make_shared<Box>(Widget::VERT);
116
117 if (!inf.title.empty())
118 {
119 auto title_hbox = make_shared<Box>(Widget::HORZ);
120
121 #ifdef USE_TILE
122 if (tile)
123 {
124 auto icon = make_shared<Image>();
125 icon->set_tile(*tile);
126 icon->set_margin_for_sdl(0, 10, 0, 0);
127 title_hbox->add_child(move(icon));
128 }
129 #else
130 UNUSED(tile);
131 #endif
132
133 auto title = make_shared<Text>(inf.title);
134 title_hbox->add_child(move(title));
135
136 title_hbox->set_cross_alignment(Widget::CENTER);
137 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
138 title_hbox->set_margin_for_crt(0, 0, 1, 0);
139 vbox->add_child(move(title_hbox));
140 }
141
142 auto desc_sw = make_shared<Switcher>();
143 auto more_sw = make_shared<Switcher>();
144 desc_sw->current() = 0;
145 more_sw->current() = 0;
146
147 const string descs[2] = {
148 trimmed_string(process_description(inf, false)),
149 trimmed_string(inf.quote),
150 };
151
152 #ifdef USE_TILE_LOCAL
153 # define MORE_PREFIX "[<w>!</w>" "|<w>Right-click</w>" "]: "
154 #else
155 # define MORE_PREFIX "[<w>!</w>" "]: "
156 #endif
157
158 const char* mores[2] = {
159 MORE_PREFIX "<w>Description</w>|Quote",
160 MORE_PREFIX "Description|<w>Quote</w>",
161 };
162
163 for (int i = 0; i < (inf.quote.empty() ? 1 : 2); i++)
164 {
165 const auto &desc = descs[static_cast<int>(i)];
166 auto scroller = make_shared<Scroller>();
167 auto fs = formatted_string::parse_string(trimmed_string(desc));
168 auto text = make_shared<Text>(fs);
169 text->set_wrap_text(true);
170 scroller->set_child(text);
171 desc_sw->add_child(move(scroller));
172 more_sw->add_child(make_shared<Text>(
173 formatted_string::parse_string(mores[i])));
174 }
175
176 more_sw->set_margin_for_sdl(20, 0, 0, 0);
177 more_sw->set_margin_for_crt(1, 0, 0, 0);
178 desc_sw->expand_h = false;
179 desc_sw->align_x = Widget::STRETCH;
180 vbox->add_child(desc_sw);
181 if (!inf.quote.empty())
182 vbox->add_child(more_sw);
183
184 #ifdef USE_TILE_LOCAL
185 vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
186 #endif
187
188 auto popup = make_shared<ui::Popup>(vbox);
189
190 bool done = false;
191 int lastch;
192 popup->on_keydown_event([&](const KeyEvent& ev) {
193 lastch = ev.key();
194 if (!inf.quote.empty() && (lastch == '!' || lastch == CK_MOUSE_CMD || lastch == '^'))
195 desc_sw->current() = more_sw->current() = 1 - desc_sw->current();
196 else
197 done = !desc_sw->current_widget()->on_event(ev);
198 return true;
199 });
200
201 #ifdef USE_TILE_WEB
202 tiles.json_open_object();
203 if (tile)
204 {
205 tiles.json_open_object("tile");
206 tiles.json_write_int("t", tile->tile);
207 tiles.json_write_int("tex", get_tile_texture(tile->tile));
208 if (tile->ymax != TILE_Y)
209 tiles.json_write_int("ymax", tile->ymax);
210 tiles.json_close_object();
211 }
212 tiles.json_write_string("title", inf.title);
213 tiles.json_write_string("prefix", inf.prefix);
214 tiles.json_write_string("suffix", inf.suffix);
215 tiles.json_write_string("footer", inf.footer);
216 tiles.json_write_string("quote", inf.quote);
217 tiles.json_write_string("body", inf.body.str());
218 tiles.push_ui_layout("describe-generic", 0);
219 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
220 #endif
221
222 ui::run_layout(move(popup), done);
223
224 return lastch;
225 }
226
process_description(const describe_info & inf,bool include_title)227 string process_description(const describe_info &inf, bool include_title)
228 {
229 string desc;
230 if (!inf.prefix.empty())
231 desc += "\n\n" + trimmed_string(filtered_lang(inf.prefix));
232 if (!inf.title.empty() && include_title)
233 desc += "\n\n" + trimmed_string(filtered_lang(inf.title));
234 desc += "\n\n" + trimmed_string(filtered_lang(inf.body.str()));
235 if (!inf.suffix.empty())
236 desc += "\n\n" + trimmed_string(filtered_lang(inf.suffix));
237 if (!inf.footer.empty())
238 desc += "\n\n" + trimmed_string(filtered_lang(inf.footer));
239 trim_string(desc);
240 return desc;
241 }
242
jewellery_base_ability_string(int subtype)243 const char* jewellery_base_ability_string(int subtype)
244 {
245 switch (subtype)
246 {
247 #if TAG_MAJOR_VERSION == 34
248 case RING_SUSTAIN_ATTRIBUTES: return "SustAt";
249 #endif
250 case RING_WIZARDRY: return "Wiz";
251 case RING_FIRE: return "Fire";
252 case RING_ICE: return "Ice";
253 #if TAG_MAJOR_VERSION == 34
254 case RING_TELEPORTATION: return "*Tele";
255 case RING_TELEPORT_CONTROL: return "+cTele";
256 case AMU_HARM: return "Harm";
257 case AMU_THE_GOURMAND: return "Gourm";
258 #endif
259 case AMU_MANA_REGENERATION: return "RegenMP";
260 case AMU_ACROBAT: return "Acrobat";
261 #if TAG_MAJOR_VERSION == 34
262 case AMU_CONSERVATION: return "Cons";
263 case AMU_CONTROLLED_FLIGHT: return "cFly";
264 #endif
265 case AMU_GUARDIAN_SPIRIT: return "Spirit";
266 case AMU_FAITH: return "Faith";
267 case AMU_REFLECTION: return "Reflect";
268 #if TAG_MAJOR_VERSION == 34
269 case AMU_INACCURACY: return "Inacc";
270 #endif
271 }
272 return "";
273 }
274
275 #define known_proprt(prop) (proprt[(prop)] && known[(prop)])
276
277 /// How to display props of a given type?
278 enum class prop_note
279 {
280 /// The raw numeral; e.g "Slay+3", "Int-1"
281 numeral,
282 /// Plusses and minuses; "rF-", "rC++"
283 symbolic,
284 /// Don't note the number; e.g. "rMut"
285 plain,
286 };
287
288 struct property_annotators
289 {
290 artefact_prop_type prop;
291 prop_note spell_out;
292 };
293
_randart_propnames(const item_def & item,bool no_comma=false)294 static vector<string> _randart_propnames(const item_def& item,
295 bool no_comma = false)
296 {
297 artefact_properties_t proprt;
298 artefact_known_props_t known;
299 artefact_desc_properties(item, proprt, known);
300
301 vector<string> propnames;
302
303 // list the following in rough order of importance
304 const property_annotators propanns[] =
305 {
306 // (Generally) negative attributes
307 // These come first, so they don't get chopped off!
308 { ARTP_PREVENT_SPELLCASTING, prop_note::plain },
309 { ARTP_PREVENT_TELEPORTATION, prop_note::plain },
310 { ARTP_CONTAM, prop_note::plain },
311 { ARTP_ANGRY, prop_note::plain },
312 { ARTP_CAUSE_TELEPORTATION, prop_note::plain },
313 { ARTP_NOISE, prop_note::plain },
314 { ARTP_HARM, prop_note::plain },
315 { ARTP_RAMPAGING, prop_note::plain },
316 { ARTP_CORRODE, prop_note::plain },
317 { ARTP_DRAIN, prop_note::plain },
318 { ARTP_SLOW, prop_note::plain },
319 { ARTP_FRAGILE, prop_note::plain },
320
321 // Evokable abilities come second
322 { ARTP_BLINK, prop_note::plain },
323 { ARTP_BERSERK, prop_note::plain },
324 { ARTP_INVISIBLE, prop_note::plain },
325 { ARTP_FLY, prop_note::plain },
326
327 // Resists, also really important
328 { ARTP_ELECTRICITY, prop_note::plain },
329 { ARTP_POISON, prop_note::plain },
330 { ARTP_FIRE, prop_note::symbolic },
331 { ARTP_COLD, prop_note::symbolic },
332 { ARTP_NEGATIVE_ENERGY, prop_note::symbolic },
333 { ARTP_WILLPOWER, prop_note::symbolic },
334 { ARTP_REGENERATION, prop_note::symbolic },
335 { ARTP_RMUT, prop_note::plain },
336 { ARTP_RCORR, prop_note::plain },
337
338 // Quantitative attributes
339 { ARTP_HP, prop_note::numeral },
340 { ARTP_MAGICAL_POWER, prop_note::numeral },
341 { ARTP_AC, prop_note::numeral },
342 { ARTP_EVASION, prop_note::numeral },
343 { ARTP_STRENGTH, prop_note::numeral },
344 { ARTP_INTELLIGENCE, prop_note::numeral },
345 { ARTP_DEXTERITY, prop_note::numeral },
346 { ARTP_SLAYING, prop_note::numeral },
347 { ARTP_SHIELDING, prop_note::numeral },
348
349 // Qualitative attributes (and Stealth)
350 { ARTP_SEE_INVISIBLE, prop_note::plain },
351 { ARTP_STEALTH, prop_note::symbolic },
352 { ARTP_CLARITY, prop_note::plain },
353 { ARTP_RMSL, prop_note::plain },
354 { ARTP_ARCHMAGI, prop_note::plain },
355 };
356
357 const unrandart_entry *entry = nullptr;
358 if (is_unrandom_artefact(item))
359 entry = get_unrand_entry(item.unrand_idx);
360
361 // For randart jewellery, note the base jewellery type if it's not
362 // covered by artefact_desc_properties()
363 if (item.base_type == OBJ_JEWELLERY
364 && (item_ident(item, ISFLAG_KNOW_TYPE)))
365 {
366 const char* type = jewellery_base_ability_string(item.sub_type);
367 if (*type)
368 propnames.push_back(type);
369 }
370 else if (item_brand_known(item)
371 && !(is_unrandom_artefact(item) && entry
372 && entry->flags & UNRAND_FLAG_SKIP_EGO))
373 {
374 string ego;
375 if (item.base_type == OBJ_WEAPONS)
376 ego = weapon_brand_name(item, true);
377 else if (item.base_type == OBJ_ARMOUR)
378 ego = armour_ego_name(item, true);
379 if (!ego.empty())
380 {
381 // XXX: Ugly hack for adding a comma if needed.
382 bool extra_props = false;
383 for (const property_annotators &ann : propanns)
384 if (known_proprt(ann.prop) && ann.prop != ARTP_BRAND)
385 {
386 extra_props = true;
387 break;
388 }
389
390 if (!no_comma && extra_props
391 || is_unrandom_artefact(item)
392 && entry && entry->inscrip != nullptr)
393 {
394 ego += ",";
395 }
396
397 propnames.push_back(ego);
398 }
399 }
400
401 if (is_unrandom_artefact(item) && entry && entry->inscrip != nullptr)
402 propnames.push_back(entry->inscrip);
403
404 for (const property_annotators &ann : propanns)
405 {
406 if (known_proprt(ann.prop))
407 {
408 const int val = proprt[ann.prop];
409
410 // Don't show rF+/rC- for =Fire, or vice versa for =Ice.
411 if (item.base_type == OBJ_JEWELLERY)
412 {
413 if (item.sub_type == RING_FIRE
414 && (ann.prop == ARTP_FIRE && val == 1
415 || ann.prop == ARTP_COLD && val == -1))
416 {
417 continue;
418 }
419 if (item.sub_type == RING_ICE
420 && (ann.prop == ARTP_COLD && val == 1
421 || ann.prop == ARTP_FIRE && val == -1))
422 {
423 continue;
424 }
425 }
426
427 ostringstream work;
428 switch (ann.spell_out)
429 {
430 case prop_note::numeral: // e.g. AC+4
431 work << showpos << artp_name(ann.prop) << val;
432 break;
433 case prop_note::symbolic: // e.g. F++
434 {
435 work << artp_name(ann.prop);
436
437 char symbol = val > 0 ? '+' : '-';
438 const int sval = abs(val);
439 if (sval > 4)
440 work << symbol << sval;
441 else
442 work << string(sval, symbol);
443
444 break;
445 }
446 case prop_note::plain: // e.g. rPois or SInv
447 work << artp_name(ann.prop);
448 break;
449 }
450 propnames.push_back(work.str());
451 }
452 }
453
454 return propnames;
455 }
456
artefact_inscription(const item_def & item)457 string artefact_inscription(const item_def& item)
458 {
459 if (item.base_type == OBJ_BOOKS)
460 return "";
461
462 const vector<string> propnames = _randart_propnames(item);
463
464 string insc = comma_separated_line(propnames.begin(), propnames.end(),
465 " ", " ");
466 if (!insc.empty() && insc[insc.length() - 1] == ',')
467 insc.erase(insc.length() - 1);
468 return insc;
469 }
470
add_inscription(item_def & item,string inscrip)471 void add_inscription(item_def &item, string inscrip)
472 {
473 if (!item.inscription.empty())
474 {
475 if (ends_with(item.inscription, ","))
476 item.inscription += " ";
477 else
478 item.inscription += ", ";
479 }
480
481 item.inscription += inscrip;
482 }
483
_jewellery_base_ability_description(int subtype)484 static const char* _jewellery_base_ability_description(int subtype)
485 {
486 switch (subtype)
487 {
488 #if TAG_MAJOR_VERSION == 34
489 case RING_SUSTAIN_ATTRIBUTES:
490 return "It sustains your strength, intelligence and dexterity.";
491 #endif
492 case RING_WIZARDRY:
493 return "It improves your spell success rate.";
494 case RING_FIRE:
495 return "It enhances your fire magic.";
496 case RING_ICE:
497 return "It enhances your ice magic.";
498 #if TAG_MAJOR_VERSION == 34
499 case RING_TELEPORTATION:
500 return "It may teleport you next to monsters.";
501 case RING_TELEPORT_CONTROL:
502 return "It can be evoked for teleport control.";
503 case AMU_HARM:
504 return "It increases damage dealt and taken.";
505 case AMU_THE_GOURMAND:
506 return "It allows you to eat raw meat even when not hungry.";
507 #endif
508 case AMU_MANA_REGENERATION:
509 return "It increases your rate of magic regeneration.";
510 case AMU_ACROBAT:
511 return "It increases your evasion while moving and waiting.";
512 #if TAG_MAJOR_VERSION == 34
513 case AMU_CONSERVATION:
514 return "It protects your inventory from destruction.";
515 #endif
516 case AMU_GUARDIAN_SPIRIT:
517 return "It causes incoming damage to be divided between your reserves "
518 "of health and magic.";
519 case AMU_FAITH:
520 return "It allows you to gain divine favour quickly.";
521 case AMU_REFLECTION:
522 return "It reflects blocked missile attacks.";
523 #if TAG_MAJOR_VERSION == 34
524 case AMU_INACCURACY:
525 return "It reduces the accuracy of all your attacks.";
526 #endif
527 }
528 return "";
529 }
530
531 struct property_descriptor
532 {
533 artefact_prop_type property;
534 const char* desc; // If it contains %d, will be replaced by value.
535 bool is_graded_resist;
536 };
537
538 static const int MAX_ARTP_NAME_LEN = 10;
539
_padded_artp_name(artefact_prop_type prop)540 static string _padded_artp_name(artefact_prop_type prop)
541 {
542 string name = artp_name(prop);
543 name = chop_string(name, MAX_ARTP_NAME_LEN - 1, false) + ":";
544 name.append(MAX_ARTP_NAME_LEN - name.length(), ' ');
545 return name;
546 }
547
_randart_descrip(const item_def & item)548 static string _randart_descrip(const item_def &item)
549 {
550 string description;
551
552 artefact_properties_t proprt;
553 artefact_known_props_t known;
554 artefact_desc_properties(item, proprt, known);
555
556 const property_descriptor propdescs[] =
557 {
558 { ARTP_AC, "It affects your AC (%d).", false },
559 { ARTP_EVASION, "It affects your evasion (%d).", false},
560 { ARTP_STRENGTH, "It affects your strength (%d).", false},
561 { ARTP_INTELLIGENCE, "It affects your intelligence (%d).", false},
562 { ARTP_DEXTERITY, "It affects your dexterity (%d).", false},
563 { ARTP_SLAYING, "It affects your accuracy & damage with ranged "
564 "weapons and melee (%d).", false},
565 { ARTP_FIRE, "fire", true},
566 { ARTP_COLD, "cold", true},
567 { ARTP_ELECTRICITY, "It insulates you from electricity.", false},
568 { ARTP_POISON, "poison", true},
569 { ARTP_NEGATIVE_ENERGY, "negative energy", true},
570 { ARTP_HP, "It affects your health (%d).", false},
571 { ARTP_MAGICAL_POWER, "It affects your magic capacity (%d).", false},
572 { ARTP_SEE_INVISIBLE, "It lets you see invisible.", false},
573 { ARTP_INVISIBLE, "It lets you turn invisible.", false},
574 { ARTP_FLY, "It grants you flight.", false},
575 { ARTP_BLINK, "It lets you blink.", false},
576 { ARTP_BERSERK, "It lets you go berserk.", false},
577 { ARTP_NOISE, "It may make noises in combat.", false},
578 { ARTP_PREVENT_SPELLCASTING, "It prevents spellcasting.", false},
579 { ARTP_CAUSE_TELEPORTATION, "It may teleport you next to monsters.", false},
580 { ARTP_PREVENT_TELEPORTATION, "It prevents most forms of teleportation.",
581 false},
582 { ARTP_ANGRY, "It may make you go berserk in combat.", false},
583 { ARTP_CLARITY, "It protects you against confusion.", false},
584 { ARTP_CONTAM, "It causes magical contamination when unequipped.", false},
585 { ARTP_RMSL, "It protects you from missiles.", false},
586 { ARTP_REGENERATION, "It increases your rate of health regeneration.",
587 false},
588 { ARTP_RCORR, "It protects you from acid and corrosion.",
589 false},
590 { ARTP_RMUT, "It protects you from mutation.", false},
591 { ARTP_CORRODE, "It may corrode you when you take damage.", false},
592 { ARTP_DRAIN, "It drains your maximum health when unequipped.", false},
593 { ARTP_SLOW, "It may slow you when you take damage.", false},
594 { ARTP_FRAGILE, "It will be destroyed if unequipped.", false },
595 { ARTP_SHIELDING, "It affects your SH (%d).", false},
596 { ARTP_HARM, "It increases damage dealt and taken.", false},
597 { ARTP_RAMPAGING, "It bestows one free step when moving towards enemies.",
598 false},
599 { ARTP_ARCHMAGI, "It increases the power of your magical spells.", false},
600 };
601
602 bool need_newline = false;
603 // Give a short description of the base type, for base types with no
604 // corresponding ARTP.
605 if (item.base_type == OBJ_JEWELLERY
606 && (item_ident(item, ISFLAG_KNOW_TYPE)))
607 {
608 const char* type = _jewellery_base_ability_description(item.sub_type);
609 if (*type)
610 {
611 description += type;
612 need_newline = true;
613 }
614 }
615
616 for (const property_descriptor &desc : propdescs)
617 {
618 if (!known_proprt(desc.property)) // can this ever happen..?
619 continue;
620
621 string sdesc = desc.desc;
622
623 // FIXME Not the nicest hack.
624 char buf[80];
625 snprintf(buf, sizeof buf, "%+d", proprt[desc.property]);
626 sdesc = replace_all(sdesc, "%d", buf);
627
628 if (desc.is_graded_resist)
629 {
630 int idx = proprt[desc.property] + 3;
631 idx = min(idx, 6);
632 idx = max(idx, 0);
633
634 const char* prefixes[] =
635 {
636 "It makes you extremely vulnerable to ",
637 "It makes you very vulnerable to ",
638 "It makes you vulnerable to ",
639 "Buggy descriptor!",
640 "It protects you from ",
641 "It greatly protects you from ",
642 "It renders you almost immune to "
643 };
644 sdesc = prefixes[idx] + sdesc + '.';
645 }
646
647 if (need_newline)
648 description += '\n';
649 description += make_stringf("%s %s",
650 _padded_artp_name(desc.property).c_str(),
651 sdesc.c_str());
652 need_newline = true;
653 }
654
655 if (known_proprt(ARTP_WILLPOWER))
656 {
657 const int stval = proprt[ARTP_WILLPOWER];
658 char buf[80];
659 snprintf(buf, sizeof buf, "%s%s It %s%s your willpower.",
660 need_newline ? "\n" : "",
661 _padded_artp_name(ARTP_WILLPOWER).c_str(),
662 (stval < -1 || stval > 1) ? "greatly " : "",
663 (stval < 0) ? "decreases" : "increases");
664 description += buf;
665 need_newline = true;
666 }
667
668 if (known_proprt(ARTP_STEALTH))
669 {
670 const int stval = proprt[ARTP_STEALTH];
671 char buf[80];
672 snprintf(buf, sizeof buf, "%s%s It makes you %s%s stealthy.",
673 need_newline ? "\n" : "",
674 _padded_artp_name(ARTP_STEALTH).c_str(),
675 (stval < -1 || stval > 1) ? "much " : "",
676 (stval < 0) ? "less" : "more");
677 description += buf;
678 need_newline = true;
679 }
680
681 return description;
682 }
683 #undef known_proprt
684
685 // If item is an unrandart with a DESCRIP field, return its contents.
686 // Otherwise, return "".
_artefact_descrip(const item_def & item)687 static string _artefact_descrip(const item_def &item)
688 {
689 if (!is_artefact(item)) return "";
690
691 ostringstream out;
692 if (is_unrandom_artefact(item))
693 {
694 bool need_newline = false;
695 auto entry = get_unrand_entry(item.unrand_idx);
696 if (entry->dbrand)
697 {
698 out << entry->dbrand;
699 need_newline = true;
700 }
701 if (entry->descrip)
702 {
703 out << (need_newline ? "\n\n" : "") << entry->descrip;
704 need_newline = true;
705 }
706 if (!_randart_descrip(item).empty())
707 out << (need_newline ? "\n\n" : "") << _randart_descrip(item);
708 }
709 else
710 out << _randart_descrip(item);
711
712 // XXX: Can't happen, right?
713 if (!item_ident(item, ISFLAG_KNOW_PROPERTIES) && item_type_known(item))
714 out << "\nIt may have some hidden properties.";
715
716 return out.str();
717 }
718
719 static const char *trap_names[] =
720 {
721 "dart",
722 "arrow", "spear",
723 #if TAG_MAJOR_VERSION > 34
724 "dispersal",
725 "teleport",
726 #endif
727 "permanent teleport",
728 "alarm", "blade",
729 "bolt", "net", "Zot",
730 #if TAG_MAJOR_VERSION == 34
731 "needle",
732 #endif
733 "shaft", "passage", "pressure plate", "web",
734 #if TAG_MAJOR_VERSION == 34
735 "gas", "teleport",
736 "shadow", "dormant shadow", "dispersal"
737 #endif
738 };
739
trap_name(trap_type trap)740 string trap_name(trap_type trap)
741 {
742 COMPILE_CHECK(ARRAYSZ(trap_names) == NUM_TRAPS);
743
744 if (trap >= 0 && trap < NUM_TRAPS)
745 return trap_names[trap];
746 return "";
747 }
748
full_trap_name(trap_type trap)749 string full_trap_name(trap_type trap)
750 {
751 string basename = trap_name(trap);
752 switch (trap)
753 {
754 case TRAP_GOLUBRIA:
755 return basename + " of Golubria";
756 case TRAP_PLATE:
757 case TRAP_WEB:
758 case TRAP_SHAFT:
759 return basename;
760 default:
761 return basename + " trap";
762 }
763 }
764
str_to_trap(const string & s)765 int str_to_trap(const string &s)
766 {
767 // "Zot trap" is capitalised in trap_names[], but the other trap
768 // names aren't.
769 const string tspec = lowercase_string(s);
770
771 // allow a couple of synonyms
772 if (tspec == "random" || tspec == "any")
773 return TRAP_RANDOM;
774
775 for (int i = 0; i < NUM_TRAPS; ++i)
776 if (tspec == lowercase_string(trap_names[i]))
777 return i;
778
779 return -1;
780 }
781
782 /**
783 * How should this panlord be described?
784 *
785 * @param name The panlord's name; used as a seed for its appearance.
786 * @param flying Whether the panlord can fly.
787 * @returns a string including a description of its head, its body, its flight
788 * mode (if any), and how it smells or looks.
789 */
_describe_demon(const string & name,bool flying)790 static string _describe_demon(const string& name, bool flying)
791 {
792 const uint32_t seed = hash32(&name[0], name.size());
793 #define HRANDOM_ELEMENT(arr, id) arr[hash_with_seed(ARRAYSZ(arr), seed, id)]
794
795 static const char* body_types[] =
796 {
797 "armoured",
798 "vast, spindly",
799 "fat",
800 "obese",
801 "muscular",
802 "spiked",
803 "splotchy",
804 "slender",
805 "tentacled",
806 "emaciated",
807 "bug-like",
808 "skeletal",
809 "mantis",
810 "slithering",
811 };
812
813 static const char* wing_names[] =
814 {
815 "with small, bat-like wings",
816 "with bony wings",
817 "with sharp, metallic wings",
818 "with the wings of a moth",
819 "with thin, membranous wings",
820 "with dragonfly wings",
821 "with large, powerful wings",
822 "with fluttering wings",
823 "with great, sinister wings",
824 "with hideous, tattered wings",
825 "with sparrow-like wings",
826 "with hooked wings",
827 "with strange knobs attached",
828 "which hovers in mid-air",
829 "with sacs of gas hanging from its back",
830 };
831
832 const char* head_names[] =
833 {
834 "a cubic structure in place of a head",
835 "a brain for a head",
836 "a hideous tangle of tentacles for a mouth",
837 "the head of an elephant",
838 "an eyeball for a head",
839 "wears a helmet over its head",
840 "a horn in place of a head",
841 "a thick, horned head",
842 "the head of a horse",
843 "a vicious glare",
844 "snakes for hair",
845 "the face of a baboon",
846 "the head of a mouse",
847 "a ram's head",
848 "the head of a rhino",
849 "eerily human features",
850 "a gigantic mouth",
851 "a mass of tentacles growing from its neck",
852 "a thin, worm-like head",
853 "huge, compound eyes",
854 "the head of a frog",
855 "an insectoid head",
856 "a great mass of hair",
857 "a skull for a head",
858 "a cow's skull for a head",
859 "the head of a bird",
860 "a large fungus growing from its neck",
861 "an ominous eye at the end of a thin stalk",
862 "a face from nightmares",
863 };
864
865 static const char* misc_descs[] =
866 {
867 " It seethes with hatred of the living.",
868 " Tiny orange flames dance around it.",
869 " Tiny purple flames dance around it.",
870 " It is surrounded by a weird haze.",
871 " It glows with a malevolent light.",
872 " It looks incredibly angry.",
873 " It oozes with slime.",
874 " It dribbles constantly.",
875 " Mould grows all over it.",
876 " Its body is covered in fungus.",
877 " It is covered with lank hair.",
878 " It looks diseased.",
879 " It looks as frightened of you as you are of it.",
880 " It moves in a series of hideous convulsions.",
881 " It moves with an unearthly grace.",
882 " It leaves a glistening oily trail.",
883 " It shimmers before your eyes.",
884 " It is surrounded by a brilliant glow.",
885 " It radiates an aura of extreme power.",
886 " It seems utterly heartbroken.",
887 " It seems filled with irrepressible glee.",
888 " It constantly shivers and twitches.",
889 " Blue sparks crawl across its body.",
890 " It seems uncertain.",
891 " A cloud of flies swarms around it.",
892 " The air around it ripples with heat.",
893 " Crystalline structures grow on everything near it.",
894 " It appears supremely confident.",
895 " Its skin is covered in a network of cracks.",
896 " Its skin has a disgusting oily sheen.",
897 " It seems somehow familiar.",
898 " It is somehow always in shadow.",
899 " It is difficult to look away.",
900 " It is constantly speaking in tongues.",
901 " It babbles unendingly.",
902 " Its body is scourged by damnation.",
903 " Its body is extensively scarred.",
904 " You find it difficult to look away.",
905 " Oddly mechanical noises accompany its jarring movements.",
906 " Its skin looks unnervingly wrinkled.",
907 };
908
909 static const char* smell_descs[] =
910 {
911 " It smells of brimstone.",
912 " It is surrounded by a sickening stench.",
913 " It smells of rotting flesh.",
914 " It stinks of death.",
915 " It stinks of decay.",
916 " It smells delicious!",
917 };
918
919 ostringstream description;
920 description << "One of the many lords of Pandemonium, " << name << " has ";
921
922 description << article_a(HRANDOM_ELEMENT(body_types, 2));
923 description << " body ";
924
925 if (flying)
926 {
927 description << HRANDOM_ELEMENT(wing_names, 3);
928 description << " ";
929 }
930
931 description << "and ";
932 description << HRANDOM_ELEMENT(head_names, 1) << ".";
933
934 if (!hash_with_seed(5, seed, 4) && you.can_smell()) // 20%
935 description << HRANDOM_ELEMENT(smell_descs, 5);
936
937 if (hash_with_seed(2, seed, 6)) // 50%
938 description << HRANDOM_ELEMENT(misc_descs, 6);
939
940 return description.str();
941 }
942
943 /**
944 * Describe a given mutant beast's tier.
945 *
946 * @param tier The mutant_beast_tier of the beast in question.
947 * @return A string describing the tier; e.g.
948 * "It is a juvenile, out of the larval stage but still below its
949 * mature strength."
950 */
_describe_mutant_beast_tier(int tier)951 static string _describe_mutant_beast_tier(int tier)
952 {
953 static const string tier_descs[] = {
954 "It is of an unusually buggy age.",
955 "It is larval and weak, freshly emerged from its mother's pouch.",
956 "It is a juvenile, no longer larval but below its mature strength.",
957 "It is mature, stronger than a juvenile but weaker than its elders.",
958 "It is an elder, stronger than mature beasts.",
959 "It is a primal beast, the most powerful of its kind.",
960 };
961 COMPILE_CHECK(ARRAYSZ(tier_descs) == NUM_BEAST_TIERS);
962
963 ASSERT_RANGE(tier, 0, NUM_BEAST_TIERS);
964 return tier_descs[tier];
965 }
966
967
968 /**
969 * Describe a given mutant beast's facets.
970 *
971 * @param facets A vector of the mutant_beast_facets in question.
972 * @return A string describing the facets; e.g.
973 * "It flies and flits around unpredictably, and its breath
974 * smoulders ominously."
975 */
_describe_mutant_beast_facets(const CrawlVector & facets)976 static string _describe_mutant_beast_facets(const CrawlVector &facets)
977 {
978 static const string facet_descs[] = {
979 " seems unusually buggy.",
980 " sports a set of venomous tails",
981 " flies swiftly and unpredictably",
982 "s breath smoulders ominously",
983 " is covered with eyes and tentacles",
984 " flickers and crackles with electricity",
985 " is covered in dense fur and muscle",
986 };
987 COMPILE_CHECK(ARRAYSZ(facet_descs) == NUM_BEAST_FACETS);
988
989 if (facets.size() == 0)
990 return "";
991
992 return "It" + comma_separated_fn(begin(facets), end(facets),
993 [] (const CrawlStoreValue &sv) -> string {
994 const int facet = sv.get_int();
995 ASSERT_RANGE(facet, 0, NUM_BEAST_FACETS);
996 return facet_descs[facet];
997 }, ", and it", ", it")
998 + ".";
999
1000 }
1001
1002 /**
1003 * Describe a given mutant beast's special characteristics: its tier & facets.
1004 *
1005 * @param mi The player-visible information about the monster in question.
1006 * @return A string describing the monster; e.g.
1007 * "It is a juvenile, out of the larval stage but still below its
1008 * mature strength. It flies and flits around unpredictably, and
1009 * its breath has a tendency to ignite when angered."
1010 */
_describe_mutant_beast(const monster_info & mi)1011 static string _describe_mutant_beast(const monster_info &mi)
1012 {
1013 const int xl = mi.props[MUTANT_BEAST_TIER].get_short();
1014 const int tier = mutant_beast_tier(xl);
1015 const CrawlVector facets = mi.props[MUTANT_BEAST_FACETS].get_vector();
1016 return _describe_mutant_beast_facets(facets)
1017 + " " + _describe_mutant_beast_tier(tier);
1018 }
1019
1020 /**
1021 * Is the item associated with some specific training goal? (E.g. mindelay)
1022 *
1023 * @return the goal, or 0 if there is none, scaled by 10.
1024 */
_item_training_target(const item_def & item)1025 static int _item_training_target(const item_def &item)
1026 {
1027 const int throw_dam = property(item, PWPN_DAMAGE);
1028 if (item.base_type == OBJ_WEAPONS || item.base_type == OBJ_STAVES)
1029 return weapon_min_delay_skill(item) * 10;
1030 else if (is_shield(item))
1031 return round(you.get_shield_skill_to_offset_penalty(item) * 10);
1032 else if (item.base_type == OBJ_MISSILES && is_throwable(&you, item))
1033 return (((10 + throw_dam / 2) - FASTEST_PLAYER_THROWING_SPEED) * 2) * 10;
1034 else
1035 return 0;
1036 }
1037
1038 /**
1039 * Does an item improve with training some skill?
1040 *
1041 * @return the skill, or SK_NONE if there is none. Note: SK_NONE is *not* 0.
1042 */
_item_training_skill(const item_def & item)1043 static skill_type _item_training_skill(const item_def &item)
1044 {
1045 if (item.base_type == OBJ_WEAPONS || item.base_type == OBJ_STAVES)
1046 return item_attack_skill(item);
1047 else if (is_shield(item))
1048 return SK_SHIELDS; // shields are armour, so do shields before armour
1049 else if (item.base_type == OBJ_ARMOUR)
1050 return SK_ARMOUR;
1051 else if (item.base_type == OBJ_MISSILES && is_throwable(&you, item))
1052 return SK_THROWING;
1053 else if (item_is_evokable(item)) // not very accurate
1054 return SK_EVOCATIONS;
1055 else
1056 return SK_NONE;
1057 }
1058
1059 /**
1060 * Whether it would make sense to set a training target for an item.
1061 *
1062 * @param item the item to check.
1063 * @param ignore_current whether to ignore any current training targets (e.g. if there is a higher target, it might not make sense to set a lower one).
1064 */
_could_set_training_target(const item_def & item,bool ignore_current)1065 static bool _could_set_training_target(const item_def &item, bool ignore_current)
1066 {
1067 if (!crawl_state.need_save || is_useless_item(item)
1068 || you.has_mutation(MUT_DISTRIBUTED_TRAINING))
1069 {
1070 return false;
1071 }
1072
1073 const skill_type skill = _item_training_skill(item);
1074 if (skill == SK_NONE)
1075 return false;
1076
1077 const int target = min(_item_training_target(item), 270);
1078
1079 return target && !is_useless_skill(skill)
1080 && you.skill(skill, 10, false, false) < target
1081 && (ignore_current || you.get_training_target(skill) < target);
1082 }
1083
1084 /**
1085 * Produce the "Your skill:" line for item descriptions where specific skill targets
1086 * are relevant (weapons, missiles, shields)
1087 *
1088 * @param skill the skill to look at.
1089 * @param show_target_button whether to show the button for setting a skill target.
1090 * @param scaled_target a target, scaled by 10, to use when describing the button.
1091 */
_your_skill_desc(skill_type skill,bool show_target_button,int scaled_target)1092 static string _your_skill_desc(skill_type skill, bool show_target_button, int scaled_target)
1093 {
1094 if (!crawl_state.need_save || skill == SK_NONE)
1095 return "";
1096 string target_button_desc = "";
1097 int min_scaled_target = min(scaled_target, 270);
1098 if (show_target_button &&
1099 you.get_training_target(skill) < min_scaled_target)
1100 {
1101 target_button_desc = make_stringf(
1102 "; use <white>(s)</white> to set %d.%d as a target for %s.",
1103 min_scaled_target / 10, min_scaled_target % 10,
1104 skill_name(skill));
1105 }
1106 int you_skill_temp = you.skill(skill, 10);
1107 int you_skill = you.skill(skill, 10, false, false);
1108
1109 return make_stringf("Your %sskill: %d.%d%s",
1110 (you_skill_temp != you_skill ? "(base) " : ""),
1111 you_skill / 10, you_skill % 10,
1112 target_button_desc.c_str());
1113 }
1114
1115 /**
1116 * Produce a description of a skill target for items where specific targets are
1117 * relevant.
1118 *
1119 * @param skill the skill to look at.
1120 * @param scaled_target a skill level target, scaled by 10.
1121 * @param training a training value, from 0 to 100. Need not be the actual training
1122 * value.
1123 */
_skill_target_desc(skill_type skill,int scaled_target,unsigned int training)1124 static string _skill_target_desc(skill_type skill, int scaled_target,
1125 unsigned int training)
1126 {
1127 string description = "";
1128 scaled_target = min(scaled_target, 270);
1129
1130 const bool max_training = (training == 100);
1131 const bool hypothetical = !crawl_state.need_save ||
1132 (training != you.training[skill]);
1133
1134 const skill_diff diffs = skill_level_to_diffs(skill,
1135 (double) scaled_target / 10, training, false);
1136 const int level_diff = xp_to_level_diff(diffs.experience / 10, 10);
1137
1138 if (max_training)
1139 description += "At 100% training ";
1140 else if (!hypothetical)
1141 {
1142 description += make_stringf("At current training (%d%%) ",
1143 you.training[skill]);
1144 }
1145 else
1146 description += make_stringf("At a training level of %d%% ", training);
1147
1148 description += make_stringf(
1149 "you %sreach %d.%d in %s %d.%d XLs.",
1150 hypothetical ? "would " : "",
1151 scaled_target / 10, scaled_target % 10,
1152 (you.experience_level + (level_diff + 9) / 10) > 27
1153 ? "the equivalent of" : "about",
1154 level_diff / 10, level_diff % 10);
1155 if (you.wizard)
1156 {
1157 description += make_stringf("\n (%d xp, %d skp)",
1158 diffs.experience, diffs.skill_points);
1159 }
1160 return description;
1161 }
1162
1163 /**
1164 * Append two skill target descriptions: one for 100%, and one for the
1165 * current training rate.
1166 */
_append_skill_target_desc(string & description,skill_type skill,int scaled_target)1167 static void _append_skill_target_desc(string &description, skill_type skill,
1168 int scaled_target)
1169 {
1170 if (!you.has_mutation(MUT_DISTRIBUTED_TRAINING))
1171 description += "\n " + _skill_target_desc(skill, scaled_target, 100);
1172 if (you.training[skill] > 0 && you.training[skill] < 100)
1173 {
1174 description += "\n " + _skill_target_desc(skill, scaled_target,
1175 you.training[skill]);
1176 }
1177 }
1178
_append_weapon_stats(string & description,const item_def & item)1179 static void _append_weapon_stats(string &description, const item_def &item)
1180 {
1181 const int base_dam = property(item, PWPN_DAMAGE);
1182 const int ammo_type = fires_ammo_type(item);
1183 const int ammo_dam = ammo_type == MI_NONE ? 0 :
1184 ammo_type_damage(ammo_type);
1185 const skill_type skill = _item_training_skill(item);
1186 const int mindelay_skill = _item_training_target(item);
1187
1188 const bool could_set_target = _could_set_training_target(item, true);
1189
1190 if (skill == SK_SLINGS)
1191 {
1192 description += make_stringf("\nFiring bullets: Base damage: %d",
1193 base_dam +
1194 ammo_type_damage(MI_SLING_BULLET));
1195 }
1196
1197 if (item.base_type == OBJ_STAVES
1198 && is_useless_skill(staff_skill(static_cast<stave_type>(item.sub_type))))
1199 {
1200 description += make_stringf(
1201 "\nYour inability to study %s prevents you from drawing on the"
1202 " full power of this staff in melee.\n",
1203 skill_name(staff_skill(static_cast<stave_type>(item.sub_type))));
1204 }
1205
1206 description += make_stringf(
1207 "\nBase accuracy: %+d Base damage: %d Base attack delay: %.1f"
1208 "\nThis weapon's minimum attack delay (%.1f) is reached at skill level %d.",
1209 property(item, PWPN_HIT),
1210 base_dam + ammo_dam,
1211 (float) property(item, PWPN_SPEED) / 10,
1212 (float) weapon_min_delay(item, item_brand_known(item)) / 10,
1213 mindelay_skill / 10);
1214
1215 if (!is_useless_item(item))
1216 {
1217 description += "\n " + _your_skill_desc(skill,
1218 could_set_target && in_inventory(item), mindelay_skill);
1219 }
1220
1221 if (could_set_target)
1222 _append_skill_target_desc(description, skill, mindelay_skill);
1223 }
1224
_handedness_string(const item_def & item)1225 static string _handedness_string(const item_def &item)
1226 {
1227 const bool quad = you.has_mutation(MUT_QUADRUMANOUS);
1228 string handname = species::hand_name(you.species);
1229 if (quad)
1230 handname += "-pair";
1231
1232 string n;
1233 switch (you.hands_reqd(item))
1234 {
1235 case HANDS_ONE:
1236 n = "one";
1237 break;
1238 case HANDS_TWO:
1239 if (quad)
1240 handname = pluralise(handname);
1241 n = "two";
1242 break;
1243 }
1244
1245 if (quad)
1246 return make_stringf("It is a weapon for %s %s.", n.c_str(), handname.c_str());
1247 else
1248 {
1249 return make_stringf("It is a %s-%s%s weapon.", n.c_str(),
1250 handname.c_str(),
1251 ends_with(handname, "e") ? "d" : "ed");
1252 }
1253
1254 }
1255
_describe_weapon(const item_def & item,bool verbose)1256 static string _describe_weapon(const item_def &item, bool verbose)
1257 {
1258 string description;
1259
1260 description.reserve(200);
1261
1262 description = "";
1263
1264 if (verbose)
1265 {
1266 description += "\n";
1267 _append_weapon_stats(description, item);
1268 }
1269
1270 const int spec_ench = (is_artefact(item) || verbose)
1271 ? get_weapon_brand(item) : SPWPN_NORMAL;
1272 const int damtype = get_vorpal_type(item);
1273
1274 if (verbose)
1275 {
1276 switch (item_attack_skill(item))
1277 {
1278 case SK_POLEARMS:
1279 description += "\n\nIt can be evoked to extend its reach.";
1280 break;
1281 case SK_AXES:
1282 description += "\n\nIt hits all enemies adjacent to the wielder, "
1283 "dealing less damage to those not targeted.";
1284 break;
1285 case SK_LONG_BLADES:
1286 description += "\n\nIt can be used to riposte, swiftly "
1287 "retaliating against a missed attack.";
1288 break;
1289 case SK_SHORT_BLADES:
1290 {
1291 string adj = (item.sub_type == WPN_DAGGER) ? "extremely"
1292 : "particularly";
1293 description += "\n\nIt is " + adj + " good for stabbing"
1294 " helpless or unaware enemies.";
1295 }
1296 break;
1297 default:
1298 break;
1299 }
1300 }
1301
1302 // ident known & no brand but still glowing
1303 // TODO: deduplicate this with the code in item-name.cc
1304 const bool enchanted = get_equip_desc(item) && spec_ench == SPWPN_NORMAL
1305 && !item_ident(item, ISFLAG_KNOW_PLUSES);
1306
1307 const unrandart_entry *entry = nullptr;
1308 if (is_unrandom_artefact(item))
1309 entry = get_unrand_entry(item.unrand_idx);
1310 const bool skip_ego = is_unrandom_artefact(item)
1311 && entry && entry->flags & UNRAND_FLAG_SKIP_EGO;
1312
1313 // special weapon descrip
1314 if (item_type_known(item)
1315 && (spec_ench != SPWPN_NORMAL || enchanted)
1316 && !skip_ego)
1317 {
1318 description += "\n\n";
1319
1320 switch (spec_ench)
1321 {
1322 case SPWPN_FLAMING:
1323 if (is_range_weapon(item))
1324 description += "Any ammunition fired from it";
1325 else
1326 description += "It";
1327 description += " burns those it strikes, dealing additional fire "
1328 "damage.";
1329 if (!is_range_weapon(item) &&
1330 (damtype == DVORP_SLICING || damtype == DVORP_CHOPPING))
1331 {
1332 description += " Big, fiery blades are also staple "
1333 "armaments of hydra-hunters.";
1334 }
1335 break;
1336 case SPWPN_FREEZING:
1337 if (is_range_weapon(item))
1338 description += "Any ammunition fired from it";
1339 else
1340 description += "It";
1341 description += " freezes those it strikes, dealing additional cold "
1342 "damage. It can also slow down cold-blooded creatures.";
1343 break;
1344 case SPWPN_HOLY_WRATH:
1345 description += "It has been blessed by the Shining One";
1346 if (is_range_weapon(item))
1347 description += ", and any ammunition fired from it causes";
1348 else
1349 description += " to cause";
1350 description += " great damage to the undead and demons.";
1351 break;
1352 case SPWPN_ELECTROCUTION:
1353 if (is_range_weapon(item))
1354 description += "Any ammunition fired from it";
1355 else
1356 description += "It";
1357 description += " occasionally discharges a powerful burst of "
1358 "electricity upon striking a foe.";
1359 break;
1360 case SPWPN_VENOM:
1361 if (is_range_weapon(item))
1362 description += "Any ammunition fired from it";
1363 else
1364 description += "It";
1365 description += " poisons the flesh of those it strikes.";
1366 break;
1367 case SPWPN_PROTECTION:
1368 description += "It grants its wielder temporary protection when "
1369 "it strikes (+7 to AC).";
1370 break;
1371 case SPWPN_DRAINING:
1372 description += "A truly terrible weapon, it drains the life of "
1373 "any living foe it strikes.";
1374 break;
1375 case SPWPN_SPEED:
1376 description += "Attacks with this weapon are significantly faster.";
1377 break;
1378 case SPWPN_VORPAL:
1379 if (is_range_weapon(item))
1380 description += "Any ammunition fired from it";
1381 else
1382 description += "It";
1383 description += " inflicts extra damage upon your enemies.";
1384 break;
1385 case SPWPN_CHAOS:
1386 if (is_range_weapon(item))
1387 {
1388 description += "Each projectile launched from it has a "
1389 "different, random effect.";
1390 }
1391 else
1392 {
1393 description += "Each time it hits an enemy it has a "
1394 "different, random effect.";
1395 }
1396 break;
1397 case SPWPN_VAMPIRISM:
1398 description += "It occasionally heals its wielder for a portion "
1399 "of the damage dealt when it wounds a living foe.";
1400 break;
1401 case SPWPN_PAIN:
1402 description += "In the hands of one skilled in necromantic "
1403 "magic, it inflicts extra damage on living creatures.";
1404 break;
1405 case SPWPN_DISTORTION:
1406 description += "It warps and distorts space around it, and may "
1407 "blink, banish, or inflict extra damage upon those it strikes. "
1408 "Unwielding it can cause banishment or high damage.";
1409 break;
1410 case SPWPN_PENETRATION:
1411 description += "Any ammunition fired by it passes through the "
1412 "targets it hits, potentially hitting all targets in "
1413 "its path until it reaches maximum range.";
1414 break;
1415 case SPWPN_REAPING:
1416 description += "Any living foe damaged by it may be reanimated "
1417 "upon death as a zombie friendly to the wielder, with an "
1418 "increasing chance as more damage is dealt.";
1419 break;
1420 case SPWPN_ANTIMAGIC:
1421 description += "It reduces the magical energy of the wielder, "
1422 "and disrupts the spells and magical abilities of those it "
1423 "strikes. Natural abilities and divine invocations are not "
1424 "affected.";
1425 break;
1426 case SPWPN_SPECTRAL:
1427 description += "When its wielder attacks, the weapon's spirit "
1428 "leaps out and strikes again. The spirit shares a part of "
1429 "any damage it takes with its wielder.";
1430 break;
1431 case SPWPN_ACID:
1432 if (is_range_weapon(item))
1433 description += "Any ammunition fired from it";
1434 else
1435 description += "It";
1436 description += " is coated in acid, damaging and corroding those "
1437 "it strikes.";
1438 break;
1439 case SPWPN_NORMAL:
1440 ASSERT(enchanted);
1441 description += "It has no special brand (it is not flaming, "
1442 "freezing, etc), but is still enchanted in some way - "
1443 "positive or negative.";
1444 break;
1445 }
1446 }
1447
1448 if (you.duration[DUR_EXCRUCIATING_WOUNDS] && &item == you.weapon())
1449 {
1450 description += "\nIt is temporarily rebranded; it is actually ";
1451 if ((int) you.props[ORIGINAL_BRAND_KEY] == SPWPN_NORMAL)
1452 description += "an unbranded weapon.";
1453 else
1454 {
1455 brand_type original = static_cast<brand_type>(
1456 you.props[ORIGINAL_BRAND_KEY].get_int());
1457 description += article_a(
1458 weapon_brand_desc("weapon", item, false, original) + ".", true);
1459 }
1460 }
1461
1462 string art_desc = _artefact_descrip(item);
1463 if (!art_desc.empty())
1464 description += "\n\n" + art_desc;
1465
1466 if (verbose)
1467 {
1468 description += "\n\nThis ";
1469 if (is_unrandom_artefact(item))
1470 description += get_artefact_base_name(item);
1471 else
1472 description += "weapon";
1473 description += " falls into the";
1474
1475 const skill_type skill = item_attack_skill(item);
1476
1477 description +=
1478 make_stringf(" '%s' category. ",
1479 skill == SK_FIGHTING ? "buggy" : skill_name(skill));
1480
1481 // XX this is shown for felids, does that actually make sense?
1482 description += _handedness_string(item);
1483
1484 if (!you.could_wield(item, true, true) && crawl_state.need_save)
1485 description += "\nIt is too large for you to wield.";
1486 }
1487
1488 if (!is_artefact(item))
1489 {
1490 if (item_ident(item, ISFLAG_KNOW_PLUSES) && item.plus >= MAX_WPN_ENCHANT)
1491 description += "\nIt cannot be enchanted further.";
1492 else
1493 {
1494 description += "\nIt can be maximally enchanted to +"
1495 + to_string(MAX_WPN_ENCHANT) + ".";
1496 }
1497 }
1498
1499 return description;
1500 }
1501
_describe_ammo(const item_def & item)1502 static string _describe_ammo(const item_def &item)
1503 {
1504 string description;
1505
1506 description.reserve(64);
1507
1508 const bool can_launch = has_launcher(item);
1509 const bool can_throw = is_throwable(nullptr, item);
1510
1511 if (item.brand && item_type_known(item))
1512 {
1513 description += "\n\n";
1514
1515 string threw_or_fired;
1516 if (can_throw)
1517 {
1518 threw_or_fired += "threw";
1519 if (can_launch)
1520 threw_or_fired += " or ";
1521 }
1522 if (can_launch)
1523 threw_or_fired += "fired";
1524
1525 switch (item.brand)
1526 {
1527 #if TAG_MAJOR_VERSION == 34
1528 case SPMSL_FLAME:
1529 description += "It burns those it strikes, causing extra injury "
1530 "to most foes and up to half again as much damage against "
1531 "particularly susceptible opponents. Compared to normal "
1532 "ammo, it is twice as likely to be destroyed on impact.";
1533 break;
1534 case SPMSL_FROST:
1535 description += "It freezes those it strikes, causing extra injury "
1536 "to most foes and up to half again as much damage against "
1537 "particularly susceptible opponents. It can also slow down "
1538 "cold-blooded creatures. Compared to normal ammo, it is "
1539 "twice as likely to be destroyed on impact.";
1540 break;
1541 #endif
1542 case SPMSL_CHAOS:
1543 description += "When ";
1544
1545 if (can_throw)
1546 {
1547 description += "thrown, ";
1548 if (can_launch)
1549 description += "or ";
1550 }
1551
1552 if (can_launch)
1553 description += "fired from an appropriate launcher, ";
1554
1555 description += "it has a random effect.";
1556 break;
1557 case SPMSL_POISONED:
1558 description += "It is coated with poison.";
1559 break;
1560 case SPMSL_CURARE:
1561 description += "It is tipped with a substance that causes "
1562 "asphyxiation, dealing direct damage as well as "
1563 "poisoning and slowing those it strikes.\n"
1564 "It is twice as likely to be destroyed on impact as "
1565 "other darts.";
1566 break;
1567 case SPMSL_FRENZY:
1568 description += "It is tipped with a substance that sends those it "
1569 "hits into a mindless rage, attacking friend and "
1570 "foe alike.\n"
1571 "The chance of successfully applying its effect "
1572 "increases with Throwing and Stealth skill.";
1573
1574 break;
1575 case SPMSL_BLINDING:
1576 description += "It is tipped with a substance that causes "
1577 "blindness and brief confusion.\n"
1578 "The chance of successfully applying its effect "
1579 "increases with Throwing and Stealth skill.";
1580 break;
1581 case SPMSL_DISPERSAL:
1582 description += "It causes any target it hits to blink, with a "
1583 "tendency towards blinking further away from the "
1584 "one who " + threw_or_fired + " it.";
1585 break;
1586 case SPMSL_SILVER:
1587 description += "It deals increased damage compared to normal ammo "
1588 "and substantially increased damage to chaotic "
1589 "and magically transformed beings. It also inflicts "
1590 "extra damage against mutated beings, according to "
1591 "how mutated they are.";
1592 break;
1593 }
1594 }
1595
1596 const int dam = property(item, PWPN_DAMAGE);
1597 const bool player_throwable = is_throwable(&you, item);
1598 if (player_throwable)
1599 {
1600 const int throw_delay = (10 + dam / 2);
1601 const int target_skill = _item_training_target(item);
1602 const bool could_set_target = _could_set_training_target(item, true);
1603
1604 description += make_stringf(
1605 "\nBase damage: %d Base attack delay: %.1f"
1606 "\nThis projectile's minimum attack delay (%.1f) "
1607 "is reached at skill level %d.",
1608 dam,
1609 (float) throw_delay / 10,
1610 (float) FASTEST_PLAYER_THROWING_SPEED / 10,
1611 target_skill / 10
1612 );
1613
1614 if (!is_useless_item(item))
1615 {
1616 description += "\n " +
1617 _your_skill_desc(SK_THROWING,
1618 could_set_target && in_inventory(item), target_skill);
1619 }
1620 if (could_set_target)
1621 _append_skill_target_desc(description, SK_THROWING, target_skill);
1622 }
1623
1624 if (ammo_always_destroyed(item))
1625 description += "\n\nIt is always destroyed on impact.";
1626 else if (!ammo_never_destroyed(item))
1627 description += "\n\nIt may be destroyed on impact.";
1628
1629 return description;
1630 }
1631
_warlock_mirror_reflect_desc()1632 static string _warlock_mirror_reflect_desc()
1633 {
1634 const int SH = crawl_state.need_save ? player_shield_class() : 0;
1635 const int reflect_chance = 100 * SH / omnireflect_chance_denom(SH);
1636 return "\n\nWith your current SH, it has a " + to_string(reflect_chance) +
1637 "% chance to reflect attacks against your willpower and other "
1638 "normally unblockable effects.";
1639 }
1640
_describe_point_change(int points)1641 static string _describe_point_change(int points)
1642 {
1643 string point_diff_description;
1644
1645 point_diff_description += make_stringf("%s by %d",
1646 points > 0 ? "increase" : "decrease",
1647 abs(points));
1648
1649 return point_diff_description;
1650 }
1651
_describe_point_diff(int original,int changed)1652 static string _describe_point_diff(int original,
1653 int changed)
1654 {
1655 string description;
1656
1657 int difference = changed - original;
1658
1659 if (difference == 0)
1660 return "remain unchanged.";
1661
1662 description += _describe_point_change(difference);
1663 description += " (";
1664 description += to_string(original);
1665 description += " -> ";
1666 description += to_string(changed);
1667 description += ").";
1668
1669 return description;
1670 }
1671
_armour_ac_sub_change_description(const item_def & item)1672 static string _armour_ac_sub_change_description(const item_def &item)
1673 {
1674 string description;
1675
1676 description.reserve(100);
1677
1678
1679 description += "\n\nIf you switch to wearing this armour,"
1680 " your AC would ";
1681
1682 int you_ac_with_this_item =
1683 you.armour_class_with_one_sub(item);
1684
1685 description += _describe_point_diff(you.armour_class(),
1686 you_ac_with_this_item);
1687
1688 return description;
1689 }
1690
_armour_ac_remove_change_description(const item_def & item)1691 static string _armour_ac_remove_change_description(const item_def &item)
1692 {
1693 string description;
1694
1695 description += "\n\nIf you remove this armour,"
1696 " your AC would ";
1697
1698 int you_ac_without_item =
1699 you.armour_class_with_one_removal(item);
1700
1701 description += _describe_point_diff(you.armour_class(),
1702 you_ac_without_item);
1703
1704 return description;
1705 }
1706
_you_are_wearing_item(const item_def & item)1707 static bool _you_are_wearing_item(const item_def &item)
1708 {
1709 return get_equip_slot(&item) != EQ_NONE;
1710 }
1711
_armour_ac_change(const item_def & item)1712 static string _armour_ac_change(const item_def &item)
1713 {
1714 string description;
1715
1716 if (!_you_are_wearing_item(item))
1717 description = _armour_ac_sub_change_description(item);
1718 else
1719 description = _armour_ac_remove_change_description(item);
1720
1721 return description;
1722 }
1723
_item_ego_desc(special_armour_type ego)1724 static const char* _item_ego_desc(special_armour_type ego)
1725 {
1726 switch (ego)
1727 {
1728 case SPARM_FIRE_RESISTANCE:
1729 return "it protects its wearer from fire.";
1730 case SPARM_COLD_RESISTANCE:
1731 return "it protects its wearer from cold.";
1732 case SPARM_POISON_RESISTANCE:
1733 return "it protects its wearer from poison.";
1734 case SPARM_SEE_INVISIBLE:
1735 return "it allows its wearer to see invisible things.";
1736 case SPARM_INVISIBILITY:
1737 return "when activated, it grants its wearer temporary "
1738 "invisibility, but also drains their maximum health.";
1739 case SPARM_STRENGTH:
1740 return "it increases the strength of its wearer (Str +3).";
1741 case SPARM_DEXTERITY:
1742 return "it increases the dexterity of its wearer (Dex +3).";
1743 case SPARM_INTELLIGENCE:
1744 return "it increases the intelligence of its wearer (Int +3).";
1745 case SPARM_PONDEROUSNESS:
1746 return "it is very cumbersome, slowing its wearer's movement.";
1747 case SPARM_FLYING:
1748 return "it grants its wearer flight.";
1749 case SPARM_WILLPOWER:
1750 return "it increases its wearer's willpower, protecting "
1751 "against certain magical effects.";
1752 case SPARM_PROTECTION:
1753 return "it protects its wearer from most sources of damage (AC +3).";
1754 case SPARM_STEALTH:
1755 return "it enhances the stealth of its wearer.";
1756 case SPARM_RESISTANCE:
1757 return "it protects its wearer from the effects of both fire and cold.";
1758 case SPARM_POSITIVE_ENERGY:
1759 return "it protects its wearer from the effects of negative energy.";
1760 case SPARM_ARCHMAGI:
1761 return "it increases the power of its wearer's magical spells.";
1762 case SPARM_PRESERVATION:
1763 return "it protects its wearer from the effects of acid and corrosion.";
1764 case SPARM_REFLECTION:
1765 return "it reflects blocked missile attacks back in the "
1766 "direction they came from.";
1767 case SPARM_SPIRIT_SHIELD:
1768 return "it causes incoming damage to be divided between "
1769 "the wearer's reserves of health and magic.";
1770 case SPARM_ARCHERY:
1771 return "it improves its wearer's accuracy and damage with "
1772 "ranged weapons, such as bows and javelins (Slay +4).";
1773 case SPARM_REPULSION:
1774 return "it protects its wearer by repelling missiles.";
1775 #if TAG_MAJOR_VERSION == 34
1776 case SPARM_CLOUD_IMMUNE:
1777 return "it does nothing special.";
1778 #endif
1779 case SPARM_HARM:
1780 return "it increases damage dealt and taken.";
1781 case SPARM_SHADOWS:
1782 return "it reduces the distance the wearer can be seen at "
1783 "and can see.";
1784 case SPARM_RAMPAGING:
1785 return "its wearer takes one free step when moving towards enemies.";
1786 default:
1787 return "it makes the wearer crave the taste of eggplant.";
1788 }
1789 }
1790
_describe_armour(const item_def & item,bool verbose)1791 static string _describe_armour(const item_def &item, bool verbose)
1792 {
1793 string description;
1794
1795 description.reserve(300);
1796
1797 if (verbose)
1798 {
1799 if (is_shield(item))
1800 {
1801 const int target_skill = _item_training_target(item);
1802 description += "\n";
1803 description += "\nBase shield rating: "
1804 + to_string(property(item, PARM_AC));
1805 const bool could_set_target = _could_set_training_target(item, true);
1806
1807 if (!is_useless_item(item))
1808 {
1809 description += " Skill to remove penalty: "
1810 + make_stringf("%d.%d", target_skill / 10,
1811 target_skill % 10);
1812
1813 if (crawl_state.need_save)
1814 {
1815 description += "\n "
1816 + _your_skill_desc(SK_SHIELDS,
1817 could_set_target && in_inventory(item), target_skill);
1818 }
1819 else
1820 description += "\n";
1821 if (could_set_target)
1822 {
1823 _append_skill_target_desc(description, SK_SHIELDS,
1824 target_skill);
1825 }
1826 }
1827
1828 if (is_unrandom_artefact(item, UNRAND_WARLOCK_MIRROR))
1829 description += _warlock_mirror_reflect_desc();
1830 }
1831 else
1832 {
1833 const int evp = property(item, PARM_EVASION);
1834 description += "\n\nBase armour rating: "
1835 + to_string(property(item, PARM_AC));
1836 if (get_armour_slot(item) == EQ_BODY_ARMOUR)
1837 {
1838 description += " Encumbrance rating: "
1839 + to_string(-evp / 10);
1840 }
1841 // Bardings reduce evasion by a fixed amount, and don't have any of
1842 // the other effects of encumbrance.
1843 else if (evp)
1844 {
1845 description += " Evasion: "
1846 + to_string(evp / 30);
1847 }
1848 }
1849 }
1850
1851 const special_armour_type ego = get_armour_ego_type(item);
1852
1853 const unrandart_entry *entry = nullptr;
1854 if (is_unrandom_artefact(item))
1855 entry = get_unrand_entry(item.unrand_idx);
1856 const bool skip_ego = is_unrandom_artefact(item)
1857 && entry && entry->flags & UNRAND_FLAG_SKIP_EGO;
1858
1859 // Only give a description for armour with a known ego.
1860 if (ego != SPARM_NORMAL && item_type_known(item) && verbose && !skip_ego)
1861 {
1862 description += "\n\n";
1863
1864 if (is_artefact(item))
1865 {
1866 // Make this match the formatting in _randart_descrip,
1867 // since instead of the item being named something like
1868 // 'cloak of invisiblity', it's 'the cloak of the Snail (+Inv, ...)'
1869 string name = string(armour_ego_name(item, true));
1870 name = chop_string(name, MAX_ARTP_NAME_LEN - 1, false) + ":";
1871 name.append(MAX_ARTP_NAME_LEN - name.length(), ' ');
1872 description += name;
1873 }
1874 else
1875 description += "'Of " + string(armour_ego_name(item, false)) + "': ";
1876
1877 string ego_desc = string(_item_ego_desc(ego));
1878 if (is_artefact(item))
1879 ego_desc = " " + uppercase_first(ego_desc);
1880 description += ego_desc;
1881 }
1882
1883 string art_desc = _artefact_descrip(item);
1884 if (!art_desc.empty())
1885 {
1886 // Only add a section break if we didn't already add one before
1887 // printing an ego-based property.
1888 if (ego == SPARM_NORMAL || !verbose || skip_ego)
1889 description += "\n";
1890 description += "\n" + art_desc;
1891 }
1892
1893 if (!is_artefact(item))
1894 {
1895 const int max_ench = armour_max_enchant(item);
1896 if (max_ench > 0)
1897 {
1898 if (item.plus < max_ench || !item_ident(item, ISFLAG_KNOW_PLUSES))
1899 {
1900 description += "\n\nIt can be maximally enchanted to +"
1901 + to_string(max_ench) + ".";
1902 }
1903 else
1904 description += "\n\nIt cannot be enchanted further.";
1905 }
1906
1907 }
1908
1909 // Only displayed if the player exists (not for item lookup from the menu).
1910 if (crawl_state.need_save
1911 && can_wear_armour(item, false, true)
1912 && item_ident(item, ISFLAG_KNOW_PLUSES)
1913 && !is_shield(item))
1914 {
1915 description += _armour_ac_change(item);
1916 }
1917
1918 return description;
1919 }
1920
_describe_lignify_ac()1921 static string _describe_lignify_ac()
1922 {
1923 const Form* tree_form = get_form(transformation::tree);
1924 vector<const item_def *> treeform_items;
1925
1926 for (auto item : you.get_armour_items())
1927 if (tree_form->slot_available(get_equip_slot(item)))
1928 treeform_items.push_back(item);
1929
1930 const int treeform_ac =
1931 (you.base_ac_with_specific_items(100, treeform_items)
1932 - you.racial_ac(true) - you.ac_changes_from_mutations()
1933 - get_form()->get_ac_bonus() + tree_form->get_ac_bonus()) / 100;
1934
1935 return make_stringf("If you quaff this potion your AC would be %d.",
1936 treeform_ac);
1937 }
1938
_describe_jewellery(const item_def & item,bool verbose)1939 static string _describe_jewellery(const item_def &item, bool verbose)
1940 {
1941 string description;
1942
1943 description.reserve(200);
1944
1945 if (verbose && !is_artefact(item)
1946 && item_ident(item, ISFLAG_KNOW_PLUSES))
1947 {
1948 // Explicit description of ring or amulet power.
1949 if (item.sub_type == AMU_REFLECTION)
1950 {
1951 description += make_stringf("\n\nIt affects your shielding (%+d).",
1952 AMU_REFLECT_SH / 2);
1953 }
1954 else if (item.plus != 0)
1955 {
1956 switch (item.sub_type)
1957 {
1958 case RING_PROTECTION:
1959 description += make_stringf("\n\nIt affects your AC (%+d).",
1960 item.plus);
1961 break;
1962
1963 case RING_EVASION:
1964 description += make_stringf("\n\nIt affects your evasion (%+d).",
1965 item.plus);
1966 break;
1967
1968 case RING_STRENGTH:
1969 description += make_stringf("\n\nIt affects your strength (%+d).",
1970 item.plus);
1971 break;
1972
1973 case RING_INTELLIGENCE:
1974 description += make_stringf("\n\nIt affects your intelligence (%+d).",
1975 item.plus);
1976 break;
1977
1978 case RING_DEXTERITY:
1979 description += make_stringf("\n\nIt affects your dexterity (%+d).",
1980 item.plus);
1981 break;
1982
1983 case RING_SLAYING:
1984 description += make_stringf("\n\nIt affects your accuracy and"
1985 " damage with ranged weapons and melee (%+d).",
1986 item.plus);
1987 break;
1988
1989 default:
1990 break;
1991 }
1992 }
1993 }
1994
1995 // Artefact properties.
1996 string art_desc = _artefact_descrip(item);
1997 if (!art_desc.empty())
1998 description += "\n\n" + art_desc;
1999
2000 return description;
2001 }
2002
_describe_item_curse(const item_def & item)2003 static string _describe_item_curse(const item_def &item)
2004 {
2005 if (!item.props.exists(CURSE_KNOWLEDGE_KEY))
2006 return "\nIt has a curse placed upon it.";
2007
2008 const CrawlVector& curses = item.props[CURSE_KNOWLEDGE_KEY].get_vector();
2009
2010 if (curses.empty())
2011 return "\nIt has a curse placed upon it.";
2012
2013 ostringstream desc;
2014
2015 desc << "\nIt has a curse which improves the following skills:\n";
2016 desc << comma_separated_fn(curses.begin(), curses.end(), desc_curse_skills,
2017 ".\n", ".\n") << ".";
2018
2019 return desc.str();
2020 }
2021
is_dumpable_artefact(const item_def & item)2022 bool is_dumpable_artefact(const item_def &item)
2023 {
2024 return is_known_artefact(item) && item_ident(item, ISFLAG_KNOW_PROPERTIES);
2025 }
2026
2027 /**
2028 * Describe a specified item.
2029 *
2030 * @param item The specified item.
2031 * @param verbose Controls various switches for the length of the description.
2032 * @param dump This controls which style the name is shown in.
2033 * @param lookup If true, the name is not shown at all.
2034 * If either of those two are true, the DB description is not shown.
2035 * @return a string with the name, db desc, and some other data.
2036 */
get_item_description(const item_def & item,bool verbose,bool dump,bool lookup)2037 string get_item_description(const item_def &item, bool verbose,
2038 bool dump, bool lookup)
2039 {
2040 ostringstream description;
2041
2042 #ifdef DEBUG_DIAGNOSTICS
2043 if (!dump && !you.suppress_wizard)
2044 {
2045 description << setfill('0');
2046 description << "\n\n"
2047 << "base: " << static_cast<int>(item.base_type)
2048 << " sub: " << static_cast<int>(item.sub_type)
2049 << " plus: " << item.plus << " plus2: " << item.plus2
2050 << " special: " << item.special
2051 << "\n"
2052 << "quant: " << item.quantity
2053 << " rnd?: " << static_cast<int>(item.rnd)
2054 << " flags: " << hex << setw(8) << item.flags
2055 << dec << "\n"
2056 << "x: " << item.pos.x << " y: " << item.pos.y
2057 << " link: " << item.link
2058 << " slot: " << item.slot
2059 << " ident_type: "
2060 << get_ident_type(item)
2061 << "\nannotate: "
2062 << stash_annotate_item(STASH_LUA_SEARCH_ANNOTATE, &item);
2063 }
2064 #endif
2065
2066 if (verbose || (item.base_type != OBJ_WEAPONS
2067 && item.base_type != OBJ_ARMOUR
2068 && item.base_type != OBJ_BOOKS))
2069 {
2070 description << "\n\n";
2071
2072 bool need_base_desc = !lookup;
2073
2074 if (dump)
2075 {
2076 description << "["
2077 << item.name(DESC_DBNAME, true, false, false)
2078 << "]";
2079 need_base_desc = false;
2080 }
2081 else if (is_unrandom_artefact(item) && item_type_known(item))
2082 {
2083 const string desc = getLongDescription(get_artefact_name(item));
2084 if (!desc.empty())
2085 {
2086 description << desc;
2087 need_base_desc = false;
2088 description.seekp((streamoff)-1, ios_base::cur);
2089 description << " ";
2090 }
2091 }
2092 // Randart jewellery properties will be listed later,
2093 // just describe artefact status here.
2094 else if (is_artefact(item) && item_type_known(item)
2095 && item.base_type == OBJ_JEWELLERY)
2096 {
2097 description << "It is an ancient artefact.";
2098 need_base_desc = false;
2099 }
2100
2101 if (need_base_desc)
2102 {
2103 string db_name = item.name(DESC_DBNAME, true, false, false);
2104 string db_desc = getLongDescription(db_name);
2105
2106 if (db_desc.empty())
2107 {
2108 if (item_type_removed(item.base_type, item.sub_type))
2109 description << "This item has been removed.\n";
2110 else if (item_type_known(item))
2111 {
2112 description << "[ERROR: no desc for item name '" << db_name
2113 << "']. Perhaps this item has been removed?\n";
2114 }
2115 else
2116 {
2117 description << uppercase_first(item.name(DESC_A, true,
2118 false, false));
2119 description << ".\n";
2120 }
2121 }
2122 else
2123 description << db_desc;
2124
2125 // Get rid of newline at end of description; in most cases we
2126 // will be adding "\n\n" immediately, and we want only one,
2127 // not two, blank lines. This allow allows the "unpleasant"
2128 // message for chunks to appear on the same line.
2129 description.seekp((streamoff)-1, ios_base::cur);
2130 description << " ";
2131 }
2132 }
2133
2134 bool need_extra_line = true;
2135 string desc;
2136 switch (item.base_type)
2137 {
2138 // Weapons, armour, jewellery, books might be artefacts.
2139 case OBJ_WEAPONS:
2140 desc = _describe_weapon(item, verbose);
2141 if (desc.empty())
2142 need_extra_line = false;
2143 else
2144 description << desc;
2145 break;
2146
2147 case OBJ_ARMOUR:
2148 desc = _describe_armour(item, verbose);
2149 if (desc.empty())
2150 need_extra_line = false;
2151 else
2152 description << desc;
2153 break;
2154
2155 case OBJ_JEWELLERY:
2156 desc = _describe_jewellery(item, verbose);
2157 if (desc.empty())
2158 need_extra_line = false;
2159 else
2160 description << desc;
2161 break;
2162
2163 case OBJ_BOOKS:
2164 if (!verbose && is_random_artefact(item))
2165 {
2166 desc += describe_item_spells(item);
2167 if (desc.empty())
2168 need_extra_line = false;
2169 else
2170 description << desc;
2171 }
2172 break;
2173
2174 case OBJ_MISSILES:
2175 description << _describe_ammo(item);
2176 break;
2177
2178 case OBJ_CORPSES:
2179 break;
2180
2181 case OBJ_STAVES:
2182 {
2183 string stats = "\n";
2184 _append_weapon_stats(stats, item);
2185 description << stats;
2186 }
2187 description << "\n\nIt falls into the 'Staves' category. ";
2188 description << _handedness_string(item);
2189 break;
2190
2191 case OBJ_MISCELLANY:
2192 if (item.sub_type == MISC_ZIGGURAT && you.zigs_completed)
2193 {
2194 const int zigs = you.zigs_completed;
2195 description << "\n\nIt is surrounded by a "
2196 << (zigs >= 27 ? "blinding " : // just plain silly
2197 zigs >= 9 ? "dazzling " :
2198 zigs >= 3 ? "bright " :
2199 "gentle ")
2200 << "glow.";
2201 }
2202 if (is_xp_evoker(item))
2203 {
2204 description << "\n\nOnce "
2205 << (item.sub_type == MISC_LIGHTNING_ROD
2206 ? "all charges have been used"
2207 : "activated")
2208 << ", this device "
2209 << (!item_is_horn_of_geryon(item) ?
2210 "and all other devices of its kind are " : "is ")
2211 << "rendered temporarily inert. However, "
2212 << (!item_is_horn_of_geryon(item) ? "they recharge " : "it recharges ")
2213 << "as you gain experience."
2214 << (!evoker_charges(item.sub_type) ?
2215 " The device is presently inert." : "");
2216 }
2217 break;
2218
2219 case OBJ_POTIONS:
2220 if (verbose && item_type_known(item))
2221 {
2222 if (item.sub_type == POT_LIGNIFY)
2223 description << "\n\n" + _describe_lignify_ac();
2224 else if (item.sub_type == POT_CANCELLATION)
2225 {
2226 if (player_is_cancellable())
2227 {
2228 description << "\n\nIf you drink this now, you will no longer be " <<
2229 describe_player_cancellation() << ".";
2230 }
2231 else
2232 description << "\n\nDrinking this now will have no effect.";
2233 }
2234 }
2235 break;
2236
2237 case OBJ_WANDS:
2238 if (item_type_known(item))
2239 {
2240 description << "\n";
2241
2242 const spell_type spell = spell_in_wand(static_cast<wand_type>(item.sub_type));
2243
2244 const string damage_str = spell_damage_string(spell, true);
2245 if (damage_str != "")
2246 description << "\nDamage: " << damage_str;
2247
2248 description << "\nNoise: " << spell_noise_string(spell);
2249 }
2250 break;
2251
2252 case OBJ_SCROLLS:
2253 case OBJ_ORBS:
2254 case OBJ_GOLD:
2255 case OBJ_RUNES:
2256
2257 #if TAG_MAJOR_VERSION == 34
2258 case OBJ_FOOD:
2259 case OBJ_RODS:
2260 #endif
2261 // No extra processing needed for these item types.
2262 break;
2263
2264 default:
2265 die("Bad item class");
2266 }
2267
2268 if (!verbose && item.cursed())
2269 description << _describe_item_curse(item);
2270 else
2271 {
2272 if (verbose)
2273 {
2274 if (need_extra_line)
2275 description << "\n";
2276 if (item.cursed())
2277 description << _describe_item_curse(item);
2278
2279 if (is_artefact(item))
2280 {
2281 if (item.base_type == OBJ_ARMOUR
2282 || item.base_type == OBJ_WEAPONS)
2283 {
2284 description << "\nThis ancient artefact cannot be changed "
2285 "by magic or mundane means.";
2286 }
2287 // Randart jewellery has already displayed this line.
2288 else if (item.base_type != OBJ_JEWELLERY
2289 || (item_type_known(item) && is_unrandom_artefact(item)))
2290 {
2291 description << "\nIt is an ancient artefact.";
2292 }
2293 }
2294 }
2295 }
2296
2297 if (god_hates_item(item))
2298 {
2299 description << "\n\n" << uppercase_first(god_name(you.religion))
2300 << " disapproves of the use of such an item.";
2301 }
2302
2303 if (verbose && origin_describable(item))
2304 description << "\n" << origin_desc(item) << ".";
2305
2306 // This information is obscure and differs per-item, so looking it up in
2307 // a docs file you don't know to exist is tedious.
2308 if (verbose)
2309 {
2310 description << "\n\n" << "Stash search prefixes: "
2311 << userdef_annotate_item(STASH_LUA_SEARCH_ANNOTATE, &item);
2312 string menu_prefix = item_prefix(item, false);
2313 if (!menu_prefix.empty())
2314 description << "\nMenu/colouring prefixes: " << menu_prefix;
2315 }
2316
2317 return description.str();
2318 }
2319
get_cloud_desc(cloud_type cloud,bool include_title)2320 string get_cloud_desc(cloud_type cloud, bool include_title)
2321 {
2322 if (cloud == CLOUD_NONE)
2323 return "";
2324 const string cl_name = cloud_type_name(cloud);
2325 const string cl_desc = getLongDescription(cl_name + " cloud");
2326
2327 string ret;
2328 if (include_title)
2329 ret = "A cloud of " + cl_name + (cl_desc.empty() ? "." : ".\n\n");
2330 ret += cl_desc + extra_cloud_info(cloud);
2331 return ret;
2332 }
2333
2334 typedef struct {
2335 string title;
2336 string description;
2337 tile_def tile;
2338 } extra_feature_desc;
2339
_get_feature_extra_descs(const coord_def & pos)2340 static vector<extra_feature_desc> _get_feature_extra_descs(const coord_def &pos)
2341 {
2342 vector<extra_feature_desc> ret;
2343 const dungeon_feature_type feat = env.map_knowledge(pos).feat();
2344
2345 if (feat_is_tree(feat) && env.forest_awoken_until)
2346 {
2347 ret.push_back({
2348 "Awoken.",
2349 getLongDescription("awoken"),
2350 tile_def(TILE_AWOKEN_OVERLAY)
2351 });
2352 }
2353 if (feat_is_wall(feat) && env.map_knowledge(pos).flags & MAP_ICY)
2354 {
2355 ret.push_back({
2356 "A covering of ice.",
2357 getLongDescription("ice covered"),
2358 tile_def(TILE_FLOOR_ICY)
2359 });
2360 }
2361 else if (!feat_is_solid(feat))
2362 {
2363 if (haloed(pos) && !umbraed(pos))
2364 {
2365 ret.push_back({
2366 "A halo.",
2367 getLongDescription("haloed"),
2368 tile_def(TILE_HALO_RANGE)
2369 });
2370 }
2371 if (umbraed(pos) && !haloed(pos))
2372 {
2373 ret.push_back({
2374 "An umbra.",
2375 getLongDescription("umbraed"),
2376 tile_def(TILE_UMBRA)
2377 });
2378 }
2379 if (liquefied(pos))
2380 {
2381 ret.push_back({
2382 "Liquefied ground.",
2383 getLongDescription("liquefied"),
2384 tile_def(TILE_LIQUEFACTION)
2385 });
2386 }
2387 if (disjunction_haloed(pos))
2388 {
2389 ret.push_back({
2390 "Translocational energy.",
2391 getLongDescription("disjunction haloed"),
2392 tile_def(TILE_DISJUNCT)
2393 });
2394 }
2395 }
2396 if (const auto cloud = env.map_knowledge(pos).cloudinfo())
2397 {
2398 ret.push_back({
2399 "A cloud of " + cloud_type_name(cloud->type) + ".",
2400 get_cloud_desc(cloud->type, false),
2401 tile_def(tileidx_cloud(*cloud)),
2402 });
2403 }
2404 return ret;
2405 }
2406
get_feature_desc(const coord_def & pos,describe_info & inf,bool include_extra)2407 void get_feature_desc(const coord_def &pos, describe_info &inf, bool include_extra)
2408 {
2409 dungeon_feature_type feat = env.map_knowledge(pos).feat();
2410
2411 string desc = feature_description_at(pos, false, DESC_A);
2412 string db_name = feat == DNGN_ENTER_SHOP ? "a shop" : desc;
2413 strip_suffix(db_name, " (summoned)");
2414 string long_desc = getLongDescription(db_name);
2415
2416 inf.title = uppercase_first(desc);
2417 if (!ends_with(desc, ".") && !ends_with(desc, "!")
2418 && !ends_with(desc, "?"))
2419 {
2420 inf.title += ".";
2421 }
2422
2423 const string marker_desc =
2424 env.markers.property_at(pos, MAT_ANY, "feature_description_long");
2425
2426 // suppress this if the feature changed out of view
2427 if (!marker_desc.empty() && env.grid(pos) == feat)
2428 long_desc += marker_desc;
2429
2430 // Display branch descriptions on the entries to those branches.
2431 if (feat_is_stair(feat))
2432 {
2433 for (branch_iterator it; it; ++it)
2434 {
2435 if (it->entry_stairs == feat)
2436 {
2437 long_desc += "\n";
2438 long_desc += getLongDescription(it->shortname);
2439 break;
2440 }
2441 }
2442
2443 if (feat_is_stone_stair(feat) || feat_is_escape_hatch(feat))
2444 {
2445 if (is_unknown_stair(pos))
2446 {
2447 long_desc += "\nYou have not yet explored it and cannot tell ";
2448 long_desc += "where it leads.";
2449 }
2450 else
2451 {
2452 long_desc +=
2453 make_stringf("\nYou can view the location it leads to by "
2454 "examining it with <w>%s</w> and pressing "
2455 "<w>%s</w>.",
2456 command_to_string(CMD_DISPLAY_MAP).c_str(),
2457 command_to_string(
2458 feat_stair_direction(feat) ==
2459 CMD_GO_UPSTAIRS ? CMD_MAP_PREV_LEVEL
2460 : CMD_MAP_NEXT_LEVEL).c_str());
2461 }
2462 }
2463 }
2464
2465 // mention the ability to pray at altars
2466 if (feat_is_altar(feat))
2467 {
2468 long_desc +=
2469 make_stringf("\n(Pray here with <w>%s</w> to learn more.)\n",
2470 command_to_string(CMD_GO_DOWNSTAIRS).c_str());
2471 }
2472
2473 // mention that permanent trees are usually flammable
2474 // (expect for autumnal trees in Wucad Mu's Monastery)
2475 if (feat_is_flammable(feat) && !is_temp_terrain(pos)
2476 && env.markers.property_at(pos, MAT_ANY, "veto_destroy") != "veto")
2477 {
2478 if (feat == DNGN_TREE)
2479 long_desc += "\n" + getLongDescription("tree burning");
2480 else if (feat == DNGN_MANGROVE)
2481 long_desc += "\n" + getLongDescription("mangrove burning");
2482 else if (feat == DNGN_DEMONIC_TREE)
2483 long_desc += "\n" + getLongDescription("demonic tree burning");
2484 }
2485
2486 // mention that diggable walls are
2487 if (feat_is_diggable(feat)
2488 && env.markers.property_at(pos, MAT_ANY, "veto_destroy") != "veto")
2489 {
2490 long_desc += "\nIt can be dug through.";
2491 }
2492
2493 inf.body << long_desc;
2494
2495 if (include_extra)
2496 {
2497 const auto extra_descs = _get_feature_extra_descs(pos);
2498 for (const auto &d : extra_descs)
2499 inf.body << (d.title == extra_descs.back().title ? "" : "\n") << d.description;
2500 }
2501
2502 inf.quote = getQuoteString(db_name);
2503 }
2504
describe_feature_wide(const coord_def & pos)2505 void describe_feature_wide(const coord_def& pos)
2506 {
2507 typedef struct {
2508 string title, body, quote;
2509 tile_def tile;
2510 } feat_info;
2511
2512 vector<feat_info> feats;
2513
2514 {
2515 describe_info inf;
2516 get_feature_desc(pos, inf, false);
2517 feat_info f = { "", "", "", tile_def(TILEG_TODO)};
2518 f.title = inf.title;
2519 f.body = trimmed_string(inf.body.str());
2520 #ifdef USE_TILE
2521 tileidx_t tile = tileidx_feature(pos);
2522 apply_variations(tile_env.flv(pos), &tile, pos);
2523 f.tile = tile_def(tile);
2524 #endif
2525 f.quote = trimmed_string(inf.quote);
2526 feats.emplace_back(f);
2527 }
2528 auto extra_descs = _get_feature_extra_descs(pos);
2529 for (const auto &desc : extra_descs)
2530 {
2531 feat_info f = { "", "", "", tile_def(TILEG_TODO)};
2532 f.title = desc.title;
2533 f.body = trimmed_string(desc.description);
2534 f.tile = desc.tile;
2535 feats.emplace_back(f);
2536 }
2537 if (crawl_state.game_is_hints())
2538 {
2539 string hint_text = trimmed_string(hints_describe_pos(pos.x, pos.y));
2540 if (!hint_text.empty())
2541 {
2542 feat_info f = { "", "", "", tile_def(TILEG_TODO)};
2543 f.body = hint_text;
2544 f.tile = tile_def(TILEG_STARTUP_HINTS);
2545 feats.emplace_back(f);
2546 }
2547 }
2548
2549 auto scroller = make_shared<Scroller>();
2550 auto vbox = make_shared<Box>(Widget::VERT);
2551
2552 for (const auto &feat : feats)
2553 {
2554 auto title_hbox = make_shared<Box>(Widget::HORZ);
2555 #ifdef USE_TILE
2556 auto icon = make_shared<Image>();
2557 icon->set_tile(feat.tile);
2558 title_hbox->add_child(move(icon));
2559 #endif
2560 auto title = make_shared<Text>(feat.title);
2561 title->set_margin_for_sdl(0, 0, 0, 10);
2562 title_hbox->add_child(move(title));
2563 title_hbox->set_cross_alignment(Widget::CENTER);
2564
2565 const bool has_desc = feat.body != feat.title && feat.body != "";
2566
2567 if (has_desc || &feat != &feats.back())
2568 {
2569 title_hbox->set_margin_for_crt(0, 0, 1, 0);
2570 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
2571 }
2572 vbox->add_child(move(title_hbox));
2573
2574 if (has_desc)
2575 {
2576 formatted_string desc_text = formatted_string::parse_string(feat.body);
2577 if (!feat.quote.empty())
2578 {
2579 desc_text.cprintf("\n\n");
2580 desc_text += formatted_string::parse_string(feat.quote);
2581 }
2582 auto text = make_shared<Text>(desc_text);
2583 if (&feat != &feats.back())
2584 {
2585 text->set_margin_for_sdl(0, 0, 20, 0);
2586 text->set_margin_for_crt(0, 0, 1, 0);
2587 }
2588 text->set_wrap_text(true);
2589 vbox->add_child(text);
2590 }
2591 }
2592 #ifdef USE_TILE_LOCAL
2593 vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
2594 #endif
2595 scroller->set_child(move(vbox));
2596
2597 auto popup = make_shared<ui::Popup>(scroller);
2598
2599 bool done = false;
2600 popup->on_keydown_event([&](const KeyEvent& ev) {
2601 done = !scroller->on_event(ev);
2602 return true;
2603 });
2604
2605 #ifdef USE_TILE_WEB
2606 tiles.json_open_object();
2607 tiles.json_open_array("feats");
2608 for (const auto &feat : feats)
2609 {
2610 tiles.json_open_object();
2611 tiles.json_write_string("title", feat.title);
2612 tiles.json_write_string("body", trimmed_string(feat.body));
2613 tiles.json_write_string("quote", trimmed_string(feat.quote));
2614 tiles.json_open_object("tile");
2615 tiles.json_write_int("t", feat.tile.tile);
2616 tiles.json_write_int("tex", get_tile_texture(feat.tile.tile));
2617 if (feat.tile.ymax != TILE_Y)
2618 tiles.json_write_int("ymax", feat.tile.ymax);
2619 tiles.json_close_object();
2620 tiles.json_close_object();
2621 }
2622 tiles.json_close_array();
2623 tiles.push_ui_layout("describe-feature-wide", 0);
2624 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
2625 #endif
2626
2627 ui::run_layout(move(popup), done);
2628 }
2629
describe_feature_type(dungeon_feature_type feat)2630 void describe_feature_type(dungeon_feature_type feat)
2631 {
2632 describe_info inf;
2633 string name = feature_description(feat, NUM_TRAPS, "", DESC_A);
2634 string title = uppercase_first(name);
2635 if (!ends_with(title, ".") && !ends_with(title, "!") && !ends_with(title, "?"))
2636 title += ".";
2637 inf.title = title;
2638 inf.body << getLongDescription(name);
2639 #ifdef USE_TILE
2640 const tileidx_t idx = tileidx_feature_base(feat);
2641 tile_def tile = tile_def(idx);
2642 show_description(inf, &tile);
2643 #else
2644 show_description(inf);
2645 #endif
2646 }
2647
get_item_desc(const item_def & item,describe_info & inf)2648 void get_item_desc(const item_def &item, describe_info &inf)
2649 {
2650 // Don't use verbose descriptions if the item contains spells,
2651 // so we can actually output these spells if space is scarce.
2652 const bool verbose = !item.has_spells();
2653 string name = item.name(DESC_INVENTORY_EQUIP) + ".";
2654 if (!in_inventory(item))
2655 name = uppercase_first(name);
2656 inf.body << name << get_item_description(item, verbose);
2657 }
2658
_allowed_actions(const item_def & item)2659 static vector<command_type> _allowed_actions(const item_def& item)
2660 {
2661 vector<command_type> actions;
2662 actions.push_back(CMD_ADJUST_INVENTORY);
2663 if (item_equip_slot(item) == EQ_WEAPON)
2664 actions.push_back(CMD_UNWIELD_WEAPON);
2665 switch (item.base_type)
2666 {
2667 case OBJ_WEAPONS:
2668 case OBJ_STAVES:
2669 if (_could_set_training_target(item, false))
2670 actions.push_back(CMD_SET_SKILL_TARGET);
2671 if (!item_is_equipped(item))
2672 {
2673 if (item_is_wieldable(item))
2674 actions.push_back(CMD_WIELD_WEAPON);
2675 }
2676 break;
2677 case OBJ_MISSILES:
2678 if (_could_set_training_target(item, false))
2679 actions.push_back(CMD_SET_SKILL_TARGET);
2680 if (!you.has_mutation(MUT_NO_GRASPING))
2681 actions.push_back(CMD_QUIVER_ITEM);
2682 break;
2683 case OBJ_ARMOUR:
2684 if (_could_set_training_target(item, false))
2685 actions.push_back(CMD_SET_SKILL_TARGET);
2686 if (item_is_equipped(item))
2687 actions.push_back(CMD_REMOVE_ARMOUR);
2688 else
2689 actions.push_back(CMD_WEAR_ARMOUR);
2690 break;
2691 case OBJ_SCROLLS:
2692 //case OBJ_BOOKS: these are handled differently
2693 actions.push_back(CMD_READ);
2694 break;
2695 case OBJ_JEWELLERY:
2696 if (item_is_equipped(item))
2697 actions.push_back(CMD_REMOVE_JEWELLERY);
2698 else
2699 actions.push_back(CMD_WEAR_JEWELLERY);
2700 break;
2701 case OBJ_POTIONS:
2702 if (you.can_drink()) // mummies and lich form forbidden
2703 actions.push_back(CMD_QUAFF);
2704 break;
2705 default:
2706 ;
2707 }
2708 if (clua.callbooleanfn(false, "ch_item_wieldable", "i", &item))
2709 actions.push_back(CMD_WIELD_WEAPON);
2710
2711 if (item_is_evokable(item))
2712 {
2713 actions.push_back(CMD_QUIVER_ITEM);
2714 actions.push_back(CMD_EVOKE);
2715 }
2716
2717 actions.push_back(CMD_DROP);
2718
2719 if (!crawl_state.game_is_tutorial())
2720 actions.push_back(CMD_INSCRIBE_ITEM);
2721
2722 return actions;
2723 }
2724
_actions_desc(const vector<command_type> & actions)2725 static string _actions_desc(const vector<command_type>& actions)
2726 {
2727 static const map<command_type, string> act_str =
2728 {
2729 { CMD_WIELD_WEAPON, "(w)ield" },
2730 { CMD_UNWIELD_WEAPON, "(u)nwield" },
2731 { CMD_QUIVER_ITEM, "(q)uiver" },
2732 { CMD_WEAR_ARMOUR, "(w)ear" },
2733 { CMD_REMOVE_ARMOUR, "(t)ake off" },
2734 { CMD_EVOKE, "e(v)oke" },
2735 { CMD_READ, "(r)ead" },
2736 { CMD_WEAR_JEWELLERY, "(p)ut on" },
2737 { CMD_REMOVE_JEWELLERY, "(r)emove" },
2738 { CMD_QUAFF, "(q)uaff" },
2739 { CMD_DROP, "(d)rop" },
2740 { CMD_INSCRIBE_ITEM, "(i)nscribe" },
2741 { CMD_ADJUST_INVENTORY, "(=)adjust" },
2742 { CMD_SET_SKILL_TARGET, "(s)kill" },
2743 };
2744 return comma_separated_fn(begin(actions), end(actions),
2745 [] (command_type cmd)
2746 {
2747 return act_str.at(cmd);
2748 },
2749 ", or ") + ".";
2750 }
2751
2752 // Take a key and a list of commands and return the command from the list
2753 // that corresponds to the key. Note that some keys are overloaded (but with
2754 // mutually-exclusive actions), so it's not just a simple lookup.
_get_action(int key,vector<command_type> actions)2755 static command_type _get_action(int key, vector<command_type> actions)
2756 {
2757 static const map<command_type, int> act_key =
2758 {
2759 { CMD_WIELD_WEAPON, 'w' },
2760 { CMD_UNWIELD_WEAPON, 'u' },
2761 { CMD_QUIVER_ITEM, 'q' },
2762 { CMD_WEAR_ARMOUR, 'w' },
2763 { CMD_REMOVE_ARMOUR, 't' },
2764 { CMD_EVOKE, 'v' },
2765 { CMD_READ, 'r' },
2766 { CMD_WEAR_JEWELLERY, 'p' },
2767 { CMD_REMOVE_JEWELLERY, 'r' },
2768 { CMD_QUAFF, 'q' },
2769 { CMD_DROP, 'd' },
2770 { CMD_INSCRIBE_ITEM, 'i' },
2771 { CMD_ADJUST_INVENTORY, '=' },
2772 { CMD_SET_SKILL_TARGET, 's' },
2773 };
2774
2775 key = tolower_safe(key);
2776
2777 for (auto cmd : actions)
2778 if (key == act_key.at(cmd))
2779 return cmd;
2780
2781 return CMD_NO_CMD;
2782 }
2783
2784 /**
2785 * Do the specified action on the specified item.
2786 *
2787 * @param item the item to have actions done on
2788 * @param action the action to do
2789 * @return whether to stay in the inventory menu afterwards
2790 */
_do_action(item_def & item,const command_type action)2791 static bool _do_action(item_def &item, const command_type action)
2792 {
2793 if (action == CMD_NO_CMD)
2794 return true;
2795
2796 const int slot = item.link;
2797 ASSERT_RANGE(slot, 0, ENDOFPACK);
2798
2799 switch (action)
2800 {
2801 case CMD_WIELD_WEAPON: wield_weapon(true, slot); break;
2802 case CMD_UNWIELD_WEAPON: wield_weapon(true, SLOT_BARE_HANDS); break;
2803 case CMD_QUIVER_ITEM: you.quiver_action.set_from_slot(slot); break;
2804 case CMD_WEAR_ARMOUR: wear_armour(slot); break;
2805 case CMD_REMOVE_ARMOUR: takeoff_armour(slot); break;
2806 case CMD_READ: read(&item); break;
2807 case CMD_WEAR_JEWELLERY: puton_ring(slot); break;
2808 case CMD_REMOVE_JEWELLERY: remove_ring(slot, true); break;
2809 case CMD_QUAFF: drink(&item); break;
2810 case CMD_DROP: drop_item(slot, item.quantity); break;
2811 case CMD_INSCRIBE_ITEM: inscribe_item(item); break;
2812 case CMD_ADJUST_INVENTORY: adjust_item(slot); break;
2813 case CMD_SET_SKILL_TARGET: target_item(item); break;
2814 case CMD_EVOKE:
2815 evoke_item(slot);
2816 break;
2817 default:
2818 die("illegal inventory cmd %d", action);
2819 }
2820 return false;
2821 }
2822
target_item(item_def & item)2823 void target_item(item_def &item)
2824 {
2825 const skill_type skill = _item_training_skill(item);
2826 if (skill == SK_NONE)
2827 return;
2828
2829 const int target = _item_training_target(item);
2830 if (target == 0)
2831 return;
2832
2833 you.set_training_target(skill, target, true);
2834 // ensure that the skill is at least enabled
2835 if (you.train[skill] == TRAINING_DISABLED)
2836 you.train[skill] = TRAINING_ENABLED;
2837 you.train_alt[skill] = you.train[skill];
2838 reset_training();
2839 }
2840
2841 /**
2842 * Display a pop-up describe any item in the game.
2843 *
2844 * @param item the item to be described.
2845 * @param fixup_desc a function (possibly null) to modify the
2846 * description before it's displayed.
2847 * @param do_actions display interaction options
2848 * @return an action to perform (if any was available or selected)
2849 *
2850 */
describe_item_popup(const item_def & item,function<void (string &)> fixup_desc,bool do_actions)2851 command_type describe_item_popup(const item_def &item,
2852 function<void (string&)> fixup_desc,
2853 bool do_actions)
2854 {
2855 if (!item.defined())
2856 return CMD_NO_CMD;
2857
2858 string name = item.name(DESC_INVENTORY_EQUIP) + ".";
2859 if (!in_inventory(item))
2860 name = uppercase_first(name);
2861
2862 string desc = get_item_description(item, true, false);
2863
2864 string quote;
2865 if (is_unrandom_artefact(item) && item_type_known(item))
2866 quote = getQuoteString(get_artefact_name(item));
2867 else
2868 quote = getQuoteString(item.name(DESC_DBNAME, true, false, false));
2869
2870 if (!(crawl_state.game_is_hints_tutorial()
2871 || quote.empty()))
2872 {
2873 desc += "\n\n" + quote;
2874 }
2875
2876 if (crawl_state.game_is_hints())
2877 desc += "\n\n" + hints_describe_item(item);
2878
2879 if (fixup_desc)
2880 fixup_desc(desc);
2881
2882 formatted_string fs_desc = formatted_string::parse_string(desc);
2883
2884 spellset spells = item_spellset(item);
2885 formatted_string spells_desc;
2886 describe_spellset(spells, &item, spells_desc, nullptr);
2887 #ifdef USE_TILE_WEB
2888 string desc_without_spells = fs_desc.to_colour_string();
2889 #endif
2890 fs_desc += spells_desc;
2891
2892 vector<command_type> actions;
2893 if (do_actions)
2894 actions = _allowed_actions(item);
2895
2896 auto vbox = make_shared<Box>(Widget::VERT);
2897 auto title_hbox = make_shared<Box>(Widget::HORZ);
2898
2899 #ifdef USE_TILE
2900 vector<tile_def> item_tiles;
2901 get_tiles_for_item(item, item_tiles, true);
2902 if (item_tiles.size() > 0)
2903 {
2904 auto tiles_stack = make_shared<Stack>();
2905 for (const auto &tile : item_tiles)
2906 {
2907 auto icon = make_shared<Image>();
2908 icon->set_tile(tile);
2909 tiles_stack->add_child(move(icon));
2910 }
2911 title_hbox->add_child(move(tiles_stack));
2912 }
2913 #endif
2914
2915 auto title = make_shared<Text>(name);
2916 title->set_margin_for_sdl(0, 0, 0, 10);
2917 title_hbox->add_child(move(title));
2918
2919 title_hbox->set_cross_alignment(Widget::CENTER);
2920 title_hbox->set_margin_for_crt(0, 0, 1, 0);
2921 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
2922 vbox->add_child(move(title_hbox));
2923
2924 auto scroller = make_shared<Scroller>();
2925 auto text = make_shared<Text>(fs_desc.trim());
2926 text->set_wrap_text(true);
2927 scroller->set_child(text);
2928 vbox->add_child(scroller);
2929
2930 formatted_string footer_text("", CYAN);
2931 if (!actions.empty())
2932 {
2933 if (!spells.empty())
2934 footer_text.cprintf("Select a spell, or ");
2935 footer_text += formatted_string(_actions_desc(actions));
2936 auto footer = make_shared<Text>();
2937 footer->set_text(footer_text);
2938 footer->set_margin_for_crt(1, 0, 0, 0);
2939 footer->set_margin_for_sdl(20, 0, 0, 0);
2940 vbox->add_child(move(footer));
2941 }
2942
2943 #ifdef USE_TILE_LOCAL
2944 vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
2945 #endif
2946
2947 auto popup = make_shared<ui::Popup>(move(vbox));
2948
2949 bool done = false;
2950 command_type action = CMD_NO_CMD;
2951 int lastch; // unused??
2952 popup->on_keydown_event([&](const KeyEvent& ev) {
2953 const auto key = ev.key() == '{' ? 'i' : ev.key();
2954 lastch = key;
2955 action = _get_action(key, actions);
2956 if (action != CMD_NO_CMD)
2957 done = true;
2958 else if (key == ' ' || key == CK_ESCAPE)
2959 done = true;
2960 else if (scroller->on_event(ev))
2961 return true;
2962 const vector<pair<spell_type,char>> spell_map = map_chars_to_spells(spells, &item);
2963 auto entry = find_if(spell_map.begin(), spell_map.end(),
2964 [key](const pair<spell_type,char>& e) { return e.second == key; });
2965 if (entry == spell_map.end())
2966 return false;
2967 describe_spell(entry->first, nullptr, &item);
2968 done = already_learning_spell();
2969 return true;
2970 });
2971
2972 #ifdef USE_TILE_WEB
2973 tiles.json_open_object();
2974 tiles.json_write_string("title", name);
2975 desc_without_spells += "SPELLSET_PLACEHOLDER";
2976 trim_string(desc_without_spells);
2977 tiles.json_write_string("body", desc_without_spells);
2978 write_spellset(spells, &item, nullptr);
2979
2980 tiles.json_write_string("actions", footer_text.tostring());
2981 tiles.json_open_array("tiles");
2982 for (const auto &tile : item_tiles)
2983 {
2984 tiles.json_open_object();
2985 tiles.json_write_int("t", tile.tile);
2986 tiles.json_write_int("tex", get_tile_texture(tile.tile));
2987 if (tile.ymax != TILE_Y)
2988 tiles.json_write_int("ymax", tile.ymax);
2989 tiles.json_close_object();
2990 }
2991 tiles.json_close_array();
2992 tiles.push_ui_layout("describe-item", 0);
2993 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
2994 #endif
2995
2996 ui::run_layout(move(popup), done);
2997
2998 return action;
2999 }
3000
3001 /**
3002 * Describe any item in the game and offer interactions if available.
3003 *
3004 * This is split out from the popup because _do_action is necessarily non
3005 * const but we would like to offer the description UI for items not in
3006 * inventory in places where only a const item_def is available.
3007 *
3008 * @param item the item to be described.
3009 * @param fixup_desc a function (possibly null) to modify the
3010 * description before it's displayed.
3011 * @return whether to remain in the inventory menu after description
3012 *
3013 */
describe_item(item_def & item,function<void (string &)> fixup_desc)3014 bool describe_item(item_def &item, function<void (string&)> fixup_desc)
3015 {
3016
3017 const bool do_actions = in_inventory(item) // Dead men use no items.
3018 && !(you.pending_revival || crawl_state.updating_scores);
3019 command_type action = describe_item_popup(item, fixup_desc, do_actions);
3020
3021 return _do_action(item, action);
3022 }
3023
inscribe_item(item_def & item)3024 void inscribe_item(item_def &item)
3025 {
3026 mprf_nocap(MSGCH_EQUIPMENT, "%s", item.name(DESC_INVENTORY).c_str());
3027
3028 const bool is_inscribed = !item.inscription.empty();
3029 string prompt = is_inscribed ? "Replace inscription with what? "
3030 : "Inscribe with what? ";
3031
3032 char buf[79];
3033 int ret = msgwin_get_line(prompt, buf, sizeof buf, nullptr,
3034 item.inscription);
3035 if (ret)
3036 {
3037 canned_msg(MSG_OK);
3038 return;
3039 }
3040
3041 string new_inscrip = buf;
3042 trim_string_right(new_inscrip);
3043
3044 if (item.inscription == new_inscrip)
3045 {
3046 canned_msg(MSG_OK);
3047 return;
3048 }
3049
3050 item.inscription = new_inscrip;
3051
3052 mprf_nocap(MSGCH_EQUIPMENT, "%s", item.name(DESC_INVENTORY).c_str());
3053 you.wield_change = true;
3054 quiver::set_needs_redraw();
3055 }
3056
3057 /**
3058 * List the simple calculated stats of a given spell, when cast by the player
3059 * in their current condition.
3060 *
3061 * @param spell The spell in question.
3062 */
_player_spell_stats(const spell_type spell)3063 static string _player_spell_stats(const spell_type spell)
3064 {
3065 string description;
3066 description += make_stringf("\nLevel: %d", spell_difficulty(spell));
3067
3068 const string schools = spell_schools_string(spell);
3069 description +=
3070 make_stringf(" School%s: %s",
3071 schools.find("/") != string::npos ? "s" : "",
3072 schools.c_str());
3073
3074 if (!crawl_state.need_save
3075 || (get_spell_flags(spell) & spflag::monster))
3076 {
3077 return description; // all other info is player-dependent
3078 }
3079
3080
3081 string failure;
3082 if (you.divine_exegesis)
3083 failure = "0%";
3084 else
3085 failure = failure_rate_to_string(raw_spell_fail(spell));
3086 description += make_stringf(" Fail: %s", failure.c_str());
3087
3088 const string damage_string = spell_damage_string(spell);
3089 const int acc = spell_acc(spell);
3090 // TODO: generalize this pattern? It's very common in descriptions
3091 const int padding = (acc != -1) ? 8 : damage_string.size() ? 6 : 5;
3092 description += make_stringf("\n\n%*s: ", padding, "Power");
3093 description += spell_power_string(spell);
3094
3095 if (damage_string != "")
3096 {
3097 description += make_stringf("\n%*s: ", padding, "Damage");
3098 description += damage_string;
3099 }
3100 if (acc != -1)
3101 {
3102 ostringstream acc_str;
3103 _print_bar(acc, 3, "", acc_str);
3104 description += make_stringf("\n%*s: %s", padding, "Accuracy",
3105 acc_str.str().c_str());
3106 }
3107
3108 description += make_stringf("\n%*s: ", padding, "Range");
3109 description += spell_range_string(spell);
3110 description += make_stringf("\n%*s: ", padding, "Noise");
3111 description += spell_noise_string(spell);
3112 description += "\n";
3113 return description;
3114 }
3115
get_skill_description(skill_type skill,bool need_title)3116 string get_skill_description(skill_type skill, bool need_title)
3117 {
3118 string lookup = skill_name(skill);
3119 string result = "";
3120
3121 if (need_title)
3122 {
3123 result = lookup;
3124 result += "\n\n";
3125 }
3126
3127 result += getLongDescription(lookup);
3128
3129 switch (skill)
3130 {
3131 case SK_INVOCATIONS:
3132 if (you.has_mutation(MUT_FORLORN))
3133 {
3134 result += "\n";
3135 result += "How on earth did you manage to pick this up?";
3136 }
3137 else if (you_worship(GOD_TROG))
3138 {
3139 result += "\n";
3140 result += "Note that Trog doesn't use Invocations, due to its "
3141 "close connection to magic.";
3142 }
3143 break;
3144
3145 case SK_SPELLCASTING:
3146 if (you_worship(GOD_TROG))
3147 {
3148 result += "\n";
3149 result += "Keep in mind, though, that Trog would greatly "
3150 "disapprove of this.";
3151 }
3152 break;
3153 default:
3154 // No further information.
3155 break;
3156 }
3157
3158 return result;
3159 }
3160
3161 /// How much power do we think the given monster casts this spell with?
_hex_pow(const spell_type spell,const int hd)3162 static int _hex_pow(const spell_type spell, const int hd)
3163 {
3164 const int cap = 200;
3165 const int pow = mons_power_for_hd(spell, hd) / ENCH_POW_FACTOR;
3166 return min(cap, pow);
3167 }
3168
3169 /**
3170 * What are the odds of the given spell, cast by a monster with the given
3171 * spell_hd, affecting the player?
3172 */
hex_chance(const spell_type spell,const int hd)3173 int hex_chance(const spell_type spell, const int hd)
3174 {
3175 const int capped_pow = _hex_pow(spell, hd);
3176 const int chance = hex_success_chance(you.willpower(), capped_pow,
3177 100, true);
3178 if (spell == SPELL_STRIP_WILLPOWER)
3179 return chance + (100 - chance) / 3; // ignores wl 1/3rd of the time
3180 return chance;
3181 }
3182
3183 /**
3184 * Describe miscast effects from a spell
3185 *
3186 * @param spell
3187 */
_miscast_damage_string(spell_type spell)3188 static string _miscast_damage_string(spell_type spell)
3189 {
3190 const map <spschool, string> damage_flavor = {
3191 { spschool::conjuration, "irresistible" },
3192 { spschool::necromancy, "draining" },
3193 { spschool::fire, "fire" },
3194 { spschool::ice, "cold" },
3195 { spschool::air, "electric" },
3196 { spschool::earth, "fragmentation" },
3197 { spschool::poison, "poison" },
3198 };
3199
3200 const map <spschool, string> special_flavor = {
3201 { spschool::summoning, "summons a nameless horror" },
3202 { spschool::transmutation, "further contaminates you" },
3203 { spschool::translocation, "anchors you in place" },
3204 { spschool::hexes, "debuffs and slows you" },
3205 };
3206
3207 spschools_type disciplines = get_spell_disciplines(spell);
3208 vector <string> descs;
3209
3210 for (const auto &flav : special_flavor)
3211 if (disciplines & flav.first)
3212 descs.push_back(flav.second);
3213
3214 int dam = max_miscast_damage(spell);
3215 vector <string> dam_flavors;
3216 for (const auto &flav : damage_flavor)
3217 if (disciplines & flav.first)
3218 dam_flavors.push_back(flav.second);
3219
3220 if (!dam_flavors.empty())
3221 {
3222 descs.push_back(make_stringf("deals up to %d %s damage", dam,
3223 comma_separated_line(dam_flavors.begin(),
3224 dam_flavors.end(),
3225 " or ").c_str()));
3226 }
3227
3228 return (descs.size() > 1 ? "either " : "")
3229 + comma_separated_line(descs.begin(), descs.end(), " or ", "; ");
3230 }
3231
3232 /**
3233 * Describe mostly non-numeric player-specific information about a spell.
3234 *
3235 * (E.g., your god's opinion of it, whether it's in a high-level book that
3236 * you can't memorise from, whether it's currently useless for whatever
3237 * reason...)
3238 *
3239 * @param spell The spell in question.
3240 */
_player_spell_desc(spell_type spell)3241 static string _player_spell_desc(spell_type spell)
3242 {
3243 if (!crawl_state.need_save || (get_spell_flags(spell) & spflag::monster))
3244 return ""; // all info is player-dependent
3245
3246 ostringstream description;
3247
3248 description << "Miscasting this spell causes magic contamination"
3249 << (fail_severity(spell) ?
3250 " and also " + _miscast_damage_string(spell) : "")
3251 << ".\n";
3252
3253 if (spell == SPELL_SPELLFORGED_SERVITOR)
3254 {
3255 spell_type servitor_spell = player_servitor_spell();
3256 description << "Your servitor";
3257 if (servitor_spell == SPELL_NO_SPELL)
3258 description << " would be unable to mimic any of your spells";
3259 else
3260 {
3261 description << " casts "
3262 << spell_title(player_servitor_spell());
3263 }
3264 description << ".\n";
3265 }
3266
3267 // Report summon cap
3268 const int limit = summons_limit(spell);
3269 if (limit)
3270 {
3271 description << "You can sustain at most " + number_in_words(limit)
3272 << " creature" << (limit > 1 ? "s" : "")
3273 << " summoned by this spell.\n";
3274 }
3275
3276 if (god_hates_spell(spell, you.religion))
3277 {
3278 description << uppercase_first(god_name(you.religion))
3279 << " frowns upon the use of this spell.\n";
3280 if (god_loathes_spell(spell, you.religion))
3281 description << "You'd be excommunicated if you dared to cast it!\n";
3282 }
3283 else if (god_likes_spell(spell, you.religion))
3284 {
3285 description << uppercase_first(god_name(you.religion))
3286 << " supports the use of this spell.\n";
3287 }
3288
3289 if (!you_can_memorise(spell))
3290 {
3291 description << "\nYou cannot "
3292 << (you.has_spell(spell) ? "cast" : "memorise")
3293 << " this spell because "
3294 << desc_cannot_memorise_reason(spell)
3295 << "\n";
3296 }
3297 else if (casting_is_useless(spell, true))
3298 {
3299 // this preempts the more general uselessness call below, for the sake
3300 // of applying slightly different formatting.
3301 description << "\n<red>"
3302 << uppercase_first(casting_uselessness_reason(spell, true))
3303 << "</red>\n";
3304 }
3305 else if (spell_is_useless(spell, true, false))
3306 {
3307 description << "\nThis spell would have no effect right now because "
3308 << spell_uselessness_reason(spell, true, false)
3309 << "\n";
3310 }
3311
3312 return description.str();
3313 }
3314
3315
3316 /**
3317 * Describe a spell, as cast by the player.
3318 *
3319 * @param spell The spell in question.
3320 * @return Information about the spell; does not include the title or
3321 * db description, but does include level, range, etc.
3322 */
player_spell_desc(spell_type spell)3323 string player_spell_desc(spell_type spell)
3324 {
3325 return _player_spell_stats(spell) + _player_spell_desc(spell);
3326 }
3327
3328 /**
3329 * Examine a given spell. Set the given string to its description, stats, &c.
3330 * If it's a book in a spell that the player is holding, mention the option to
3331 * memorise it.
3332 *
3333 * @param spell The spell in question.
3334 * @param mon_owner If this spell is being examined from a monster's
3335 * description, 'spell' is that monster. Else, null.
3336 * @param description Set to the description & details of the spell.
3337 */
_get_spell_description(const spell_type spell,const monster_info * mon_owner,string & description)3338 static void _get_spell_description(const spell_type spell,
3339 const monster_info *mon_owner,
3340 string &description)
3341 {
3342 description.reserve(500);
3343
3344 const string long_descrip = getLongDescription(string(spell_title(spell))
3345 + " spell");
3346
3347 if (!long_descrip.empty())
3348 description += long_descrip;
3349 else
3350 {
3351 description += "This spell has no description. "
3352 "Casting it may therefore be unwise. "
3353 #ifdef DEBUG
3354 "Instead, go fix it. ";
3355 #else
3356 "Please file a bug report.";
3357 #endif
3358 }
3359
3360 if (mon_owner)
3361 {
3362 const int hd = mon_owner->spell_hd();
3363 const int range = mons_spell_range_for_hd(spell, hd);
3364 description += "\nRange : ";
3365 if (spell == SPELL_CALL_DOWN_LIGHTNING)
3366 description += stringize_glyph(mons_char(mon_owner->type)) + "..---->";
3367 else
3368 description += range_string(range, range, mons_char(mon_owner->type));
3369 description += "\n";
3370
3371 // only display this if the player exists (not in the main menu)
3372 if (crawl_state.need_save && (get_spell_flags(spell) & spflag::WL_check)
3373 #ifndef DEBUG_DIAGNOSTICS
3374 && mon_owner->attitude != ATT_FRIENDLY
3375 #endif
3376 )
3377 {
3378 string wiz_info;
3379 #ifdef WIZARD
3380 if (you.wizard)
3381 wiz_info += make_stringf(" (pow %d)", _hex_pow(spell, hd));
3382 #endif
3383 description += you.immune_to_hex(spell)
3384 ? make_stringf("You cannot be affected by this "
3385 "spell right now. %s\n",
3386 wiz_info.c_str())
3387 : make_stringf("Chance to defeat your Will: %d%%%s\n",
3388 hex_chance(spell, hd),
3389 wiz_info.c_str());
3390 }
3391
3392 }
3393 else
3394 description += player_spell_desc(spell);
3395
3396 const string quote = getQuoteString(string(spell_title(spell)) + " spell");
3397 if (!quote.empty())
3398 description += "\n" + quote;
3399 }
3400
3401 /**
3402 * Provide the text description of a given spell.
3403 *
3404 * @param spell The spell in question.
3405 * @param inf[out] The spell's description is concatenated onto the end of
3406 * inf.body.
3407 */
get_spell_desc(const spell_type spell,describe_info & inf)3408 void get_spell_desc(const spell_type spell, describe_info &inf)
3409 {
3410 string desc;
3411 _get_spell_description(spell, nullptr, desc);
3412 inf.body << desc;
3413 }
3414
3415 /**
3416 * Examine a given spell. List its description and details, and handle
3417 * memorising the spell in question, if the player is able & chooses to do so.
3418 *
3419 * @param spelled The spell in question.
3420 * @param mon_owner If this spell is being examined from a monster's
3421 * description, 'mon_owner' is that monster. Else, null.
3422 */
describe_spell(spell_type spell,const monster_info * mon_owner,const item_def * item)3423 void describe_spell(spell_type spell, const monster_info *mon_owner,
3424 const item_def* item)
3425 {
3426 UNUSED(item);
3427
3428 string desc;
3429 _get_spell_description(spell, mon_owner, desc);
3430
3431 auto vbox = make_shared<Box>(Widget::VERT);
3432 #ifdef USE_TILE_LOCAL
3433 vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
3434 #endif
3435
3436 auto title_hbox = make_shared<Box>(Widget::HORZ);
3437 #ifdef USE_TILE
3438 auto spell_icon = make_shared<Image>();
3439 spell_icon->set_tile(tile_def(tileidx_spell(spell)));
3440 title_hbox->add_child(move(spell_icon));
3441 #endif
3442
3443 string spl_title = spell_title(spell);
3444 trim_string(desc);
3445
3446 auto title = make_shared<Text>();
3447 title->set_text(spl_title);
3448 title->set_margin_for_sdl(0, 0, 0, 10);
3449 title_hbox->add_child(move(title));
3450
3451 title_hbox->set_cross_alignment(Widget::CENTER);
3452 title_hbox->set_margin_for_crt(0, 0, 1, 0);
3453 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
3454 vbox->add_child(move(title_hbox));
3455
3456 auto scroller = make_shared<Scroller>();
3457 auto text = make_shared<Text>();
3458 text->set_text(formatted_string::parse_string(desc));
3459 text->set_wrap_text(true);
3460 scroller->set_child(move(text));
3461 vbox->add_child(scroller);
3462
3463 auto popup = make_shared<ui::Popup>(move(vbox));
3464
3465 bool done = false;
3466 int lastch;
3467 popup->on_keydown_event([&](const KeyEvent& ev) {
3468 lastch = ev.key();
3469 done = (lastch == CK_ESCAPE || lastch == CK_ENTER || lastch == ' ');
3470 if (scroller->on_event(ev))
3471 return true;
3472 return done;
3473 });
3474
3475 #ifdef USE_TILE_WEB
3476 tiles.json_open_object();
3477 auto tile = tile_def(tileidx_spell(spell));
3478 tiles.json_open_object("tile");
3479 tiles.json_write_int("t", tile.tile);
3480 tiles.json_write_int("tex", get_tile_texture(tile.tile));
3481 if (tile.ymax != TILE_Y)
3482 tiles.json_write_int("ymax", tile.ymax);
3483 tiles.json_close_object();
3484 tiles.json_write_string("title", spl_title);
3485 tiles.json_write_string("desc", desc);
3486 tiles.push_ui_layout("describe-spell", 0);
3487 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
3488 #endif
3489
3490 ui::run_layout(move(popup), done);
3491 }
3492
3493 /**
3494 * Examine a given ability. List its description and details.
3495 *
3496 * @param ability The ability in question.
3497 */
describe_ability(ability_type ability)3498 void describe_ability(ability_type ability)
3499 {
3500 describe_info inf;
3501 inf.title = ability_name(ability);
3502 inf.body << get_ability_desc(ability, false);
3503 tile_def tile = tile_def(tileidx_ability(ability));
3504 show_description(inf, &tile);
3505 }
3506
3507 /**
3508 * Examine a given deck.
3509 */
describe_deck(deck_type deck)3510 void describe_deck(deck_type deck)
3511 {
3512 describe_info inf;
3513
3514 if (deck == DECK_STACK)
3515 inf.title = "A stacked deck";
3516 else
3517 inf.title = "The " + deck_name(deck);
3518
3519 inf.body << deck_description(deck);
3520
3521 show_description(inf);
3522 }
3523
_describe_draconian(const monster_info & mi)3524 static string _describe_draconian(const monster_info& mi)
3525 {
3526 string description;
3527 const int subsp = mi.draco_or_demonspawn_subspecies();
3528
3529 if (subsp != mi.type)
3530 {
3531 description += "It has ";
3532
3533 switch (subsp)
3534 {
3535 case MONS_BLACK_DRACONIAN: description += "black "; break;
3536 case MONS_YELLOW_DRACONIAN: description += "yellow "; break;
3537 case MONS_GREEN_DRACONIAN: description += "green "; break;
3538 case MONS_PURPLE_DRACONIAN: description += "purple "; break;
3539 case MONS_RED_DRACONIAN: description += "red "; break;
3540 case MONS_WHITE_DRACONIAN: description += "white "; break;
3541 case MONS_GREY_DRACONIAN: description += "grey "; break;
3542 case MONS_PALE_DRACONIAN: description += "pale "; break;
3543 default:
3544 break;
3545 }
3546
3547 description += "scales. ";
3548 }
3549
3550 switch (subsp)
3551 {
3552 case MONS_BLACK_DRACONIAN:
3553 description += "Sparks flare out of its mouth and nostrils.";
3554 break;
3555 case MONS_YELLOW_DRACONIAN:
3556 description += "Acidic fumes swirl around it.";
3557 break;
3558 case MONS_GREEN_DRACONIAN:
3559 description += "Venom drips from its jaws.";
3560 break;
3561 case MONS_PURPLE_DRACONIAN:
3562 description += "Its outline shimmers with magical energy.";
3563 break;
3564 case MONS_RED_DRACONIAN:
3565 description += "Smoke pours from its nostrils.";
3566 break;
3567 case MONS_WHITE_DRACONIAN:
3568 description += "Frost pours from its nostrils.";
3569 break;
3570 case MONS_GREY_DRACONIAN:
3571 description += "Its scales and tail are adapted to the water.";
3572 break;
3573 case MONS_PALE_DRACONIAN:
3574 description += "It is cloaked in a pall of superheated steam.";
3575 break;
3576 default:
3577 break;
3578 }
3579
3580 return description;
3581 }
3582
_describe_demonspawn_role(monster_type type)3583 static string _describe_demonspawn_role(monster_type type)
3584 {
3585 switch (type)
3586 {
3587 case MONS_BLOOD_SAINT:
3588 return "It weaves powerful and unpredictable spells of devastation.";
3589 case MONS_WARMONGER:
3590 return "It is devoted to combat, disrupting the magic of its foes as "
3591 "it battles endlessly.";
3592 case MONS_CORRUPTER:
3593 return "It corrupts space around itself, and can twist even the very "
3594 "flesh of its opponents.";
3595 case MONS_BLACK_SUN:
3596 return "It shines with an unholy radiance, and wields powers of "
3597 "darkness from its devotion to the deities of death.";
3598 default:
3599 return "";
3600 }
3601 }
3602
_describe_demonspawn_base(int species)3603 static string _describe_demonspawn_base(int species)
3604 {
3605 switch (species)
3606 {
3607 case MONS_MONSTROUS_DEMONSPAWN:
3608 return "It is more beast now than whatever species it is descended from.";
3609 case MONS_GELID_DEMONSPAWN:
3610 return "It is covered in icy armour.";
3611 case MONS_INFERNAL_DEMONSPAWN:
3612 return "It gives off an intense heat.";
3613 case MONS_TORTUROUS_DEMONSPAWN:
3614 return "It oozes dark energies.";
3615 }
3616 return "";
3617 }
3618
_describe_demonspawn(const monster_info & mi)3619 static string _describe_demonspawn(const monster_info& mi)
3620 {
3621 string description;
3622 const int subsp = mi.draco_or_demonspawn_subspecies();
3623
3624 description += _describe_demonspawn_base(subsp);
3625
3626 if (subsp != mi.type)
3627 {
3628 const string demonspawn_role = _describe_demonspawn_role(mi.type);
3629 if (!demonspawn_role.empty())
3630 description += " " + demonspawn_role;
3631 }
3632
3633 return description;
3634 }
3635
_get_resist_name(mon_resist_flags res_type)3636 static const char* _get_resist_name(mon_resist_flags res_type)
3637 {
3638 switch (res_type)
3639 {
3640 case MR_RES_ELEC:
3641 return "electricity";
3642 case MR_RES_POISON:
3643 return "poison";
3644 case MR_RES_FIRE:
3645 return "fire";
3646 case MR_RES_STEAM:
3647 return "steam";
3648 case MR_RES_COLD:
3649 return "cold";
3650 case MR_RES_ACID:
3651 return "acid";
3652 case MR_RES_MIASMA:
3653 return "miasma";
3654 case MR_RES_NEG:
3655 return "negative energy";
3656 case MR_RES_DAMNATION:
3657 return "damnation";
3658 case MR_RES_VORTEX:
3659 return "polar vortices";
3660 default:
3661 return "buggy resistance";
3662 }
3663 }
3664
_get_threat_desc(mon_threat_level_type threat)3665 static const char* _get_threat_desc(mon_threat_level_type threat)
3666 {
3667 switch (threat)
3668 {
3669 case MTHRT_TRIVIAL: return "harmless";
3670 case MTHRT_EASY: return "easy";
3671 case MTHRT_TOUGH: return "dangerous";
3672 case MTHRT_NASTY: return "extremely dangerous";
3673 case MTHRT_UNDEF:
3674 default: return "buggily threatening";
3675 }
3676 }
3677
3678 /**
3679 * Describe monster attack 'flavours' that trigger before the attack.
3680 *
3681 * @param flavour The flavour in question; e.g. AF_SWOOP.
3682 * @return A description of anything that happens 'before' an attack
3683 * with the given flavour;
3684 * e.g. "swoop behind its target and ".
3685 */
_special_flavour_prefix(attack_flavour flavour)3686 static const char* _special_flavour_prefix(attack_flavour flavour)
3687 {
3688 switch (flavour)
3689 {
3690 case AF_KITE:
3691 return "retreat from adjacent foes and ";
3692 case AF_SWOOP:
3693 return "swoop behind its foe and ";
3694 default:
3695 return "";
3696 }
3697 }
3698
3699 /**
3700 * Describe monster attack 'flavours' that have extra range.
3701 *
3702 * @param flavour The flavour in question; e.g. AF_REACH_STING.
3703 * @return If the flavour has extra-long range, say so. E.g.,
3704 * " from a distance". (Else "").
3705 */
_flavour_range_desc(attack_flavour flavour)3706 static const char* _flavour_range_desc(attack_flavour flavour)
3707 {
3708 if (flavour_has_reach(flavour))
3709 return " from a distance";
3710 return "";
3711 }
3712
_flavour_base_desc(attack_flavour flavour)3713 static string _flavour_base_desc(attack_flavour flavour)
3714 {
3715 static const map<attack_flavour, string> base_descs = {
3716 { AF_ACID, "deal extra acid damage"},
3717 { AF_BLINK, "blink self" },
3718 { AF_BLINK_WITH, "blink together with the defender" },
3719 { AF_COLD, "deal up to %d extra cold damage" },
3720 { AF_CONFUSE, "cause confusion" },
3721 { AF_DRAIN_STR, "drain strength" },
3722 { AF_DRAIN_INT, "drain intelligence" },
3723 { AF_DRAIN_DEX, "drain dexterity" },
3724 { AF_DRAIN_STAT, "drain strength, intelligence or dexterity" },
3725 { AF_DRAIN, "drain life" },
3726 { AF_ELEC, "deal up to %d extra electric damage" },
3727 { AF_FIRE, "deal up to %d extra fire damage" },
3728 { AF_MUTATE, "cause mutations" },
3729 { AF_POISON_PARALYSE, "poison and cause paralysis or slowing" },
3730 { AF_POISON, "cause poisoning" },
3731 { AF_POISON_STRONG, "cause strong poisoning" },
3732 { AF_VAMPIRIC, "drain health from the living" },
3733 { AF_DISTORT, "cause wild translocation effects" },
3734 { AF_RAGE, "cause berserking" },
3735 { AF_STICKY_FLAME, "apply sticky flame" },
3736 { AF_CHAOTIC, "cause unpredictable effects" },
3737 { AF_STEAL, "steal items" },
3738 { AF_CRUSH, "begin ongoing constriction" },
3739 { AF_REACH, "" },
3740 { AF_HOLY, "deal extra damage to undead and demons" },
3741 { AF_ANTIMAGIC, "drain magic" },
3742 { AF_PAIN, "cause pain to the living" },
3743 { AF_ENSNARE, "ensnare with webbing" },
3744 { AF_ENGULF, "engulf" },
3745 { AF_PURE_FIRE, "" },
3746 { AF_DRAIN_SPEED, "drain speed" },
3747 { AF_VULN, "reduce willpower" },
3748 { AF_SHADOWSTAB, "deal increased damage when unseen" },
3749 { AF_DROWN, "deal drowning damage" },
3750 { AF_CORRODE, "cause corrosion" },
3751 { AF_SCARAB, "drain speed and drain health" },
3752 { AF_TRAMPLE, "knock back the defender" },
3753 { AF_REACH_STING, "cause poisoning" },
3754 { AF_REACH_TONGUE, "deal extra acid damage" },
3755 { AF_WEAKNESS, "cause weakness" },
3756 { AF_KITE, "" },
3757 { AF_SWOOP, "" },
3758 { AF_PLAIN, "" },
3759 };
3760
3761 const string* desc = map_find(base_descs, flavour);
3762 ASSERT(desc);
3763 return *desc;
3764 }
3765
3766 /**
3767 * Provide a short, and-prefixed flavour description of the given attack
3768 * flavour, if any.
3769 *
3770 * @param flavour E.g. AF_COLD, AF_PLAIN.
3771 * @param HD The hit dice of the monster using the flavour.
3772 * @return "" if AF_PLAIN; else " <desc>", e.g.
3773 * " to deal up to 27 extra cold damage if any damage is dealt".
3774 */
_flavour_effect(attack_flavour flavour,int HD)3775 static string _flavour_effect(attack_flavour flavour, int HD)
3776 {
3777 const string base_desc = _flavour_base_desc(flavour);
3778 if (base_desc.empty())
3779 return base_desc;
3780
3781 const int flavour_dam = flavour_damage(flavour, HD, false);
3782 const string flavour_desc = make_stringf(base_desc.c_str(), flavour_dam);
3783
3784 if (!flavour_triggers_damageless(flavour)
3785 && flavour != AF_KITE && flavour != AF_SWOOP)
3786 {
3787 return " to " + flavour_desc + " if any damage is dealt";
3788 }
3789
3790 return " to " + flavour_desc;
3791 }
3792
3793 struct mon_attack_info
3794 {
3795 mon_attack_def definition;
3796 const item_def* weapon;
operator <mon_attack_info3797 bool operator < (const mon_attack_info &other) const
3798 {
3799 return std::tie(definition.type, definition.flavour,
3800 definition.damage, weapon)
3801 < std::tie(other.definition.type, other.definition.flavour,
3802 other.definition.damage, other.weapon);
3803 }
3804 };
3805
3806 /**
3807 * What weapon is the given monster using for the given attack, if any?
3808 *
3809 * @param mi The monster in question.
3810 * @param atk The attack number. (E.g. 0, 1, 2...)
3811 * @return The melee weapon being used by the monster for the given
3812 * attack, if any.
3813 */
_weapon_for_attack(const monster_info & mi,int atk)3814 static const item_def* _weapon_for_attack(const monster_info& mi, int atk)
3815 {
3816 const item_def* weapon
3817 = atk == 0 ? mi.inv[MSLOT_WEAPON].get() :
3818 atk == 1 && mi.wields_two_weapons() ? mi.inv[MSLOT_ALT_WEAPON].get() :
3819 nullptr;
3820
3821 if (weapon && is_weapon(*weapon))
3822 return weapon;
3823 return nullptr;
3824 }
3825
_monster_attacks_description(const monster_info & mi)3826 static string _monster_attacks_description(const monster_info& mi)
3827 {
3828 ostringstream result;
3829 map<mon_attack_info, int> attack_counts;
3830 brand_type special_flavour = SPWPN_NORMAL;
3831
3832 if (mi.props.exists(SPECIAL_WEAPON_KEY))
3833 {
3834 ASSERT(mi.type == MONS_PANDEMONIUM_LORD || mons_is_pghost(mi.type));
3835 special_flavour = (brand_type) mi.props[SPECIAL_WEAPON_KEY].get_int();
3836 }
3837
3838 for (int i = 0; i < MAX_NUM_ATTACKS; ++i)
3839 {
3840 const mon_attack_def &attack = mi.attack[i];
3841 if (attack.type == AT_NONE)
3842 break; // assumes there are no gaps in attack arrays
3843
3844 const item_def* weapon = _weapon_for_attack(mi, i);
3845 mon_attack_info attack_info = { attack, weapon };
3846
3847 ++attack_counts[attack_info];
3848 }
3849
3850 // Hydrae have only one explicit attack, which is repeated for each head.
3851 if (mons_genus(mi.base_type) == MONS_HYDRA)
3852 for (auto &attack_count : attack_counts)
3853 attack_count.second = mi.num_heads;
3854
3855 vector<string> attack_descs;
3856 for (const auto &attack_count : attack_counts)
3857 {
3858 const mon_attack_info &info = attack_count.first;
3859 const mon_attack_def &attack = info.definition;
3860
3861 const string weapon_name =
3862 info.weapon ? info.weapon->name(DESC_PLAIN).c_str()
3863 : ghost_brand_name(special_flavour, mi.type).c_str();
3864 const string weapon_note = weapon_name.size() ?
3865 make_stringf(" plus %s %s",
3866 mi.pronoun(PRONOUN_POSSESSIVE), weapon_name.c_str())
3867 : "";
3868
3869 const string count_desc =
3870 attack_count.second == 1 ? "" :
3871 attack_count.second == 2 ? " twice" :
3872 " " + number_in_words(attack_count.second) + " times";
3873
3874 // XXX: hack alert
3875 if (attack.flavour == AF_PURE_FIRE)
3876 {
3877 attack_descs.push_back(
3878 make_stringf("%s for up to %d fire damage",
3879 mon_attack_name(attack.type, false).c_str(),
3880 flavour_damage(attack.flavour, mi.hd, false)));
3881 continue;
3882 }
3883
3884 // Damage is listed in parentheses for attacks with a flavour
3885 // description, but not for plain attacks.
3886 bool has_flavour = !_flavour_base_desc(attack.flavour).empty();
3887 const string damage_desc =
3888 make_stringf("%sfor up to %d damage%s%s%s",
3889 has_flavour ? "(" : "",
3890 attack.damage,
3891 attack_count.second > 1 ? " each" : "",
3892 weapon_note.c_str(),
3893 has_flavour ? ")" : "");
3894
3895 attack_descs.push_back(
3896 make_stringf("%s%s%s%s %s%s",
3897 _special_flavour_prefix(attack.flavour),
3898 mon_attack_name(attack.type, false).c_str(),
3899 _flavour_range_desc(attack.flavour),
3900 count_desc.c_str(),
3901 damage_desc.c_str(),
3902 _flavour_effect(attack.flavour, mi.hd).c_str()));
3903 }
3904
3905 if (!attack_descs.empty())
3906 {
3907 result << uppercase_first(mi.pronoun(PRONOUN_SUBJECTIVE));
3908 result << " can " << comma_separated_line(attack_descs.begin(),
3909 attack_descs.end(),
3910 "; and ", "; ");
3911 _describe_mons_to_hit(mi, result);
3912 result << ".\n";
3913 }
3914
3915 if (mi.type == MONS_ROYAL_JELLY)
3916 {
3917 result << "It will release varied jellies when damaged or killed, with"
3918 " the number of jellies proportional to the amount of damage.\n";
3919 result << "It will release all of its jellies when polymorphed.\n";
3920 }
3921
3922 return result.str();
3923 }
3924
_monster_missiles_description(const monster_info & mi)3925 static string _monster_missiles_description(const monster_info& mi)
3926 {
3927 item_def *missile = mi.inv[MSLOT_MISSILE].get();
3928 if (!missile)
3929 return "";
3930
3931 string desc;
3932 desc += uppercase_first(mi.pronoun(PRONOUN_SUBJECTIVE));
3933 desc += mi.pronoun_plurality() ? " are quivering " : " is quivering ";
3934 if (missile->is_type(OBJ_MISSILES, MI_THROWING_NET))
3935 desc += missile->name(DESC_A, false, false, true, false);
3936 else
3937 desc += pluralise(missile->name(DESC_PLAIN, false, false, true, false));
3938 desc += ".\n";
3939 return desc;
3940 }
3941
_monster_spells_description(const monster_info & mi)3942 static string _monster_spells_description(const monster_info& mi)
3943 {
3944 // Show monster spells and spell-like abilities.
3945 if (!mi.has_spells())
3946 return "";
3947
3948 formatted_string description;
3949 describe_spellset(monster_spellset(mi), nullptr, description, &mi);
3950 description.cprintf("\nTo read a description, press the key listed above. "
3951 "(AdB) indicates damage (the sum of A B-sided dice), "
3952 "(x%%) indicates the chance to defeat your Will, "
3953 "and (y) indicates the spell range");
3954 description.cprintf(crawl_state.need_save
3955 ? "; shown in red if you are in range.\n"
3956 : ".\n");
3957
3958 return description.to_colour_string();
3959 }
3960
_speed_description(int speed)3961 static const char *_speed_description(int speed)
3962 {
3963 // These thresholds correspond to the player mutations for fast and slow.
3964 ASSERT(speed != 10);
3965 if (speed < 7)
3966 return "extremely slowly";
3967 else if (speed < 8)
3968 return "very slowly";
3969 else if (speed < 10)
3970 return "slowly";
3971 else if (speed > 15)
3972 return "extremely quickly";
3973 else if (speed > 13)
3974 return "very quickly";
3975 else if (speed > 10)
3976 return "quickly";
3977
3978 return "buggily";
3979 }
3980
_add_energy_to_string(int speed,int energy,string what,vector<string> & fast,vector<string> & slow)3981 static void _add_energy_to_string(int speed, int energy, string what,
3982 vector<string> &fast, vector<string> &slow)
3983 {
3984 if (energy == 10)
3985 return;
3986
3987 const int act_speed = (speed * 10) / energy;
3988 if (act_speed > 10)
3989 fast.push_back(what + " " + _speed_description(act_speed));
3990 if (act_speed < 10)
3991 slow.push_back(what + " " + _speed_description(act_speed));
3992 }
3993
3994 /**
3995 * Display the % chance of a player hitting the given monster.
3996 *
3997 * @param mi[in] Player-visible info about the monster in question.
3998 * @param result[in,out] The stringstream to append to.
3999 */
describe_to_hit(const monster_info & mi,ostringstream & result,bool parenthesize)4000 void describe_to_hit(const monster_info& mi, ostringstream &result,
4001 bool parenthesize)
4002 {
4003 // TODO: don't do this if the player doesn't exist (main menu)
4004
4005 const item_def* weapon = you.weapon();
4006 if (weapon != nullptr && !is_weapon(*weapon))
4007 return; // breadwielding
4008
4009 const bool melee = weapon == nullptr || !is_range_weapon(*weapon);
4010 int acc_pct;
4011 if (melee)
4012 {
4013 melee_attack attk(&you, nullptr);
4014 acc_pct = to_hit_pct(mi, attk, true);
4015 }
4016 else
4017 {
4018 // TODO: handle throwing to-hit somehow?
4019 const int missile = quiver::find_action_from_launcher(you.weapon())->get_item();
4020 if (missile < 0)
4021 return; // failure to launch
4022 ranged_attack attk(&you, nullptr, &you.inv[missile], is_pproj_active());
4023 acc_pct = to_hit_pct(mi, attk, false);
4024 }
4025
4026 if (parenthesize)
4027 result << " (";
4028 result << "about " << (100 - acc_pct) << "% to evade ";
4029 if (weapon == nullptr)
4030 result << "your " << you.hand_name(true);
4031 else
4032 result << weapon->name(DESC_YOUR, false, false, false);
4033 if (parenthesize)
4034 result << ")";
4035 }
4036
_visible_to(const monster_info & mi)4037 static bool _visible_to(const monster_info& mi)
4038 {
4039 // XXX: this duplicates player::visible_to
4040 const bool invis_to = you.invisible() && !mi.can_see_invisible()
4041 && !you.in_water();
4042 return mi.attitude == ATT_FRIENDLY || (!mi.is(MB_BLIND) && !invis_to);
4043 }
4044
4045 /**
4046 * Display the % chance of a the given monster hitting the player.
4047 *
4048 * @param mi[in] Player-visible info about the monster in question.
4049 * @param result[in,out] The stringstream to append to.
4050 */
_describe_mons_to_hit(const monster_info & mi,ostringstream & result)4051 static void _describe_mons_to_hit(const monster_info& mi, ostringstream &result)
4052 {
4053 if (crawl_state.game_is_arena() || !crawl_state.need_save)
4054 return;
4055
4056 const item_def* weapon = mi.inv[MSLOT_WEAPON].get();
4057 const bool melee = weapon == nullptr || !is_range_weapon(*weapon);
4058 const bool skilled = mons_class_flag(mi.type, melee ? M_FIGHTER : M_ARCHER);
4059 const int base_to_hit = mon_to_hit_base(mi.hd, skilled, !melee);
4060 const int weapon_to_hit = weapon ? weapon->plus + property(*weapon, PWPN_HIT) : 0;
4061 const int total_base_hit = base_to_hit + weapon_to_hit;
4062
4063 int post_roll_modifiers = 0;
4064 if (mi.is(MB_CONFUSED))
4065 post_roll_modifiers += CONFUSION_TO_HIT_MALUS;
4066
4067 const bool invisible = !_visible_to(mi);
4068 if (invisible)
4069 post_roll_modifiers -= total_base_hit * 35 / 100;
4070 else
4071 {
4072 post_roll_modifiers += TRANSLUCENT_SKIN_TO_HIT_MALUS
4073 * you.get_mutation_level(MUT_TRANSLUCENT_SKIN);
4074 if (you.backlit(false))
4075 post_roll_modifiers += BACKLIGHT_TO_HIT_BONUS;
4076 if (you.umbra() && !mi.nightvision())
4077 post_roll_modifiers += UMBRA_TO_HIT_MALUS;
4078 }
4079 // We ignore pproj because monsters never have it passively.
4080
4081 // We ignore the EV penalty for not being able to see an enemy because, if you
4082 // can't see an enemy, you can't get a monster description for them. (Except through
4083 // ?/M, but let's neglect that for now.)
4084 const int ev = you.evasion();
4085
4086 const int to_land = weapon && is_unrandom_artefact(*weapon, UNRAND_SNIPER) ? AUTOMATIC_HIT :
4087 total_base_hit + post_roll_modifiers;
4088 const int beat_ev_chance = mon_to_hit_pct(to_land, ev);
4089
4090 const int shield_class = player_shield_class();
4091 const int shield_bypass = mon_shield_bypass(mi.hd);
4092 // ignore penalty for unseen attacker, as with EV above
4093 const int beat_sh_chance = mon_beat_sh_pct(shield_bypass, shield_class);
4094
4095 const int hit_chance = beat_ev_chance * beat_sh_chance / 100;
4096 result << " (about " << hit_chance << "% to hit you)";
4097 }
4098
4099 /**
4100 * Print a bar of +s and .s representing a given stat to a provided stream.
4101 *
4102 * @param value[in] The current value represented by the bar.
4103 * @param scale[in] The value that each + and . represents.
4104 * @param name The name of the bar.
4105 * @param result[in,out] The stringstream to append to.
4106 * @param base_value[in] The 'base' value represented by the bar. If
4107 * INT_MAX, is ignored.
4108 */
_print_bar(int value,int scale,string name,ostringstream & result,int base_value)4109 static void _print_bar(int value, int scale, string name,
4110 ostringstream &result, int base_value)
4111 {
4112 if (base_value == INT_MAX)
4113 base_value = value;
4114
4115 if (name.size())
4116 result << name << " ";
4117
4118 const int display_max = value ? value : base_value;
4119 const bool currently_disabled = !value && base_value;
4120
4121 if (currently_disabled)
4122 result << "none (normally ";
4123
4124 if (display_max == 0)
4125 result << "none";
4126 else
4127 {
4128 for (int i = 0; i * scale < display_max; i++)
4129 {
4130 result << "+";
4131 if (i % 5 == 4)
4132 result << " ";
4133 }
4134 }
4135
4136 if (currently_disabled)
4137 result << ")";
4138
4139 #ifdef DEBUG_DIAGNOSTICS
4140 if (!you.suppress_wizard)
4141 result << " (" << value << ")";
4142 #endif
4143
4144 #ifdef DEBUG_DIAGNOSTICS
4145 if (currently_disabled)
4146 if (!you.suppress_wizard)
4147 result << " (base: " << base_value << ")";
4148 #endif
4149 }
4150
4151 /**
4152 * Append information about a given monster's HP to the provided stream.
4153 *
4154 * @param mi[in] Player-visible info about the monster in question.
4155 * @param result[in,out] The stringstream to append to.
4156 */
_describe_monster_hp(const monster_info & mi,ostringstream & result)4157 static void _describe_monster_hp(const monster_info& mi, ostringstream &result)
4158 {
4159 result << "Max HP: " << mi.get_max_hp_desc() << "\n";
4160 }
4161
4162 /**
4163 * Append information about a given monster's AC to the provided stream.
4164 *
4165 * @param mi[in] Player-visible info about the monster in question.
4166 * @param result[in,out] The stringstream to append to.
4167 */
_describe_monster_ac(const monster_info & mi,ostringstream & result)4168 static void _describe_monster_ac(const monster_info& mi, ostringstream &result)
4169 {
4170 // MAX_GHOST_EVASION + two pips (so with EV in parens it's the same)
4171 _print_bar(mi.ac, 5, " AC:", result);
4172 result << "\n";
4173 }
4174
4175 /**
4176 * Append information about a given monster's EV to the provided stream.
4177 *
4178 * @param mi[in] Player-visible info about the monster in question.
4179 * @param result[in,out] The stringstream to append to.
4180 */
_describe_monster_ev(const monster_info & mi,ostringstream & result)4181 static void _describe_monster_ev(const monster_info& mi, ostringstream &result)
4182 {
4183 _print_bar(mi.ev, 5, " EV:", result, mi.base_ev);
4184 describe_to_hit(mi, result, true);
4185 result << "\n";
4186 }
4187
4188 /**
4189 * Append information about a given monster's WL to the provided stream.
4190 *
4191 * @param mi[in] Player-visible info about the monster in question.
4192 * @param result[in,out] The stringstream to append to.
4193 */
_describe_monster_wl(const monster_info & mi,ostringstream & result)4194 static void _describe_monster_wl(const monster_info& mi, ostringstream &result)
4195 {
4196 if (mi.willpower() == WILL_INVULN)
4197 {
4198 result << " Will: ∞\n";
4199 return;
4200 }
4201
4202 const int bar_scale = WL_PIP;
4203 _print_bar(mi.willpower(), bar_scale, " Will:", result);
4204 result << "\n";
4205 }
4206
4207 /**
4208 * Returns a string describing a given monster's habitat.
4209 *
4210 * @param mi[in] Player-visible info about the monster in question.
4211 * @return The habitat description.
4212 */
_monster_habitat_description(const monster_info & mi)4213 string _monster_habitat_description(const monster_info& mi)
4214 {
4215 const monster_type type = mons_is_job(mi.type)
4216 ? mi.draco_or_demonspawn_subspecies()
4217 : mi.type;
4218
4219 switch (mons_habitat_type(type, mi.base_type))
4220 {
4221 case HT_AMPHIBIOUS:
4222 return uppercase_first(make_stringf("%s can travel through water.\n",
4223 mi.pronoun(PRONOUN_SUBJECTIVE)));
4224 case HT_AMPHIBIOUS_LAVA:
4225 return uppercase_first(make_stringf("%s can travel through lava.\n",
4226 mi.pronoun(PRONOUN_SUBJECTIVE)));
4227 default:
4228 return "";
4229 }
4230 }
4231
4232 // Size adjectives
4233 const char* const size_adj[] =
4234 {
4235 "tiny",
4236 "very small",
4237 "small",
4238 "medium",
4239 "large",
4240 "very large",
4241 "giant",
4242 };
4243 COMPILE_CHECK(ARRAYSZ(size_adj) == NUM_SIZE_LEVELS);
4244
4245 // This is used in monster description and on '%' screen for player size
get_size_adj(const size_type size,bool ignore_medium)4246 const char* get_size_adj(const size_type size, bool ignore_medium)
4247 {
4248 ASSERT_RANGE(size, 0, ARRAYSZ(size_adj));
4249 if (ignore_medium && size == SIZE_MEDIUM)
4250 return nullptr; // don't mention medium size
4251 return size_adj[size];
4252 }
4253
_monster_current_target_description(const monster_info & mi)4254 static string _monster_current_target_description(const monster_info &mi)
4255 {
4256 // is it morally wrong to use pos to get the actual monster? Possibly...
4257 if (!in_bounds(mi.pos) || !monster_at(mi.pos))
4258 return "";
4259 const monster *m = monster_at(mi.pos);
4260 ostringstream result;
4261 if (mi.is(MB_ALLY_TARGET))
4262 {
4263 auto allies = find_allies_targeting(*m);
4264 if (allies.size() == 1)
4265 result << "It is currently targeted by " << allies[0]->name(DESC_YOUR) << ".\n";
4266 else
4267 {
4268 result << "It is currently targeted by allies:\n";
4269 for (auto *a : allies)
4270 result << " " << a->name(DESC_YOUR) << "\n";
4271 }
4272 }
4273
4274 // TODO: this might be ambiguous, give a relative position?
4275 if (mi.attitude == ATT_FRIENDLY && m->get_foe())
4276 result << "It is currently targeting " << m->get_foe()->name(DESC_THE) << ".\n";
4277
4278 return result.str();
4279 }
4280
4281 // Describe a monster's (intrinsic) resistances, speed and a few other
4282 // attributes.
_monster_stat_description(const monster_info & mi)4283 static string _monster_stat_description(const monster_info& mi)
4284 {
4285 if (mons_is_sensed(mi.type) || mons_is_projectile(mi.type))
4286 return "";
4287
4288 ostringstream result;
4289
4290 _describe_monster_hp(mi, result);
4291 _describe_monster_ac(mi, result);
4292 _describe_monster_ev(mi, result);
4293 _describe_monster_wl(mi, result);
4294
4295 result << "\n";
4296
4297 resists_t resist = mi.resists();
4298
4299 const mon_resist_flags resists[] =
4300 {
4301 MR_RES_ELEC, MR_RES_POISON, MR_RES_FIRE,
4302 MR_RES_STEAM, MR_RES_COLD, MR_RES_ACID,
4303 MR_RES_MIASMA, MR_RES_NEG, MR_RES_DAMNATION,
4304 MR_RES_VORTEX,
4305 };
4306
4307 vector<string> extreme_resists;
4308 vector<string> high_resists;
4309 vector<string> base_resists;
4310 vector<string> suscept;
4311
4312 for (mon_resist_flags rflags : resists)
4313 {
4314 int level = get_resist(resist, rflags);
4315
4316 if (level != 0)
4317 {
4318 const char* attackname = _get_resist_name(rflags);
4319 if (rflags == MR_RES_DAMNATION || rflags == MR_RES_VORTEX)
4320 level = 3; // one level is immunity
4321 level = max(level, -1);
4322 level = min(level, 3);
4323 switch (level)
4324 {
4325 case -1:
4326 suscept.emplace_back(attackname);
4327 break;
4328 case 1:
4329 base_resists.emplace_back(attackname);
4330 break;
4331 case 2:
4332 high_resists.emplace_back(attackname);
4333 break;
4334 case 3:
4335 extreme_resists.emplace_back(attackname);
4336 break;
4337 }
4338 }
4339 }
4340
4341 if (mi.props.exists(CLOUD_IMMUNE_MB_KEY) && mi.props[CLOUD_IMMUNE_MB_KEY])
4342 extreme_resists.emplace_back("clouds of all kinds");
4343
4344 vector<string> resist_descriptions;
4345 if (!extreme_resists.empty())
4346 {
4347 const string tmp = "immune to "
4348 + comma_separated_line(extreme_resists.begin(),
4349 extreme_resists.end());
4350 resist_descriptions.push_back(tmp);
4351 }
4352 if (!high_resists.empty())
4353 {
4354 const string tmp = "very resistant to "
4355 + comma_separated_line(high_resists.begin(), high_resists.end());
4356 resist_descriptions.push_back(tmp);
4357 }
4358 if (!base_resists.empty())
4359 {
4360 const string tmp = "resistant to "
4361 + comma_separated_line(base_resists.begin(), base_resists.end());
4362 resist_descriptions.push_back(tmp);
4363 }
4364
4365 const char* pronoun = mi.pronoun(PRONOUN_SUBJECTIVE);
4366 const bool plural = mi.pronoun_plurality();
4367
4368 if (mi.threat != MTHRT_UNDEF)
4369 {
4370 result << uppercase_first(pronoun) << " "
4371 << conjugate_verb("look", plural) << " "
4372 << _get_threat_desc(mi.threat) << ".\n";
4373 }
4374
4375 if (!resist_descriptions.empty())
4376 {
4377 result << uppercase_first(pronoun) << " "
4378 << conjugate_verb("are", plural) << " "
4379 << comma_separated_line(resist_descriptions.begin(),
4380 resist_descriptions.end(),
4381 "; and ", "; ")
4382 << ".\n";
4383 }
4384
4385 // Is monster susceptible to anything? (On a new line.)
4386 if (!suscept.empty())
4387 {
4388 result << uppercase_first(pronoun) << " "
4389 << conjugate_verb("are", plural) << " susceptible to "
4390 << comma_separated_line(suscept.begin(), suscept.end())
4391 << ".\n";
4392 }
4393
4394 if (mi.is(MB_CHAOTIC))
4395 {
4396 result << uppercase_first(pronoun) << " "
4397 << conjugate_verb("are", plural)
4398 << " vulnerable to silver and hated by Zin.\n";
4399 }
4400
4401 if (mons_class_flag(mi.type, M_STATIONARY)
4402 && !mons_is_tentacle_or_tentacle_segment(mi.type))
4403 {
4404 result << uppercase_first(pronoun) << " cannot move.\n";
4405 }
4406
4407 if (mons_class_flag(mi.type, M_COLD_BLOOD)
4408 && get_resist(resist, MR_RES_COLD) <= 0)
4409 {
4410 result << uppercase_first(pronoun)
4411 << " " << conjugate_verb("are", plural)
4412 << " cold-blooded and may be slowed by cold attacks.\n";
4413 }
4414
4415 // Seeing invisible.
4416 if (mi.can_see_invisible())
4417 result << uppercase_first(pronoun) << " can see invisible.\n";
4418
4419 // Echolocation, wolf noses, jellies, etc
4420 if (!mons_can_be_blinded(mi.type))
4421 {
4422 result << uppercase_first(pronoun) << " "
4423 << conjugate_verb("are", plural)
4424 << " immune to blinding.\n";
4425 }
4426 // XXX: could mention "immune to dazzling" here, but that's spammy, since
4427 // it's true of such a huge number of monsters. (undead, statues, plants).
4428 // Might be better to have some place where players can see holiness &
4429 // information about holiness.......?
4430
4431 if (mi.intel() <= I_BRAINLESS)
4432 {
4433 // Matters for Ely.
4434 result << uppercase_first(pronoun) << " "
4435 << conjugate_verb("are", plural) << " mindless.\n";
4436 }
4437 else if (mi.intel() >= I_HUMAN)
4438 {
4439 // Matters for Yred, Gozag, Zin, TSO, Alistair....
4440 result << uppercase_first(pronoun) << " "
4441 << conjugate_verb("are", plural) << " intelligent.\n";
4442 }
4443
4444 // Unusual monster speed.
4445 const int speed = mi.base_speed();
4446 bool did_speed = false;
4447 if (speed != 10 && speed != 0)
4448 {
4449 did_speed = true;
4450 result << uppercase_first(pronoun) << " "
4451 << conjugate_verb("are", plural) << " "
4452 << mi.speed_description();
4453 }
4454 const mon_energy_usage def = DEFAULT_ENERGY;
4455 if (!(mi.menergy == def))
4456 {
4457 const mon_energy_usage me = mi.menergy;
4458 vector<string> fast, slow;
4459 if (!did_speed)
4460 result << uppercase_first(pronoun) << " ";
4461 _add_energy_to_string(speed, me.move,
4462 conjugate_verb("cover", plural) + " ground",
4463 fast, slow);
4464 // since MOVE_ENERGY also sets me.swim
4465 if (me.swim != me.move)
4466 {
4467 _add_energy_to_string(speed, me.swim,
4468 conjugate_verb("swim", plural), fast, slow);
4469 }
4470 _add_energy_to_string(speed, me.attack,
4471 conjugate_verb("attack", plural), fast, slow);
4472 if (mons_class_itemuse(mi.type) >= MONUSE_STARTING_EQUIPMENT)
4473 {
4474 _add_energy_to_string(speed, me.missile,
4475 conjugate_verb("shoot", plural), fast, slow);
4476 }
4477 _add_energy_to_string(
4478 speed, me.spell,
4479 mi.is_actual_spellcaster() ? conjugate_verb("cast", plural)
4480 + " spells" :
4481 mi.is_priest() ? conjugate_verb("use", plural)
4482 + " invocations"
4483 : conjugate_verb("use", plural)
4484 + " natural abilities", fast, slow);
4485 _add_energy_to_string(speed, me.special,
4486 conjugate_verb("use", plural)
4487 + " special abilities",
4488 fast, slow);
4489 if (mons_class_itemuse(mi.type) >= MONUSE_STARTING_EQUIPMENT)
4490 {
4491 _add_energy_to_string(speed, me.item,
4492 conjugate_verb("use", plural) + " items",
4493 fast, slow);
4494 }
4495
4496 if (speed >= 10)
4497 {
4498 if (did_speed && fast.size() == 1)
4499 result << " and " << fast[0];
4500 else if (!fast.empty())
4501 {
4502 if (did_speed)
4503 result << ", ";
4504 result << comma_separated_line(fast.begin(), fast.end());
4505 }
4506 if (!slow.empty())
4507 {
4508 if (did_speed || !fast.empty())
4509 result << ", but ";
4510 result << comma_separated_line(slow.begin(), slow.end());
4511 }
4512 }
4513 else if (speed < 10)
4514 {
4515 if (did_speed && slow.size() == 1)
4516 result << " and " << slow[0];
4517 else if (!slow.empty())
4518 {
4519 if (did_speed)
4520 result << ", ";
4521 result << comma_separated_line(slow.begin(), slow.end());
4522 }
4523 if (!fast.empty())
4524 {
4525 if (did_speed || !slow.empty())
4526 result << ", but ";
4527 result << comma_separated_line(fast.begin(), fast.end());
4528 }
4529 }
4530 result << ".\n";
4531 }
4532 else if (did_speed)
4533 result << ".\n";
4534
4535 if (mi.type == MONS_SHADOW)
4536 {
4537 // Cf. monster::action_energy() in monster.cc.
4538 result << uppercase_first(pronoun) << " "
4539 << conjugate_verb("cover", plural)
4540 << " ground more quickly when invisible.\n";
4541 }
4542
4543 if (mi.airborne())
4544 result << uppercase_first(pronoun) << " can fly.\n";
4545
4546 // Unusual regeneration rates.
4547 if (!mi.can_regenerate())
4548 result << uppercase_first(pronoun) << " cannot regenerate.\n";
4549 else if (mons_class_fast_regen(mi.type))
4550 result << uppercase_first(pronoun) << " "
4551 << conjugate_verb("regenerate", plural)
4552 << " quickly.\n";
4553
4554 const char* mon_size = get_size_adj(mi.body_size(), true);
4555 if (mon_size)
4556 {
4557 result << uppercase_first(pronoun) << " "
4558 << conjugate_verb("are", plural) << " "
4559 << mon_size << ".\n";
4560 }
4561
4562 if (in_good_standing(GOD_ZIN, 0) && !mi.pos.origin() && monster_at(mi.pos))
4563 {
4564 recite_counts retval;
4565 monster *m = monster_at(mi.pos);
4566 auto eligibility = zin_check_recite_to_single_monster(m, retval);
4567 if (eligibility == RE_INELIGIBLE)
4568 {
4569 result << uppercase_first(pronoun) <<
4570 " cannot be affected by reciting Zin's laws.";
4571 }
4572 else if (eligibility == RE_TOO_STRONG)
4573 {
4574 result << uppercase_first(pronoun) << " "
4575 << conjugate_verb("are", plural)
4576 << " too strong to be affected by reciting Zin's laws.";
4577 }
4578 else // RE_ELIGIBLE || RE_RECITE_TIMER
4579 {
4580 result << uppercase_first(pronoun) <<
4581 " can be affected by reciting Zin's laws.";
4582 }
4583
4584 if (you.wizard)
4585 {
4586 result << " (Recite power:" << zin_recite_power()
4587 << ", Hit dice:" << mi.hd << ")";
4588 }
4589 result << "\n";
4590 }
4591
4592 result << _monster_attacks_description(mi);
4593 result << _monster_missiles_description(mi);
4594 result << _monster_habitat_description(mi);
4595 result << _monster_spells_description(mi);
4596
4597 return result.str();
4598 }
4599
serpent_of_hell_branch(monster_type m)4600 branch_type serpent_of_hell_branch(monster_type m)
4601 {
4602 switch (m)
4603 {
4604 case MONS_SERPENT_OF_HELL_COCYTUS:
4605 return BRANCH_COCYTUS;
4606 case MONS_SERPENT_OF_HELL_DIS:
4607 return BRANCH_DIS;
4608 case MONS_SERPENT_OF_HELL_TARTARUS:
4609 return BRANCH_TARTARUS;
4610 case MONS_SERPENT_OF_HELL:
4611 return BRANCH_GEHENNA;
4612 default:
4613 die("bad serpent of hell monster_type");
4614 }
4615 }
4616
serpent_of_hell_flavour(monster_type m)4617 string serpent_of_hell_flavour(monster_type m)
4618 {
4619 return lowercase_string(branches[serpent_of_hell_branch(m)].shortname);
4620 }
4621
4622 // Fetches the monster's database description and reads it into inf.
get_monster_db_desc(const monster_info & mi,describe_info & inf,bool & has_stat_desc)4623 void get_monster_db_desc(const monster_info& mi, describe_info &inf,
4624 bool &has_stat_desc)
4625 {
4626 if (inf.title.empty())
4627 inf.title = getMiscString(mi.common_name(DESC_DBNAME) + " title");
4628 if (inf.title.empty())
4629 inf.title = uppercase_first(mi.full_name(DESC_A)) + ".";
4630
4631 string db_name;
4632
4633 if (mi.props.exists("dbname"))
4634 db_name = mi.props["dbname"].get_string();
4635 else if (mi.mname.empty())
4636 db_name = mi.db_name();
4637 else
4638 db_name = mi.full_name(DESC_PLAIN);
4639
4640 if (mons_species(mi.type) == MONS_SERPENT_OF_HELL)
4641 db_name += " " + serpent_of_hell_flavour(mi.type);
4642
4643 // This is somewhat hackish, but it's a good way of over-riding monsters'
4644 // descriptions in Lua vaults by using MonPropsMarker. This is also the
4645 // method used by set_feature_desc_long, etc. {due}
4646 if (!mi.description.empty())
4647 inf.body << mi.description;
4648 // Don't get description for player ghosts.
4649 else if (mi.type != MONS_PLAYER_GHOST
4650 && mi.type != MONS_PLAYER_ILLUSION)
4651 {
4652 inf.body << getLongDescription(db_name);
4653 }
4654
4655 // And quotes {due}
4656 if (!mi.quote.empty())
4657 inf.quote = mi.quote;
4658 else
4659 inf.quote = getQuoteString(db_name);
4660
4661 string symbol;
4662 symbol += get_monster_data(mi.type)->basechar;
4663 if (isaupper(symbol[0]))
4664 symbol = "cap-" + symbol;
4665
4666 string quote2;
4667 if (!mons_is_unique(mi.type))
4668 {
4669 string symbol_prefix = "__" + symbol + "_prefix";
4670 inf.prefix = getLongDescription(symbol_prefix);
4671
4672 string symbol_suffix = "__" + symbol + "_suffix";
4673 quote2 = getQuoteString(symbol_suffix);
4674 }
4675
4676 if (!inf.quote.empty() && !quote2.empty())
4677 inf.quote += "\n";
4678 inf.quote += quote2;
4679
4680 const string it = mi.pronoun(PRONOUN_SUBJECTIVE);
4681 const string it_o = mi.pronoun(PRONOUN_OBJECTIVE);
4682 const string It = uppercase_first(it);
4683 const string is = conjugate_verb("are", mi.pronoun_plurality());
4684
4685 switch (mi.type)
4686 {
4687 case MONS_RED_DRACONIAN:
4688 case MONS_WHITE_DRACONIAN:
4689 case MONS_GREEN_DRACONIAN:
4690 case MONS_PALE_DRACONIAN:
4691 case MONS_BLACK_DRACONIAN:
4692 case MONS_YELLOW_DRACONIAN:
4693 case MONS_PURPLE_DRACONIAN:
4694 case MONS_GREY_DRACONIAN:
4695 case MONS_DRACONIAN_SHIFTER:
4696 case MONS_DRACONIAN_SCORCHER:
4697 case MONS_DRACONIAN_ANNIHILATOR:
4698 case MONS_DRACONIAN_STORMCALLER:
4699 case MONS_DRACONIAN_MONK:
4700 case MONS_DRACONIAN_KNIGHT:
4701 {
4702 inf.body << "\n" << _describe_draconian(mi) << "\n";
4703 break;
4704 }
4705
4706 case MONS_MONSTROUS_DEMONSPAWN:
4707 case MONS_GELID_DEMONSPAWN:
4708 case MONS_INFERNAL_DEMONSPAWN:
4709 case MONS_TORTUROUS_DEMONSPAWN:
4710 case MONS_BLOOD_SAINT:
4711 case MONS_WARMONGER:
4712 case MONS_CORRUPTER:
4713 case MONS_BLACK_SUN:
4714 {
4715 inf.body << "\n" << _describe_demonspawn(mi) << "\n";
4716 break;
4717 }
4718
4719 case MONS_PLAYER_GHOST:
4720 inf.body << "The apparition of " << get_ghost_description(mi) << ".\n";
4721 if (mi.props.exists(MIRRORED_GHOST_KEY))
4722 inf.body << "It looks just like you...spooky!\n";
4723 break;
4724
4725 case MONS_PLAYER_ILLUSION:
4726 inf.body << "An illusion of " << get_ghost_description(mi) << ".\n";
4727 break;
4728
4729 case MONS_PANDEMONIUM_LORD:
4730 inf.body << _describe_demon(mi.mname, mi.airborne()) << "\n";
4731 break;
4732
4733 case MONS_MUTANT_BEAST:
4734 // vault renames get their own descriptions
4735 if (mi.mname.empty() || !mi.is(MB_NAME_REPLACE))
4736 inf.body << _describe_mutant_beast(mi) << "\n";
4737 break;
4738
4739 case MONS_BLOCK_OF_ICE:
4740 if (mi.is(MB_SLOWLY_DYING))
4741 inf.body << "\nIt is quickly melting away.\n";
4742 break;
4743
4744 case MONS_BRIAR_PATCH: // death msg uses "crumbling"
4745 case MONS_PILLAR_OF_SALT:
4746 // XX why are these "quick" here but "slow" elsewhere??
4747 if (mi.is(MB_SLOWLY_DYING))
4748 inf.body << "\nIt is quickly crumbling away.\n";
4749 break;
4750
4751 case MONS_PROGRAM_BUG:
4752 inf.body << "If this monster is a \"program bug\", then it's "
4753 "recommended that you save your game and reload. Please report "
4754 "monsters who masquerade as program bugs or run around the "
4755 "dungeon without a proper description to the authorities.\n";
4756 break;
4757
4758 default:
4759 break;
4760 }
4761
4762 if (mons_class_is_fragile(mi.type))
4763 {
4764 if (mi.is(MB_CRUMBLING))
4765 inf.body << "\nIt is quickly crumbling away.\n";
4766 else if (mi.is(MB_WITHERING))
4767 inf.body << "\nIt is quickly withering away.\n";
4768 else
4769 inf.body << "\nIf struck, it will die soon after.\n";
4770 }
4771
4772 if (!mons_is_unique(mi.type))
4773 {
4774 string symbol_suffix = "__";
4775 symbol_suffix += symbol;
4776 symbol_suffix += "_suffix";
4777
4778 string suffix = getLongDescription(symbol_suffix)
4779 + getLongDescription(symbol_suffix + "_examine");
4780
4781 if (!suffix.empty())
4782 inf.body << "\n" << suffix;
4783 }
4784
4785 const int curse_power = mummy_curse_power(mi.type);
4786 if (curse_power && !mi.is(MB_SUMMONED))
4787 {
4788 inf.body << "\n" << It << " will inflict a ";
4789 if (curse_power > 10)
4790 inf.body << "powerful ";
4791 inf.body << "necromantic curse on "
4792 << mi.pronoun(PRONOUN_POSSESSIVE) << " foe when destroyed.\n";
4793 }
4794
4795 // Get information on resistances, speed, etc.
4796 string result = _monster_stat_description(mi);
4797 if (!result.empty())
4798 {
4799 inf.body << "\n" << result;
4800 has_stat_desc = true;
4801 }
4802
4803 bool did_stair_use = false;
4804 if (!mons_class_can_use_stairs(mi.type))
4805 {
4806 inf.body << It << " " << is << " incapable of using stairs.\n";
4807 did_stair_use = true;
4808 }
4809
4810 result = _monster_current_target_description(mi);
4811 if (!result.empty())
4812 inf.body << "\n" << result;
4813
4814 if (mi.is(MB_SUMMONED) || mi.is(MB_PERM_SUMMON))
4815 {
4816 inf.body << "\nThis monster has been summoned"
4817 << (mi.is(MB_SUMMONED) ? ", and is thus only temporary. "
4818 : " in a durable way. ");
4819 // TODO: hacks; convert angered_by_attacks to a monster_info check
4820 // (but on the other hand, it is really limiting to not have access
4821 // to the monster...)
4822 if (!mi.pos.origin() && monster_at(mi.pos)
4823 && monster_at(mi.pos)->angered_by_attacks()
4824 && mi.attitude == ATT_FRIENDLY)
4825 {
4826 inf.body << "If angered " << it
4827 << " will immediately vanish, yielding ";
4828 }
4829 else
4830 inf.body << "Killing " << it_o << " yields ";
4831 inf.body << "no experience or items";
4832
4833 if (!did_stair_use && !mi.is(MB_PERM_SUMMON))
4834 inf.body << "; " << it << " " << is << " incapable of using stairs";
4835
4836 if (mi.is(MB_PERM_SUMMON))
4837 inf.body << " and " << it << " cannot be abjured";
4838
4839 inf.body << ".\n";
4840 }
4841 else if (mi.is(MB_NO_REWARD))
4842 inf.body << "\nKilling this monster yields no experience or items.";
4843 else if (mons_class_leaves_hide(mi.type))
4844 {
4845 inf.body << "\nIf " << it << " " << is <<
4846 " slain, it may be possible to recover "
4847 << mi.pronoun(PRONOUN_POSSESSIVE)
4848 << " hide, which can be used as armour.\n";
4849 }
4850
4851 if (mi.is(MB_SUMMONED_CAPPED))
4852 {
4853 inf.body << "\nYou have summoned too many monsters of this kind to "
4854 "sustain them all, and thus this one will shortly "
4855 "expire.\n";
4856 }
4857
4858 if (!inf.quote.empty())
4859 inf.quote += "\n";
4860
4861 #ifdef DEBUG_DIAGNOSTICS
4862 if (you.suppress_wizard)
4863 return;
4864 if (mi.pos.origin() || !monster_at(mi.pos))
4865 return; // not a real monster
4866 monster& mons = *monster_at(mi.pos);
4867
4868 if (mons.has_originating_map())
4869 {
4870 inf.body << make_stringf("\nPlaced by map: %s",
4871 mons.originating_map().c_str());
4872 }
4873
4874 inf.body << "\nMonster health: "
4875 << mons.hit_points << "/" << mons.max_hit_points << "\n";
4876
4877 const actor *mfoe = mons.get_foe();
4878 inf.body << "Monster foe: "
4879 << (mfoe? mfoe->name(DESC_PLAIN, true)
4880 : "(none)");
4881
4882 vector<string> attitude;
4883 if (mons.friendly())
4884 attitude.emplace_back("friendly");
4885 if (mons.neutral())
4886 attitude.emplace_back("neutral");
4887 if (mons.good_neutral())
4888 attitude.emplace_back("good_neutral");
4889 if (mons.strict_neutral())
4890 attitude.emplace_back("strict_neutral");
4891 if (mons.pacified())
4892 attitude.emplace_back("pacified");
4893 if (mons.wont_attack())
4894 attitude.emplace_back("wont_attack");
4895 if (!attitude.empty())
4896 {
4897 string att = comma_separated_line(attitude.begin(), attitude.end(),
4898 "; ", "; ");
4899 if (mons.has_ench(ENCH_INSANE))
4900 inf.body << "; frenzied and insane (otherwise " << att << ")";
4901 else
4902 inf.body << "; " << att;
4903 }
4904 else if (mons.has_ench(ENCH_INSANE))
4905 inf.body << "; frenzied and insane";
4906
4907 inf.body << "\n\nHas holiness: ";
4908 inf.body << holiness_description(mi.holi);
4909 inf.body << ".";
4910
4911 const monster_spells &hspell_pass = mons.spells;
4912 bool found_spell = false;
4913
4914 for (unsigned int i = 0; i < hspell_pass.size(); ++i)
4915 {
4916 if (!found_spell)
4917 {
4918 inf.body << "\n\nMonster Spells:\n";
4919 found_spell = true;
4920 }
4921
4922 inf.body << " " << i << ": "
4923 << spell_title(hspell_pass[i].spell)
4924 << " (";
4925 if (hspell_pass[i].flags & MON_SPELL_EMERGENCY)
4926 inf.body << "emergency, ";
4927 if (hspell_pass[i].flags & MON_SPELL_NATURAL)
4928 inf.body << "natural, ";
4929 if (hspell_pass[i].flags & MON_SPELL_MAGICAL)
4930 inf.body << "magical, ";
4931 if (hspell_pass[i].flags & MON_SPELL_WIZARD)
4932 inf.body << "wizard, ";
4933 if (hspell_pass[i].flags & MON_SPELL_PRIEST)
4934 inf.body << "priest, ";
4935 if (hspell_pass[i].flags & MON_SPELL_VOCAL)
4936 inf.body << "vocal, ";
4937 if (hspell_pass[i].flags & MON_SPELL_BREATH)
4938 inf.body << "breath, ";
4939 inf.body << (int) hspell_pass[i].freq << ")";
4940 }
4941
4942 bool has_item = false;
4943 for (mon_inv_iterator ii(mons); ii; ++ii)
4944 {
4945 if (!has_item)
4946 {
4947 inf.body << "\n\nMonster Inventory:\n";
4948 has_item = true;
4949 }
4950 inf.body << " " << ii.slot() << ": "
4951 << ii->name(DESC_A, false, true);
4952 }
4953
4954 if (mons.props.exists("blame"))
4955 {
4956 inf.body << "\n\nMonster blame chain:\n";
4957
4958 const CrawlVector& blame = mons.props["blame"].get_vector();
4959
4960 for (const auto &entry : blame)
4961 inf.body << " " << entry.get_string() << "\n";
4962 }
4963 inf.body << "\n\n" << debug_constriction_string(&mons);
4964 #endif
4965 }
4966
describe_monsters(const monster_info & mi,const string &)4967 int describe_monsters(const monster_info &mi, const string& /*footer*/)
4968 {
4969 bool has_stat_desc = false;
4970 describe_info inf;
4971 formatted_string desc;
4972
4973 get_monster_db_desc(mi, inf, has_stat_desc);
4974
4975 spellset spells = monster_spellset(mi);
4976
4977 auto vbox = make_shared<Box>(Widget::VERT);
4978 auto title_hbox = make_shared<Box>(Widget::HORZ);
4979
4980 #ifdef USE_TILE_LOCAL
4981 auto dgn = make_shared<Dungeon>();
4982 dgn->width = dgn->height = 1;
4983 dgn->buf().add_monster(mi, 0, 0);
4984 title_hbox->add_child(move(dgn));
4985 #endif
4986
4987 auto title = make_shared<Text>();
4988 title->set_text(inf.title);
4989 title->set_margin_for_sdl(0, 0, 0, 10);
4990 title_hbox->add_child(move(title));
4991
4992 title_hbox->set_cross_alignment(Widget::CENTER);
4993 title_hbox->set_margin_for_crt(0, 0, 1, 0);
4994 title_hbox->set_margin_for_sdl(0, 0, 20, 0);
4995 vbox->add_child(move(title_hbox));
4996
4997 desc += inf.body.str();
4998 if (crawl_state.game_is_hints())
4999 desc += formatted_string(hints_describe_monster(mi, has_stat_desc));
5000 desc += inf.footer;
5001 desc = formatted_string::parse_string(trimmed_string(desc));
5002
5003 const formatted_string quote = formatted_string(trimmed_string(inf.quote));
5004
5005 auto desc_sw = make_shared<Switcher>();
5006 auto more_sw = make_shared<Switcher>();
5007 desc_sw->current() = 0;
5008 more_sw->current() = 0;
5009
5010 #ifdef USE_TILE_LOCAL
5011 # define MORE_PREFIX "[<w>!</w>" "|<w>Right-click</w>" "]: "
5012 #else
5013 # define MORE_PREFIX "[<w>!</w>" "]: "
5014 #endif
5015
5016 const char* mores[2] = {
5017 MORE_PREFIX "<w>Description</w>|Quote",
5018 MORE_PREFIX "Description|<w>Quote</w>",
5019 };
5020
5021 for (int i = 0; i < (inf.quote.empty() ? 1 : 2); i++)
5022 {
5023 const formatted_string *content[2] = { &desc, "e };
5024 auto scroller = make_shared<Scroller>();
5025 auto text = make_shared<Text>(content[i]->trim());
5026 text->set_wrap_text(true);
5027 scroller->set_child(text);
5028 desc_sw->add_child(move(scroller));
5029
5030 more_sw->add_child(make_shared<Text>(
5031 formatted_string::parse_string(mores[i])));
5032 }
5033
5034 more_sw->set_margin_for_sdl(20, 0, 0, 0);
5035 more_sw->set_margin_for_crt(1, 0, 0, 0);
5036 desc_sw->expand_h = false;
5037 desc_sw->align_x = Widget::STRETCH;
5038 vbox->add_child(desc_sw);
5039 if (!inf.quote.empty())
5040 vbox->add_child(more_sw);
5041
5042 #ifdef USE_TILE_LOCAL
5043 vbox->max_size().width = tiles.get_crt_font()->char_width()*80;
5044 #endif
5045
5046 auto popup = make_shared<ui::Popup>(move(vbox));
5047
5048 bool done = false;
5049 int lastch;
5050 popup->on_keydown_event([&](const KeyEvent& ev) {
5051 const auto key = ev.key();
5052 lastch = key;
5053 done = key == CK_ESCAPE;
5054 if (!inf.quote.empty() && (key == '!' || key == CK_MOUSE_CMD))
5055 {
5056 int n = (desc_sw->current() + 1) % 2;
5057 desc_sw->current() = more_sw->current() = n;
5058 #ifdef USE_TILE_WEB
5059 tiles.json_open_object();
5060 tiles.json_write_int("pane", n);
5061 tiles.ui_state_change("describe-monster", 0);
5062 #endif
5063 }
5064 if (desc_sw->current_widget()->on_event(ev))
5065 return true;
5066 const vector<pair<spell_type,char>> spell_map = map_chars_to_spells(spells, nullptr);
5067 auto entry = find_if(spell_map.begin(), spell_map.end(),
5068 [key](const pair<spell_type,char>& e) { return e.second == key; });
5069 if (entry == spell_map.end())
5070 return false;
5071 describe_spell(entry->first, &mi, nullptr);
5072 return true;
5073 });
5074
5075 #ifdef USE_TILE_WEB
5076 tiles.json_open_object();
5077 tiles.json_write_string("title", inf.title);
5078 formatted_string needle;
5079 describe_spellset(spells, nullptr, needle, &mi);
5080 string desc_without_spells = desc.to_colour_string();
5081 if (!needle.empty())
5082 {
5083 desc_without_spells = replace_all(desc_without_spells,
5084 needle.to_colour_string(), "SPELLSET_PLACEHOLDER");
5085 }
5086 tiles.json_write_string("body", desc_without_spells);
5087 tiles.json_write_string("quote", quote);
5088 write_spellset(spells, nullptr, &mi);
5089
5090 {
5091 tileidx_t t = tileidx_monster(mi);
5092 tileidx_t t0 = t & TILE_FLAG_MASK;
5093 tileidx_t flag = t & (~TILE_FLAG_MASK);
5094
5095 if (!mons_class_is_stationary(mi.type) || mi.type == MONS_TRAINING_DUMMY)
5096 {
5097 tileidx_t mcache_idx = mcache.register_monster(mi);
5098 t = flag | (mcache_idx ? mcache_idx : t0);
5099 t0 = t & TILE_FLAG_MASK;
5100 }
5101
5102 tiles.json_write_int("fg_idx", t0);
5103 tiles.json_write_name("flag");
5104 tiles.write_tileidx(flag);
5105
5106 if (t0 >= TILEP_MCACHE_START)
5107 {
5108 mcache_entry *entry = mcache.get(t0);
5109 if (entry)
5110 tiles.send_mcache(entry, false);
5111 else
5112 {
5113 tiles.json_write_comma();
5114 tiles.write_message("\"doll\":[[%d,%d]]", TILEP_MONS_UNKNOWN, TILE_Y);
5115 tiles.json_write_null("mcache");
5116 }
5117 }
5118 else if (get_tile_texture(t0) == TEX_PLAYER)
5119 {
5120 tiles.json_write_comma();
5121 tiles.write_message("\"doll\":[[%u,%d]]", (unsigned int) t0, TILE_Y);
5122 tiles.json_write_null("mcache");
5123 }
5124 }
5125 tiles.push_ui_layout("describe-monster", 1);
5126 popup->on_layout_pop([](){ tiles.pop_ui_layout(); });
5127 #endif
5128
5129 ui::run_layout(move(popup), done);
5130
5131 return lastch;
5132 }
5133
5134 static const char* xl_rank_names[] =
5135 {
5136 "weakling",
5137 "amateur",
5138 "novice",
5139 "journeyman",
5140 "adept",
5141 "veteran",
5142 "master",
5143 "legendary"
5144 };
5145
_xl_rank_name(const int xl_rank)5146 static string _xl_rank_name(const int xl_rank)
5147 {
5148 const string rank = xl_rank_names[xl_rank];
5149
5150 return article_a(rank);
5151 }
5152
short_ghost_description(const monster * mon,bool abbrev)5153 string short_ghost_description(const monster *mon, bool abbrev)
5154 {
5155 ASSERT(mons_is_pghost(mon->type));
5156
5157 const ghost_demon &ghost = *(mon->ghost);
5158 const char* rank = xl_rank_names[ghost_level_to_rank(ghost.xl)];
5159
5160 string desc = make_stringf("%s %s %s", rank,
5161 species::name(ghost.species).c_str(),
5162 get_job_name(ghost.job));
5163
5164 if (abbrev || strwidth(desc) > 40)
5165 {
5166 desc = make_stringf("%s %s%s",
5167 rank,
5168 species::get_abbrev(ghost.species),
5169 get_job_abbrev(ghost.job));
5170 }
5171
5172 return desc;
5173 }
5174
5175 // Describes the current ghost's previous owner. The caller must
5176 // prepend "The apparition of" or whatever and append any trailing
5177 // punctuation that's wanted.
get_ghost_description(const monster_info & mi,bool concise)5178 string get_ghost_description(const monster_info &mi, bool concise)
5179 {
5180 ostringstream gstr;
5181
5182 const species_type gspecies = mi.i_ghost.species;
5183
5184 gstr << mi.mname << " the "
5185 << skill_title_by_rank(mi.i_ghost.best_skill,
5186 mi.i_ghost.best_skill_rank,
5187 gspecies,
5188 species::has_low_str(gspecies), mi.i_ghost.religion)
5189 << ", " << _xl_rank_name(mi.i_ghost.xl_rank) << " ";
5190
5191 if (concise)
5192 {
5193 gstr << species::get_abbrev(gspecies)
5194 << get_job_abbrev(mi.i_ghost.job);
5195 }
5196 else
5197 {
5198 gstr << species::name(gspecies)
5199 << " "
5200 << get_job_name(mi.i_ghost.job);
5201 }
5202
5203 if (mi.i_ghost.religion != GOD_NO_GOD)
5204 {
5205 gstr << " of "
5206 << god_name(mi.i_ghost.religion);
5207 }
5208
5209 return gstr.str();
5210 }
5211
describe_skill(skill_type skill)5212 void describe_skill(skill_type skill)
5213 {
5214 describe_info inf;
5215 inf.title = skill_name(skill);
5216 inf.body << get_skill_description(skill, false);
5217 tile_def tile = tile_def(tileidx_skill(skill, TRAINING_ENABLED));
5218 show_description(inf, &tile);
5219 }
5220
5221 // only used in tiles
get_command_description(const command_type cmd,bool terse)5222 string get_command_description(const command_type cmd, bool terse)
5223 {
5224 string lookup = command_to_name(cmd);
5225
5226 if (!terse)
5227 lookup += " verbose";
5228
5229 string result = getLongDescription(lookup);
5230 if (result.empty())
5231 {
5232 if (!terse)
5233 {
5234 // Try for the terse description.
5235 result = get_command_description(cmd, true);
5236 if (!result.empty())
5237 return result + ".";
5238 }
5239 return command_to_name(cmd);
5240 }
5241
5242 return result.substr(0, result.length() - 1);
5243 }
5244
5245 /**
5246 * Provide auto-generated information about the given cloud type. Describe
5247 * opacity & related factors.
5248 *
5249 * @param cloud_type The cloud_type in question.
5250 * @return e.g. "\nThis cloud is opaque; one tile will not block vision, but
5251 * multiple will. \n\nClouds of this kind the player makes will vanish very
5252 * quickly once outside the player's sight."
5253 */
extra_cloud_info(cloud_type cloud_type)5254 string extra_cloud_info(cloud_type cloud_type)
5255 {
5256 const bool opaque = is_opaque_cloud(cloud_type);
5257 const string opacity_info = !opaque ? "" :
5258 "\nThis cloud is opaque; one tile will not block vision, but "
5259 "multiple will.";
5260 const string vanish_info
5261 = make_stringf("\n\nClouds of this kind an adventurer makes will vanish"
5262 " %s once outside their sight.",
5263 opaque ? "quickly" : "almost instantly");
5264 return opacity_info + vanish_info;
5265 }
5266