1 /**
2  * @file
3  * @brief Functions used to print information about spells, spellbooks, etc.
4  **/
5 
6 #include "AppHdr.h"
7 
8 #include "describe-spells.h"
9 
10 #include "colour.h"
11 #include "delay.h"
12 #include "describe.h"
13 #include "english.h"
14 #include "externs.h"
15 #include "invent.h"
16 #include "libutil.h"
17 #include "menu.h"
18 #include "mon-book.h"
19 #include "mon-cast.h"
20 #include "mon-explode.h" // ball_lightning_damage
21 #include "mon-project.h" // iood_damage
22 #include "religion.h"
23 #include "shopping.h"
24 #include "spl-book.h"
25 #include "spl-damage.h"
26 #include "spl-summoning.h" // mons_ball_lightning_hd
27 #include "spl-util.h"
28 #include "spl-zap.h"
29 #include "stringutil.h"
30 #include "state.h"
31 #include "tag-version.h"
32 #include "tileweb.h"
33 #include "unicode.h"
34 #ifdef USE_TILE
35  #include "tilepick.h"
36 #endif
37 
38 /**
39  * Returns a spellset containing the spells for the given item.
40  *
41  * @param item      The item in question.
42  * @return          A single-element vector, containing the list of all
43  *                  non-null spells in the given book, blank-labeled.
44  */
item_spellset(const item_def & item)45 spellset item_spellset(const item_def &item)
46 {
47     if (!item.has_spells())
48         return {};
49 
50     return { { "\n", spells_in_book(item) } };
51 }
52 
53 /**
54  * What's the appropriate descriptor for a given type of "spell" that's not
55  * really a spell?
56  *
57  * @param type              The type of spell-ability; e.g. MON_SPELL_MAGICAL.
58  * @return                  A descriptor of the spell type; e.g. "divine",
59  *                          "magical", etc.
60  */
_ability_type_descriptor(mon_spell_slot_flag type)61 static string _ability_type_descriptor(mon_spell_slot_flag type)
62 {
63     static const map<mon_spell_slot_flag, string> descriptors =
64     {
65         { MON_SPELL_NATURAL, "natural" },
66         { MON_SPELL_MAGICAL, "magical" },
67         { MON_SPELL_PRIEST,  "divine"  },
68         { MON_SPELL_VOCAL,   "natural" },
69     };
70 
71     return lookup(descriptors, type, "buggy");
72 }
73 
_abil_type_vuln_core(bool silencable,bool antimagicable)74 static const char* _abil_type_vuln_core(bool silencable, bool antimagicable)
75 {
76     // No one gets confused by the rare spells that are hit by silence
77     // but not antimagic, AFAIK. Let's keep it simple.
78     if (!antimagicable)
79         return "silence";
80     if (silencable)
81         return "silence and antimagic";
82     // Explicitly clarify about spells that are hit by antimagic but
83     // NOT silence, since those confuse players nonstop.
84     return "antimagic (but not silence)";
85 }
86 
87 /**
88  * What type of effects is this spell type vulnerable to?
89  *
90  * @param type              The type of spell-ability; e.g. MON_SPELL_MAGICAL.
91  * @return                  A description of the spell's vulnerabilities.
92  */
_ability_type_vulnerabilities(mon_spell_slot_flag type)93 static string _ability_type_vulnerabilities(mon_spell_slot_flag type)
94 {
95     if (type == MON_SPELL_NATURAL)
96         return "";
97     const bool silencable = type == MON_SPELL_WIZARD
98                             || type == MON_SPELL_PRIEST
99                             || type == MON_SPELL_VOCAL;
100     const bool antimagicable = type == MON_SPELL_WIZARD
101                                || type == MON_SPELL_MAGICAL;
102     ASSERT(silencable || antimagicable);
103     return make_stringf(", which are affected by %s",
104                         _abil_type_vuln_core(silencable, antimagicable));
105 }
106 
107 /**
108  * What description should a given (set of) monster spellbooks be prefixed
109  * with?
110  *
111  * @param type              The type of book(s); e.g. MON_SPELL_MAGICAL.
112  * @return                  A header string for the bookset; e.g.,
113  *                          "has mastered one of the following spellbooks:"
114  *                          "possesses the following natural abilities:"
115  */
_booktype_header(mon_spell_slot_flag type,bool pronoun_plural)116 static string _booktype_header(mon_spell_slot_flag type, bool pronoun_plural)
117 {
118     const string vulnerabilities = _ability_type_vulnerabilities(type);
119 
120     if (type == MON_SPELL_WIZARD)
121     {
122         return make_stringf("%s mastered %s%s:",
123                             conjugate_verb("have", pronoun_plural).c_str(),
124                             "the following spells",
125                             vulnerabilities.c_str());
126     }
127 
128     const string descriptor = _ability_type_descriptor(type);
129 
130     return make_stringf("%s the following %s abilities%s:",
131                         conjugate_verb("possess", pronoun_plural).c_str(),
132                         descriptor.c_str(),
133                         vulnerabilities.c_str());
134 }
135 
136 /**
137  * Append all spells of a given type that a given monster may know to the
138  * provided vector.
139  *
140  * @param mi                The player's knowledge of a monster.
141  * @param type              The type of spells to select.
142  *                          (E.g. MON_SPELL_MAGICAL, MON_SPELL_WIZARD...)
143  * @param[out] all_books    An output vector of "spellbooks".
144  */
_monster_spellbooks(const monster_info & mi,mon_spell_slot_flag type,spellset & all_books)145 static void _monster_spellbooks(const monster_info &mi,
146                                 mon_spell_slot_flag type,
147                                 spellset &all_books)
148 {
149     vector<mon_spell_slot> book_slots = get_unique_spells(mi, type);
150 
151     if (book_slots.empty())
152         return;
153 
154     const string set_name = type == MON_SPELL_WIZARD ? "Book" : "Set";
155 
156     spellbook_contents output_book;
157 
158     output_book.label +=
159         "\n" +
160         uppercase_first(mi.pronoun(PRONOUN_SUBJECTIVE)) +
161         " " +
162         _booktype_header(type, mi.pronoun_plurality());
163 
164     // Does the monster have a spell that allows them to cast Abjuration?
165     bool mons_abjure = false;
166 
167     for (const auto& slot : book_slots)
168     {
169         const spell_type spell = slot.spell;
170         if (!spell_is_soh_breath(spell))
171         {
172             output_book.spells.emplace_back(spell);
173             if (get_spell_flags(spell) & spflag::mons_abjure)
174                 mons_abjure = true;
175             continue;
176         }
177 
178         const vector<spell_type> *breaths = soh_breath_spells(spell);
179         ASSERT(breaths);
180         for (auto breath : *breaths)
181             output_book.spells.emplace_back(breath);
182     }
183 
184     if (mons_abjure)
185         output_book.spells.emplace_back(SPELL_ABJURATION);
186 
187     all_books.emplace_back(output_book);
188 }
189 
190 /**
191  * Return a spellset containing the spells potentially given by the given
192  * monster information.
193  *
194  * @param mi    The player's knowledge of a monster.
195  * @return      The spells potentially castable by that monster (as far as
196  *              the player knows).
197  */
monster_spellset(const monster_info & mi)198 spellset monster_spellset(const monster_info &mi)
199 {
200     if (!mi.has_spells())
201         return {};
202 
203     static const mon_spell_slot_flag book_flags[] =
204     {
205         MON_SPELL_NATURAL,
206         MON_SPELL_VOCAL,
207         MON_SPELL_MAGICAL,
208         MON_SPELL_PRIEST,
209         MON_SPELL_WIZARD,
210     };
211 
212     spellset books;
213 
214     for (auto book_flag : book_flags)
215         _monster_spellbooks(mi, book_flag, books);
216 
217     ASSERT(books.size());
218     return books;
219 }
220 
221 
222 /**
223  * Build a flat vector containing all unique spells in a given spellset.
224  *
225  * @param spellset      The spells in question.
226  * @return              An ordered set of unique spells in the given set.
227  *                      Guaranteed to be in the same order as in the spellset.
228  */
_spellset_contents(const spellset & spells)229 static vector<spell_type> _spellset_contents(const spellset &spells)
230 {
231     // find unique spells (O(nlogn))
232     set<spell_type> unique_spells;
233     for (auto &book : spells)
234         for (auto spell : book.spells)
235             unique_spells.insert(spell);
236 
237     // list spells in original order (O(nlogn)?)
238     vector<spell_type> spell_list;
239     for (auto &book : spells)
240     {
241         for (auto spell : book.spells)
242         {
243             if (unique_spells.erase(spell) == 1)
244                 spell_list.emplace_back(spell);
245         }
246     }
247 
248     return spell_list;
249 }
250 
251 /**
252  * What spell should a given colour be listed with?
253  *
254  * @param spell         The spell in question.
255  * @param source_item   The physical item holding the spells. May be null.
256  */
_spell_colour(spell_type spell,const item_def * const source_item)257 static int _spell_colour(spell_type spell, const item_def* const source_item)
258 {
259     if (!crawl_state.need_save)
260         return COL_UNKNOWN;
261 
262     if (!source_item)
263         return spell_highlight_by_utility(spell, COL_UNKNOWN);
264 
265     if (you.has_spell(spell))
266         return COL_MEMORIZED;
267 
268     // this is kind of ugly.
269     if (!you_can_memorise(spell)
270         || you.experience_level < spell_difficulty(spell)
271         || player_spell_levels() < spell_levels_required(spell))
272     {
273         return COL_USELESS;
274     }
275 
276     if (god_hates_spell(spell, you.religion))
277         return COL_FORBIDDEN;
278 
279     if (!you.has_spell(spell))
280         return COL_UNMEMORIZED;
281 
282     return spell_highlight_by_utility(spell, COL_UNKNOWN);
283 }
284 
285 /**
286  * List the name(s) of the school(s) the given spell is in.
287  *
288  * XXX: This is almost certainly duplicating something somewhere. Also, it's
289  * pretty ugly.
290  *
291  * @param spell     The spell in question.
292  * @return          A '/'-separated list of spellschool names.
293  */
_spell_schools(spell_type spell)294 static string _spell_schools(spell_type spell)
295 {
296     string schools;
297 
298     for (const auto school_flag : spschools_type::range())
299     {
300         if (!spell_typematch(spell, school_flag))
301             continue;
302 
303         if (!schools.empty())
304             schools += "/";
305         schools += spelltype_long_name(school_flag);
306     }
307 
308     return schools;
309 }
310 
311 /**
312  * Should spells from the given source be listed in two columns instead of
313  * one?
314  *
315  * @param source_item   The source of the spells; a book, or nullptr in the
316  *                      case of monster spellbooks.
317  * @return              source_item == nullptr
318  */
_list_spells_doublecolumn(const item_def * const source_item)319 static bool _list_spells_doublecolumn(const item_def* const source_item)
320 {
321     return !source_item;
322 }
323 
324 /**
325  * Produce a mapping from characters (used as indices) to spell types in
326  * the given spellset.
327  *
328  * @param spells        A list of 'books' of spells.
329  * @param source_item   The source of the spells; a book, or nullptr in the
330  *                      case of monster spellbooks.
331  * @return              A list of all unique spells in the given set, ordered
332  *                      either in original order or column-major order, the
333  *                      latter in the case of a double-column layout.
334  */
map_chars_to_spells(const spellset & spells,const item_def * const source_item)335 vector<pair<spell_type,char>> map_chars_to_spells(const spellset &spells,
336                                        const item_def* const source_item)
337 {
338     char next_ch = 'a';
339     const vector<spell_type> flat_spells = _spellset_contents(spells);
340     vector<pair<spell_type,char>> ret;
341     if (!_list_spells_doublecolumn(source_item))
342     {
343         for (auto spell : flat_spells)
344             ret.emplace_back(pair<spell_type,char>(spell, next_ch++));
345     }
346     else
347     {
348         for (size_t i = 0; i < flat_spells.size(); i += 2)
349             ret.emplace_back(pair<spell_type,char>(flat_spells[i], next_ch++));
350         for (size_t i = 1; i < flat_spells.size(); i += 2)
351             ret.emplace_back(pair<spell_type,char>(flat_spells[i], next_ch++));
352     }
353     return ret;
354 }
355 
_range_string(const spell_type & spell,const monster_info * mon_owner,int hd)356 static string _range_string(const spell_type &spell, const monster_info *mon_owner, int hd)
357 {
358     auto flags = get_spell_flags(spell);
359     int pow = mons_power_for_hd(spell, hd);
360     int range = spell_range(spell, pow, false);
361     const bool has_range = mon_owner
362                         && range > 0
363                         && !testbits(flags, spflag::selfench);
364     if (!has_range)
365         return "";
366     const bool in_range = has_range
367                     && crawl_state.need_save
368                     && in_bounds(mon_owner->pos)
369                     && grid_distance(you.pos(), mon_owner->pos) <= range;
370     const char *range_col = in_range ? "lightred" : "lightgray";
371     return make_stringf("(<%s>%d</%s>)", range_col, range, range_col);
372 }
373 
_spell_damage(spell_type spell,int hd)374 static dice_def _spell_damage(spell_type spell, int hd)
375 {
376     const int pow = mons_power_for_hd(spell, hd);
377 
378     switch (spell)
379     {
380         case SPELL_FREEZE:
381             return freeze_damage(pow);
382         case SPELL_WATERSTRIKE:
383             return waterstrike_damage(hd);
384         case SPELL_IOOD:
385             return iood_damage(pow, INFINITE_DISTANCE);
386         case SPELL_GLACIATE:
387             return glaciate_damage(pow, 3);
388         case SPELL_CONJURE_BALL_LIGHTNING:
389             return ball_lightning_damage(mons_ball_lightning_hd(pow, false));
390         default:
391             break;
392     }
393 
394     const zap_type zap = spell_to_zap(spell);
395     if (zap == NUM_ZAPS)
396         return dice_def(0,0);
397 
398     return zap_damage(zap, pow, true, false);
399 }
400 
_spell_hd(spell_type spell,const monster_info & mon_owner)401 static int _spell_hd(spell_type spell, const monster_info &mon_owner)
402 {
403     if (spell == SPELL_SEARING_BREATH && mon_owner.type == MONS_XTAHUA)
404         return mon_owner.hd * 3 / 2;
405     if (mons_spell_is_spell(spell))
406         return mon_owner.spell_hd();
407     return mon_owner.hd;
408 }
409 
_spell_colour(spell_type spell)410 static colour_t _spell_colour(spell_type spell)
411 {
412     switch (spell)
413     {
414         case SPELL_FREEZE:
415         case SPELL_GLACIATE:
416             return WHITE;
417         case SPELL_WATERSTRIKE:
418             return LIGHTBLUE;
419         case SPELL_IOOD:
420             return LIGHTMAGENTA;
421         default:
422             break;
423     }
424     const zap_type zap = spell_to_zap(spell);
425     if (zap == NUM_ZAPS)
426         return COL_UNKNOWN;
427     return zap_colour(zap);
428 }
429 
_colourize(string base,colour_t col)430 static string _colourize(string base, colour_t col)
431 {
432     if (col < NUM_TERM_COLOURS)
433     {
434         if (col == BLACK)
435             col = DARKGRAY;
436         const string col_name = colour_to_str(col);
437         return make_stringf("<%s>%s</%s>",
438                             col_name.c_str(), base.c_str(), col_name.c_str());
439     }
440     string out = make_stringf("%c", base[0]);
441     for (int i = 1; i < (int)base.length() - 1; i++)
442     {
443         const int term_col = element_colour(col, false, you.pos());
444         const string col_name = colour_to_str(term_col);
445         out += "<" + col_name + ">" + base[i] + "</" + col_name + ">";
446     }
447     out += base[base.length() - 1];
448     return out;
449 }
450 
_effect_string(spell_type spell,const monster_info * mon_owner)451 static string _effect_string(spell_type spell, const monster_info *mon_owner)
452 {
453     if (!mon_owner)
454         return "";
455 
456     const int hd = _spell_hd(spell, *mon_owner);
457     if (!hd)
458         return "";
459 
460     if (testbits(get_spell_flags(spell), spflag::WL_check))
461     {
462         // WL chances only make sense vs a player
463         if (!crawl_state.need_save
464 #ifndef DEBUG_DIAGNOSTICS
465             || mon_owner->attitude == ATT_FRIENDLY
466 #endif
467             )
468         {
469             return "";
470         }
471         if (you.immune_to_hex(spell))
472             return "(immune)";
473         return make_stringf("(%d%%)", hex_chance(spell, hd));
474     }
475 
476     const dice_def dam = _spell_damage(spell, hd);
477     if (dam.num == 0 || dam.size == 0)
478         return "";
479     string mult = "";
480     if (spell == SPELL_MARSHLIGHT)
481         mult = "2x";
482     else if (spell == SPELL_CONJURE_BALL_LIGHTNING)
483         mult = "3x";
484     return make_stringf("(%s%dd%d)", mult.c_str(), dam.num, dam.size);
485 }
486 
487 /**
488  * Describe a given set of spells.
489  *
490  * @param book              A labeled set of spells, corresponding to a book
491  *                          or monster spellbook.
492  * @param spell_map         The letters to use for each spell.
493  * @param source_item       The physical item holding the spells. May be null.
494  * @param description[out]  An output string to append to.
495  * @param mon_owner         If this spellset is being examined from a monster's
496  *                          description, 'mon_owner' is that monster. Else,
497  *                          it's null.
498  */
_describe_book(const spellbook_contents & book,vector<pair<spell_type,char>> & spell_map,const item_def * const source_item,formatted_string & description,const monster_info * mon_owner)499 static void _describe_book(const spellbook_contents &book,
500                            vector<pair<spell_type,char>> &spell_map,
501                            const item_def* const source_item,
502                            formatted_string &description,
503                            const monster_info *mon_owner)
504 {
505     description.textcolour(LIGHTGREY);
506 
507     description.cprintf("%s", book.label.c_str());
508 
509     // only display header for book spells
510     if (source_item)
511     {
512         description.cprintf(
513             "\n Spells                            Type                      Level       Known");
514     }
515     description.cprintf("\n");
516 
517     // list spells in two columns, instead of one? (monster books)
518     const bool doublecolumn = _list_spells_doublecolumn(source_item);
519 
520     bool first_line_element = true;
521     const int hd = mon_owner ? mon_owner->spell_hd() : 0;
522     for (auto spell : book.spells)
523     {
524         description.cprintf(" ");
525 
526         if (!mon_owner)
527             description.textcolour(_spell_colour(spell, source_item));
528 
529         // don't crash if we have more spells than letters.
530         auto entry = find_if(spell_map.begin(), spell_map.end(),
531                 [&spell](const pair<spell_type,char>& e)
532                 {
533                     return e.first == spell;
534                 });
535         const char spell_letter = entry != spell_map.end()
536                                             ? entry->second : ' ';
537 
538         const string range_str = _range_string(spell, mon_owner, hd);
539         string effect_str = _effect_string(spell, mon_owner);
540 
541         const int effect_len = effect_str.length();
542         const int range_len = range_str.empty() ? 0 : 3;
543         const int effect_range_space = effect_len && range_len ? 1 : 0;
544         const int chop_len = 30 - effect_len - range_len - effect_range_space;
545 
546         if (effect_len && !testbits(get_spell_flags(spell), spflag::WL_check))
547             effect_str = _colourize(effect_str, _spell_colour(spell));
548 
549         string spell_name = spell_title(spell);
550         if (spell == SPELL_LEHUDIBS_CRYSTAL_SPEAR
551             && chop_len < (int)spell_name.length())
552         {
553             // looks nicer than Lehudib's Crystal S
554             spell_name = "Crystal Spear";
555         }
556         description += formatted_string::parse_string(
557                 make_stringf("%c - %s%s%s%s", spell_letter,
558                              chop_string(spell_name, chop_len).c_str(),
559                              effect_str.c_str(),
560                              effect_range_space ? " " : "",
561                              range_str.c_str()));
562 
563         // only display type & level for book spells
564         if (doublecolumn)
565         {
566             // print monster spells in two columns
567             if (first_line_element)
568                 description.cprintf("    ");
569             else
570                 description.cprintf("\n");
571             first_line_element = !first_line_element;
572             continue;
573         }
574 
575         string schools =
576 #if TAG_MAJOR_VERSION == 34
577             source_item->base_type == OBJ_RODS ? "Evocations"
578                                                :
579 #endif
580                          _spell_schools(spell);
581 
582         string known = "";
583         if (!mon_owner)
584             known = you.spell_library[spell] ? "         yes" : "          no";
585 
586         description.cprintf("%s%d%s\n",
587                             chop_string(schools, 30).c_str(),
588                             spell_difficulty(spell),
589                             known.c_str());
590     }
591 
592     // are we halfway through a column?
593     if (doublecolumn && book.spells.size() % 2)
594         description.cprintf("\n");
595 }
596 
597 
598 /**
599  * List a given set of spells.
600  *
601  * @param spells            The set of spells to be listed.
602  * @param source_item       The physical item holding the spells. May be null.
603  * @param description[out]  An output string to append to.
604  * @param mon_owner         If this spellset is being examined from a monster's
605  *                          description, 'mon_owner' is that monster. Else,
606  *                          it's null.
607  */
describe_spellset(const spellset & spells,const item_def * const source_item,formatted_string & description,const monster_info * mon_owner)608 void describe_spellset(const spellset &spells,
609                        const item_def* const source_item,
610                        formatted_string &description,
611                        const monster_info *mon_owner)
612 {
613     auto spell_map = map_chars_to_spells(spells, source_item);
614     for (auto book : spells)
615         _describe_book(book, spell_map, source_item, description, mon_owner);
616 }
617 
618 #ifdef USE_TILE_WEB
_write_book(const spellbook_contents & book,vector<pair<spell_type,char>> & spell_map,const item_def * const source_item,const monster_info * mon_owner)619 static void _write_book(const spellbook_contents &book,
620                            vector<pair<spell_type,char>> &spell_map,
621                            const item_def* const source_item,
622                            const monster_info *mon_owner)
623 {
624     tiles.json_open_object();
625     tiles.json_write_string("label", book.label);
626     const int hd = mon_owner ? mon_owner->spell_hd() : 0;
627     tiles.json_open_array("spells");
628     for (auto spell : book.spells)
629     {
630         tiles.json_open_object();
631         tiles.json_write_string("title", spell_title(spell));
632         tiles.json_write_int("colour", _spell_colour(spell, source_item));
633         tiles.json_write_name("tile");
634         tiles.write_tileidx(tileidx_spell(spell));
635 
636         // don't crash if we have more spells than letters.
637         auto entry = find_if(spell_map.begin(), spell_map.end(),
638                 [&spell](const pair<spell_type,char>& e) { return e.first == spell; });
639         const char spell_letter = entry != spell_map.end() ? entry->second : ' ';
640         tiles.json_write_string("letter", string(1, spell_letter));
641 
642         string effect_str = _effect_string(spell, mon_owner);
643         if (!testbits(get_spell_flags(spell), spflag::WL_check))
644             effect_str = _colourize(effect_str, _spell_colour(spell));
645         tiles.json_write_string("effect", effect_str);
646 
647         string range_str = _range_string(spell, mon_owner, hd);
648         if (range_str.size() > 0)
649             tiles.json_write_string("range_string", range_str);
650 
651 #if TAG_MAJOR_VERSION == 34
652         string schools = (source_item && source_item->base_type == OBJ_RODS) ?
653                 "Evocations" : _spell_schools(spell);
654 #else
655         string schools = _spell_schools(spell);
656 #endif
657         tiles.json_write_string("schools", schools);
658         tiles.json_write_int("level", spell_difficulty(spell));
659         tiles.json_close_object();
660     }
661     tiles.json_close_array();
662     tiles.json_close_object();
663 }
664 
write_spellset(const spellset & spells,const item_def * const source_item,const monster_info * mon_owner)665 void write_spellset(const spellset &spells,
666                        const item_def* const source_item,
667                        const monster_info *mon_owner)
668 {
669     auto spell_map = map_chars_to_spells(spells, source_item);
670     tiles.json_open_array("spellset");
671     for (auto book : spells)
672         _write_book(book, spell_map, source_item, mon_owner);
673     tiles.json_close_array();
674 }
675 #endif
676 
677 /**
678  * Return a description of the spells in the given item.
679  *
680  * @param item      The book in question.
681  * @return          A column-and-row listing of the spells in the given item,
682  *                  including names, schools & levels.
683  */
describe_item_spells(const item_def & item)684 string describe_item_spells(const item_def &item)
685 {
686     formatted_string description;
687     describe_spellset(item_spellset(item), &item, description);
688     return description.tostring();
689 }
690