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, &quote };
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