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