1 /**
2  * @file
3  * @brief Spellbook contents array and management functions
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "options.h"
9 #include "spl-book.h"
10 #include "book-data.h"
11 
12 #include <algorithm>
13 #include <cstdio>
14 #include <cstdlib>
15 #include <cstring>
16 #include <iomanip>
17 #include <unordered_set>
18 
19 #include "artefact.h"
20 #include "colour.h"
21 #include "command.h"
22 #include "database.h"
23 #include "delay.h"
24 #include "describe.h"
25 #include "end.h"
26 #include "god-conduct.h"
27 #include "invent.h"
28 #include "item-prop.h"
29 #include "libutil.h"
30 #include "message.h"
31 #include "output.h"
32 #include "prompt.h"
33 #include "random-pick.h"
34 #include "religion.h"
35 #include "spl-cast.h"
36 #include "spl-util.h"
37 #include "state.h"
38 #include "stringutil.h"
39 #include "tilepick.h"
40 #include "transform.h"
41 #include "unicode.h"
42 
43 #define RANDART_BOOK_TYPE_KEY  "randart_book_type"
44 #define RANDART_BOOK_LEVEL_KEY "randart_book_level"
45 
46 #define RANDART_BOOK_TYPE_LEVEL "level"
47 #define RANDART_BOOK_TYPE_THEME "theme"
48 
49 struct sortable_spell
50 {
sortable_spellsortable_spell51     sortable_spell(spell_type s) : spell(s),
52                 raw_fail(raw_spell_fail(s)),
53                 fail_rate(failure_rate_to_int(raw_fail)),
54                 fail_rate_colour(failure_rate_colour(s)),
55                 level(spell_levels_required(s)),
56                 difficulty(spell_difficulty(s)),
57                 name(spell_title(s)),
58                 school(spell_schools_string(s))
59     {
60     }
61 
62     spell_type spell;
63     int raw_fail;
64     int fail_rate;
65     int fail_rate_colour;
66     int level;
67     int difficulty;
68     string name;
69     string school; // TODO: set?
70 
operator ==(const sortable_spell & x,const sortable_spell & y)71     friend bool operator==(const sortable_spell& x, const sortable_spell& y)
72     {
73         return x.spell == y.spell;
74     }
75 };
76 
77 
78 struct hash_sortable_spell
79 {
operator ()hash_sortable_spell80     spell_type operator()(const sortable_spell& s) const
81     {
82         return s.spell;
83     }
84 };
85 
86 typedef vector<sortable_spell>                 spell_list;
87 typedef unordered_set<spell_type, hash<int>>   spell_set;
88 
89 static const map<wand_type, spell_type> _wand_spells =
90 {
91     { WAND_FLAME, SPELL_THROW_FLAME },
92     { WAND_PARALYSIS, SPELL_PARALYSE },
93     { WAND_DIGGING, SPELL_DIG },
94     { WAND_ICEBLAST, SPELL_ICEBLAST },
95     { WAND_POLYMORPH, SPELL_POLYMORPH },
96     { WAND_CHARMING, SPELL_CHARMING },
97     { WAND_ACID, SPELL_CORROSIVE_BOLT },
98     { WAND_MINDBURST, SPELL_MINDBURST },
99 };
100 
101 
spell_in_wand(wand_type wand)102 spell_type spell_in_wand(wand_type wand)
103 {
104     if (item_type_removed(OBJ_WANDS, wand))
105         return SPELL_NO_SPELL;
106 
107     if (const spell_type* const spl = map_find(_wand_spells, wand))
108         return *spl;
109 
110     die("Unknown wand: %d", wand);
111 }
112 
is_wand_spell(spell_type spell)113 bool is_wand_spell(spell_type spell)
114 {
115     for (auto p : _wand_spells)
116         if (spell == p.second)
117             return true;
118     return false;
119 }
120 
spells_in_book(const item_def & book)121 vector<spell_type> spells_in_book(const item_def &book)
122 {
123     ASSERT(book.base_type == OBJ_BOOKS);
124 
125     vector<spell_type> ret;
126     const CrawlHashTable &props = book.props;
127     if (!props.exists(SPELL_LIST_KEY))
128         return spellbook_template(static_cast<book_type>(book.sub_type));
129 
130     const CrawlVector &spells = props[SPELL_LIST_KEY].get_vector();
131 
132     ASSERT(spells.get_type() == SV_INT);
133     ASSERT(spells.size() == RANDBOOK_SIZE);
134 
135     for (int spell : spells)
136         //TODO: don't put SPELL_NO_SPELL in them in the first place.
137         if (spell != SPELL_NO_SPELL)
138             ret.emplace_back(static_cast<spell_type>(spell));
139 
140     return ret;
141 }
142 
spellbook_template(book_type book)143 vector<spell_type> spellbook_template(book_type book)
144 {
145     ASSERT_RANGE(book, 0, (int)ARRAYSZ(spellbook_templates));
146     return spellbook_templates[book];
147 }
148 
book_exists(book_type bt)149 bool book_exists(book_type bt)
150 {
151     switch (bt)
152     {
153     case BOOK_RANDART_LEVEL:
154     case BOOK_RANDART_THEME:
155     case BOOK_MANUAL:
156     case NUM_BOOKS:
157         return false;
158     default:
159         return !item_type_removed(OBJ_BOOKS, bt);
160     }
161 }
162 
163 #ifdef DEBUG
validate_spellbooks()164 void validate_spellbooks()
165 {
166     for (int i = 0; i < NUM_BOOKS; ++i)
167     {
168         const book_type book = static_cast<book_type>(i);
169         if (!book_exists(book))
170             continue;
171 
172         spell_type last = SPELL_NO_SPELL;
173         for (spell_type spell : spellbook_template(book))
174         {
175             ASSERT(spell != SPELL_NO_SPELL);
176             if (last != SPELL_NO_SPELL
177                 && spell_difficulty(last) > spell_difficulty(spell))
178             {
179                 item_def item;
180                 item.base_type = OBJ_BOOKS;
181                 item.sub_type  = i;
182 
183                 end(1, false, "Spellbook '%s' has spells out of level order "
184                     "('%s' is before '%s')",
185                     item.name(DESC_PLAIN, false, true).c_str(),
186                     spell_title(last),
187                     spell_title(spell));
188             }
189             last = spell;
190 
191             spell_flags flags = get_spell_flags(spell);
192 
193             if (flags & (spflag::monster | spflag::testing))
194             {
195                 item_def item;
196                 item.base_type = OBJ_BOOKS;
197                 item.sub_type  = i;
198 
199                 end(1, false, "Spellbook '%s' contains invalid spell "
200                              "'%s'",
201                     item.name(DESC_PLAIN, false, true).c_str(),
202                     spell_title(spell));
203             }
204         }
205     }
206 }
207 #endif
208 
is_player_book_spell(spell_type which_spell)209 bool is_player_book_spell(spell_type which_spell)
210 {
211     for (int i = 0; i < NUM_BOOKS; ++i)
212     {
213         auto bt = static_cast<book_type>(i);
214         if (!book_exists(bt))
215             continue;
216         for (spell_type spell : spellbook_template(bt))
217             if (spell == which_spell)
218                 return true;
219     }
220     return false;
221 }
222 
223 // Needs to be castable by the player somehow, but via idiosyncratic means.
224 // Religion reusing a spell enum, or something weirder like sonic wave.
225 // A spell doesn't need to be here if it just the beam type that is used.
226 static unordered_set<int> _player_nonbook_spells =
227 {
228     // items
229     SPELL_THUNDERBOLT,
230     SPELL_PHANTOM_MIRROR, // this isn't cast directly, but the player code at
231                           // least uses the enum value
232     SPELL_SONIC_WAVE,
233     // religion
234     SPELL_SMITING,
235     // Ds powers
236     SPELL_HURL_DAMNATION,
237 };
238 
is_player_spell(spell_type which_spell)239 bool is_player_spell(spell_type which_spell)
240 {
241     return !spell_removed(which_spell)
242         && (is_player_book_spell(which_spell)
243             || is_wand_spell(which_spell)
244             || _player_nonbook_spells.count(which_spell) > 0);
245 }
246 
247 /**
248  * Is the player ever allowed to memorise the given spell? (Based on race, not
249  * spell slot restrictions, etc)
250  *
251  * @param spell     The type of spell in question.
252  * @return          Whether the the player is allowed to memorise the spell.
253  */
you_can_memorise(spell_type spell)254 bool you_can_memorise(spell_type spell)
255 {
256     return !spell_is_useless(spell, false, true);
257 }
258 
player_can_memorise(const item_def & book)259 bool player_can_memorise(const item_def &book)
260 {
261     if (!item_is_spellbook(book) || !player_spell_levels())
262         return false;
263 
264     for (spell_type stype : spells_in_book(book))
265     {
266         // Easiest spell already too difficult?
267         if (spell_difficulty(stype) > you.experience_level
268             || player_spell_levels() < spell_levels_required(stype))
269         {
270             return false;
271         }
272 
273         if (!you.has_spell(stype))
274             return true;
275     }
276     return false;
277 }
278 
279 /**
280  * Populate the given list with all spells the player can currently memorise,
281  * from library or Vehumet. Does not filter by currently known spells, spell
282  * levels, etc.
283  *
284  * @param available_spells  A list to be populated with available spells.
285  */
_list_available_spells(spell_set & available_spells)286 static void _list_available_spells(spell_set &available_spells)
287 {
288     for (spell_type st = SPELL_NO_SPELL; st < NUM_SPELLS; st++)
289     {
290         if (you.spell_library[st])
291             available_spells.insert(st);
292     }
293 
294     // Handle Vehumet gifts
295     for (auto gift : you.vehumet_gifts)
296         available_spells.insert(gift);
297 }
298 
player_has_available_spells()299 bool player_has_available_spells()
300 {
301     spell_set available_spells;
302     _list_available_spells(available_spells);
303 
304     const int avail_slots = player_spell_levels();
305 
306     for (const spell_type spell : available_spells)
307     {
308         if (!you.has_spell(spell) && you_can_memorise(spell)
309             && spell_difficulty(spell) <= avail_slots
310             && spell_difficulty(spell) <= you.experience_level)
311         {
312             return true;
313         }
314     }
315     return false;
316 }
317 
_get_spell_list(bool just_check=false,bool memorise_only=true)318 static spell_list _get_spell_list(bool just_check = false,
319                                   bool memorise_only = true)
320 {
321     spell_list mem_spells;
322     spell_set available_spells;
323     _list_available_spells(available_spells);
324 
325     if (available_spells.empty())
326     {
327         if (!just_check)
328         {
329             if (you.has_mutation(MUT_INNATE_CASTER))
330                 mprf(MSGCH_PROMPT, "You need no library to learn spells.");
331             else
332                 mprf(MSGCH_PROMPT, "Your library has no spells.");
333         }
334         return mem_spells;
335     }
336 
337     int num_known      = 0;
338     int num_misc       = 0;
339     int num_restricted = 0;
340     int num_low_xl     = 0;
341     int num_low_levels = 0;
342     int num_memable    = 0;
343 
344     for (const spell_type spell : available_spells)
345     {
346         if (you.has_spell(spell))
347         {
348             num_known++;
349 
350             // Divine Exegesis includes spells the player already knows.
351             if (you.divine_exegesis)
352                 mem_spells.emplace_back(spell);
353         }
354         else if (!you_can_memorise(spell))
355         {
356             if (!memorise_only)
357                 mem_spells.emplace_back(spell);
358 
359             if (cannot_use_schools(get_spell_disciplines(spell)))
360                 num_restricted++;
361             else
362                 num_misc++;
363         }
364         else
365         {
366             mem_spells.emplace_back(spell);
367 
368             const int avail_slots = player_spell_levels();
369 
370             // don't filter out spells that are too high-level for us; we
371             // probably still want to see them. (since that's temporary.)
372 
373             if (mem_spells.back().difficulty > you.experience_level)
374                 num_low_xl++;
375             else if (avail_slots < mem_spells.back().level)
376                 num_low_levels++;
377             else
378                 num_memable++;
379         }
380     }
381 
382     const int total = num_known + num_misc + num_low_xl + num_low_levels
383                       + num_restricted;
384 
385     const char* unavail_reason;
386 
387     if (num_memable || num_low_levels > 0 || num_low_xl > 0)
388         unavail_reason = "";
389     else if (num_known == total)
390         unavail_reason = "You already know all available spells.";
391     else if (num_restricted == total || num_restricted + num_known == total)
392     {
393         unavail_reason = "You cannot currently memorise any of the available "
394                          "spells because you cannot use those schools of "
395                          "magic.";
396     }
397     else if (num_misc == total || (num_known + num_misc) == total
398              || num_misc + num_known + num_restricted == total)
399     {
400         unavail_reason = "You cannot memorise any of the available spells.";
401     }
402     else
403     {
404         unavail_reason = "You can't memorise any new spells for an unknown "
405                          "reason; please file a bug report.";
406     }
407 
408     if (!just_check && *unavail_reason)
409         mprf(MSGCH_PROMPT, "%s", unavail_reason);
410     return mem_spells;
411 }
412 
library_add_spells(vector<spell_type> spells)413 bool library_add_spells(vector<spell_type> spells)
414 {
415     vector<spell_type> new_spells;
416     for (spell_type st : spells)
417     {
418         if (!you.spell_library[st])
419         {
420             you.spell_library.set(st, true);
421             bool memorise = you_can_memorise(st);
422             if (memorise)
423                 new_spells.push_back(st);
424             if (!memorise || Options.auto_hide_spells)
425                 you.hidden_spells.set(st, true);
426         }
427     }
428     if (!new_spells.empty())
429     {
430         vector<string> spellnames(new_spells.size());
431         transform(new_spells.begin(), new_spells.end(), spellnames.begin(), spell_title);
432         mprf("You add the spell%s %s to your library.",
433              spellnames.size() > 1 ? "s" : "",
434              comma_separated_line(spellnames.begin(),
435                                   spellnames.end()).c_str());
436         return true;
437     }
438     return false;
439 }
440 
has_spells_to_memorise(bool silent)441 bool has_spells_to_memorise(bool silent)
442 {
443     // TODO: this is a bit dumb
444     spell_list mem_spells(_get_spell_list(silent, true));
445     return !mem_spells.empty();
446 }
447 
_sort_mem_spells(const sortable_spell & a,const sortable_spell & b)448 static bool _sort_mem_spells(const sortable_spell &a, const sortable_spell &b)
449 {
450     // Put unmemorisable spells last
451     const bool mem_a = you_can_memorise(a.spell);
452     const bool mem_b = you_can_memorise(b.spell);
453     if (mem_a != mem_b)
454         return mem_a;
455 
456     // List the Vehumet gifts at the very top.
457     const bool offering_a = vehumet_is_offering(a.spell);
458     const bool offering_b = vehumet_is_offering(b.spell);
459     if (offering_a != offering_b)
460         return offering_a;
461 
462     // List spells we can memorise right away first.
463     const int player_levels = player_spell_levels();
464     if (player_levels >= a.level && player_spell_levels() < b.level)
465         return true;
466     else if (player_spell_levels() < a.level && player_spell_levels() >= b.level)
467         return false;
468 
469     // Don't sort by failure rate beyond what the player can see in the
470     // success descriptions.
471     if (a.fail_rate != b.fail_rate)
472         return a.fail_rate < b.fail_rate;
473 
474     if (a.difficulty != b.difficulty)
475         return a.difficulty < b.difficulty;
476 
477     return strcasecmp(spell_title(a.spell), spell_title(b.spell)) < 0;
478 }
479 
_sort_divine_spells(const sortable_spell & a,const sortable_spell & b)480 static bool _sort_divine_spells(const sortable_spell &a, const sortable_spell &b)
481 {
482     // Put useless spells last
483     const bool useful_a = !spell_is_useless(a.spell, true, true);
484     const bool useful_b = !spell_is_useless(b.spell, true, true);
485     if (useful_a != useful_b)
486         return useful_a;
487 
488     // Put higher levels spells first, as they're more likely to be what we
489     // want.
490     if (a.difficulty != b.difficulty)
491         return a.difficulty > b.difficulty;
492 
493     return strcasecmp(spell_title(a.spell), spell_title(b.spell)) < 0;
494 }
495 
get_sorted_spell_list(bool silent,bool memorise_only)496 vector<spell_type> get_sorted_spell_list(bool silent, bool memorise_only)
497 {
498     spell_list mem_spells(_get_spell_list(silent, memorise_only));
499 
500     if (you.divine_exegesis)
501         sort(mem_spells.begin(), mem_spells.end(), _sort_divine_spells);
502     else
503         sort(mem_spells.begin(), mem_spells.end(), _sort_mem_spells);
504 
505     vector<spell_type> result;
506     for (auto s : mem_spells)
507         result.push_back(s.spell);
508 
509     return result;
510 }
511 
512 class SpellLibraryMenu : public Menu
513 {
514 public:
515     enum class action { cast, memorise, describe, hide, unhide } current_action;
516 
517 protected:
calc_title()518     virtual formatted_string calc_title() override
519     {
520         return formatted_string::parse_string(
521                     make_stringf("<w>Spells %s                 Type                          %sLevel",
522                         current_action == action::cast ? "(Cast)    "
523                         : current_action == action::memorise ? "(Memorise)"
524                         : current_action == action::describe ? "(Describe)"
525                         : current_action == action::hide ? "(Hide)    "
526                         : "(Show)    ",
527                         you.divine_exegesis ? "" : "Failure  "));
528     }
529 
530 private:
531     spell_list& spells;
532     string spell_levels_str;
533     string search_text;
534     int hidden_count;
535 
update_more()536     void update_more()
537     {
538         // TODO: convert this all to widgets
539         ostringstream desc;
540 
541         // line 1
542         desc << spell_levels_str << "    ";
543         if (search_text.size())
544         {
545             // TODO: couldn't figure out how to do this in pure c++
546             const string match_text = make_stringf("Matches: '<w>%.20s</w>'",
547                             replace_all(search_text, "<", "<<").c_str());
548             int escaped_count = (int) std::count(search_text.begin(),
549                                                     search_text.end(), '<');
550             // the width here is a bit complicated because it needs to ignore
551             // any color codes and escaped '<'s.
552             desc << std::left << std::setw(43 + escaped_count) << match_text;
553         }
554         else
555             desc << std::setw(36) << "";
556         if (hidden_count)
557         {
558             desc << std::right << std::setw(hidden_count == 1 ? 3 : 2)
559                  << hidden_count
560                  << (hidden_count > 1 ? " spells" : " spell")
561                  << " hidden";
562         }
563         desc << "\n";
564 
565         const string act = you.divine_exegesis ? "Cast" : "Memorise";
566         // line 2
567         desc << "[<yellow>?</yellow>] help                "
568                 "[<yellow>Ctrl-f</yellow>] search      "
569                 "[<yellow>!</yellow>] ";
570         desc << ( current_action == action::cast
571                             ? "<w>Cast</w>|Describe|Hide|Show"
572                  : current_action == action::memorise
573                             ? "<w>Memorise</w>|Describe|Hide|Show"
574                  : current_action == action::describe
575                             ? act + "|<w>Describe</w>|Hide|Show"
576                  : current_action == action::hide
577                             ? act + "|Describe|<w>Hide</w>|Show"
578                  : act + "|Describe|Hide|<w>Show</w>");
579 
580         set_more(formatted_string::parse_string(desc.str()));
581     }
582 
process_key(int keyin)583     virtual bool process_key(int keyin) override
584     {
585         bool entries_changed = false;
586         switch (keyin)
587         {
588         case '!':
589 #ifdef TOUCH_UI
590         case CK_TOUCH_DUMMY:
591 #endif
592             switch (current_action)
593             {
594                 case action::cast:
595                 case action::memorise:
596                     current_action = action::describe;
597                     entries_changed = true; // need to add hotkeys
598                     break;
599                 case action::describe:
600                     current_action = action::hide;
601                     break;
602                 case action::hide:
603                     current_action = action::unhide;
604                     entries_changed = true;
605                     break;
606                 case action::unhide:
607                     current_action = you.divine_exegesis ? action::cast
608                                                           : action::memorise;
609                     entries_changed = true;
610                     break;
611             }
612             update_title();
613             update_more();
614             break;
615 
616         case CONTROL('F'):
617         {
618             char linebuf[80] = "";
619             const bool validline = title_prompt(linebuf, sizeof linebuf,
620                                                 "Search for what? (regex) ");
621             string old_search = search_text;
622             if (validline)
623                 search_text = linebuf;
624             else
625                 search_text = "";
626             entries_changed = old_search != search_text;
627             break;
628         }
629 
630         case '?':
631             show_spell_library_help();
632             break;
633         case CK_MOUSE_B2:
634         case CK_MOUSE_CMD:
635         CASE_ESCAPE
636             if (search_text.size())
637             {
638                 search_text = "";
639                 entries_changed = true;
640                 break;
641             }
642             // intentional fallthrough if search is empty
643         default:
644             return Menu::process_key(keyin);
645         }
646 
647         if (entries_changed)
648         {
649             update_entries();
650             update_more();
651         }
652         return true;
653     }
654 
entry_colour(const sortable_spell & entry)655     colour_t entry_colour(const sortable_spell& entry)
656     {
657         if (vehumet_is_offering(entry.spell))
658             return LIGHTBLUE;
659         else
660         {
661             return spell_highlight_by_utility(entry.spell, COL_UNKNOWN, false,
662                     you.divine_exegesis ? false : true);
663         }
664     }
665 
666     // Update the list of spells. If show_hidden is true, show only hidden
667     // ones; otherwise, show only non-hidden ones.
update_entries()668     void update_entries()
669     {
670         clear();
671         hidden_count = 0;
672         const bool show_hidden = current_action == action::unhide;
673         menu_letter hotkey;
674         text_pattern pat(search_text, true);
675         for (auto& spell : spells)
676         {
677             if (!search_text.empty()
678                 && !pat.matches(spell.name)
679                 && !pat.matches(spell.school))
680             {
681                 continue;
682             }
683 
684             const bool spell_hidden = you.hidden_spells.get(spell.spell);
685 
686             if (spell_hidden)
687                 hidden_count++;
688 
689             if (spell_hidden != show_hidden)
690                 continue;
691 
692             // TODO: it might be cleaner to reorder and put unavailable spells
693             // under a different category
694             // n.b. this memorise code only checks spell-specific constraints,
695             // so it adds hotkeys e.g. if you lack the spell levels. Not sure
696             // if this is intentional.
697             const bool unavailable = current_action == action::memorise &&
698                                         !you_can_memorise(spell.spell)
699                                 || current_action == action::cast &&
700                                     spell_is_useless(spell.spell, true, true);
701 
702             const colour_t colour = unavailable ? (colour_t) DARKGRAY
703                                                 : entry_colour(spell);
704 
705             ostringstream desc;
706             desc << "<" << colour_to_str(colour) << ">";
707 
708             desc << left;
709             desc << chop_string(spell.name, 30);
710             desc << spell.school;
711 
712             int so_far = strwidth(desc.str()) - (colour_to_str(colour).length()+2);
713             if (so_far < 60)
714                 desc << string(60 - so_far, ' ');
715             desc << "</" << colour_to_str(colour) << ">";
716 
717             if (!you.divine_exegesis)
718             {
719                 desc << "<" << colour_to_str(spell.fail_rate_colour) << ">";
720                 desc << chop_string(failure_rate_to_string(spell.raw_fail), 12);
721                 desc << "</" << colour_to_str(spell.fail_rate_colour) << ">";
722             }
723 
724             desc << spell.difficulty;
725 
726             MenuEntry* me = new MenuEntry(desc.str(), MEL_ITEM, 1,
727                     // don't add a hotkey if you can't memorise/cast it
728                     unavailable ? 0 : char(hotkey));
729             // But do increment hotkeys anyway, to keep the hotkeys consistent.
730             ++hotkey;
731 
732             me->indent_no_hotkeys = true;
733             me->colour = colour;
734             me->add_tile(tile_def(tileidx_spell(spell.spell)));
735 
736             me->data = &(spell.spell);
737             add_entry(me);
738         }
739         update_menu(true);
740     }
741 
742 public:
SpellLibraryMenu(spell_list & list)743     SpellLibraryMenu(spell_list& list)
744         : Menu(MF_SINGLESELECT | MF_ANYPRINTABLE
745                | MF_ALWAYS_SHOW_MORE | MF_ALLOW_FORMATTING
746                // To have the ctrl-f menu show up in webtiles
747                | MF_ALLOW_FILTER, "spell"),
748         current_action(you.divine_exegesis ? action::cast : action::memorise),
749         spells(list),
750         hidden_count(0)
751     {
752         set_highlighter(nullptr);
753         // Actual text handled by calc_title
754         set_title(new MenuEntry(""), true, true);
755 
756         if (you.divine_exegesis)
757         {
758             spell_levels_str = make_stringf(
759                 "<lightgreen>Select a spell to cast with Divine Exegesis: %d MP available</lightgreen>",
760                 you.magic_points);
761         }
762         else
763         {
764             spell_levels_str = make_stringf("<lightgreen>%d spell level%s"
765                         "</lightgreen>", player_spell_levels(),
766                         (player_spell_levels() > 1 || player_spell_levels() == 0)
767                                                     ? "s left" : " left ");
768             if (player_spell_levels() < 9)
769                 spell_levels_str += " ";
770         }
771         set_more(formatted_string::parse_string(spell_levels_str + "\n"));
772 
773 #ifdef USE_TILE_LOCAL
774         FontWrapper *font = tiles.get_crt_font();
775         int title_width = font->string_width(calc_title());
776         m_ui.vbox->min_size().width = 38 + title_width + 10;
777 #endif
778         m_ui.scroller->expand_v = true; // TODO: doesn't work on webtiles
779 
780         update_entries();
781         update_more();
782         on_single_selection = [this](const MenuEntry& item)
783         {
784             const spell_type spell = *static_cast<spell_type*>(item.data);
785             ASSERT(is_valid_spell(spell));
786 
787             switch (current_action)
788             {
789             case action::memorise:
790             case action::cast:
791                 return false;
792             case action::describe:
793                 describe_spell(spell, nullptr);
794                 break;
795             case action::hide:
796             case action::unhide:
797                 you.hidden_spells.set(spell, !you.hidden_spells.get(spell));
798                 update_entries();
799                 update_menu(true);
800                 update_more();
801                 break;
802             }
803             return true;
804         };
805     }
806 };
807 
_choose_mem_spell(spell_list & spells)808 static spell_type _choose_mem_spell(spell_list &spells)
809 {
810     // If we've gotten this far, we know that at least one spell here is
811     // memorisable, which is enough.
812 
813     SpellLibraryMenu spell_menu(spells);
814 
815     const vector<MenuEntry*> sel = spell_menu.show();
816     if (!crawl_state.doing_prev_cmd_again)
817     {
818         redraw_screen();
819         update_screen();
820     }
821     if (sel.empty())
822         return SPELL_NO_SPELL;
823     const spell_type spell = *static_cast<spell_type*>(sel[0]->data);
824     ASSERT(is_valid_spell(spell));
825     return spell;
826 }
827 
can_learn_spell(bool silent)828 bool can_learn_spell(bool silent)
829 {
830     if (you.duration[DUR_BRAINLESS])
831     {
832         if (!silent)
833             mpr("Your brain is not functional enough to learn spells.");
834         return false;
835     }
836 
837     if (you.confused())
838     {
839         if (!silent)
840             canned_msg(MSG_TOO_CONFUSED);
841         return false;
842     }
843 
844     if (you.berserk())
845     {
846         if (!silent)
847             canned_msg(MSG_TOO_BERSERK);
848         return false;
849     }
850 
851     return true;
852 }
853 
learn_spell()854 bool learn_spell()
855 {
856     spell_list spells(_get_spell_list());
857     if (spells.empty())
858         return false;
859 
860     sort(spells.begin(), spells.end(), _sort_mem_spells);
861 
862     spell_type specspell = _choose_mem_spell(spells);
863 
864     if (specspell == SPELL_NO_SPELL)
865     {
866         canned_msg(MSG_OK);
867         return false;
868     }
869 
870     return learn_spell(specspell);
871 }
872 
873 /**
874  * Why can't the player memorise the given spell?
875  *
876  * @param spell     The spell in question.
877  * @return          A string describing (one of) the reason(s) the player
878  *                  can't memorise this spell.
879  */
desc_cannot_memorise_reason(spell_type spell)880 string desc_cannot_memorise_reason(spell_type spell)
881 {
882     return spell_uselessness_reason(spell, false, true);
883 }
884 
885 /**
886  * Can the player learn the given spell?
887  *
888  * @param   specspell  The spell to be learned.
889  * @param   wizard     Whether to skip some checks for wizmode memorisation.
890  * @return             false if the player can't learn the spell for any
891  *                     reason, true otherwise.
892 */
_learn_spell_checks(spell_type specspell,bool wizard=false)893 static bool _learn_spell_checks(spell_type specspell, bool wizard = false)
894 {
895     if (spell_removed(specspell))
896     {
897         mpr("Sorry, this spell is gone!");
898         return false;
899     }
900 
901     if (!wizard && !can_learn_spell())
902         return false;
903 
904     if (already_learning_spell((int) specspell))
905         return false;
906 
907     if (!you_can_memorise(specspell))
908     {
909         mpr(desc_cannot_memorise_reason(specspell));
910         return false;
911     }
912 
913     if (you.has_spell(specspell))
914     {
915         mpr("You already know that spell!");
916         return false;
917     }
918 
919     if (you.spell_no >= MAX_KNOWN_SPELLS)
920     {
921         mpr("Your head is already too full of spells!");
922         return false;
923     }
924 
925     if (you.experience_level < spell_difficulty(specspell) && !wizard)
926     {
927         mpr("You're too inexperienced to learn that spell!");
928         return false;
929     }
930 
931     if (player_spell_levels() < spell_levels_required(specspell) && !wizard)
932     {
933         mpr("You can't memorise that many levels of magic yet!");
934         return false;
935     }
936 
937     return true;
938 }
939 
940 /**
941  * Attempt to make the player learn the given spell.
942  *
943  * @param   specspell  The spell to be learned.
944  * @param   wizard     Whether to memorise instantly and skip some checks for
945  *                     wizmode memorisation.
946  * @param   interactive whether to do the interactive prompt
947  * @return             true if the player learned the spell, false
948  *                     otherwise.
949 */
learn_spell(spell_type specspell,bool wizard,bool interactive)950 bool learn_spell(spell_type specspell, bool wizard, bool interactive)
951 {
952     if (!_learn_spell_checks(specspell, wizard))
953         return false;
954 
955     string mem_spell_warning_string = god_spell_warn_string(specspell, you.religion);
956 
957     if (!mem_spell_warning_string.empty())
958         mprf(MSGCH_WARN, "%s", mem_spell_warning_string.c_str());
959 
960     if (!wizard)
961     {
962         const int severity = fail_severity(specspell);
963 
964         if (raw_spell_fail(specspell) >= 100 && !vehumet_is_offering(specspell))
965             mprf(MSGCH_WARN, "This spell is impossible to cast!");
966         else if (severity > 0)
967         {
968             mprf(MSGCH_WARN, "This spell is %s to cast%s",
969                              fail_severity_adjs[severity],
970                              severity > 1 ? "!" : ".");
971         }
972     }
973 
974     if (interactive)
975     {
976         const string prompt = make_stringf(
977                  "Memorise %s, consuming %d spell level%s and leaving %d?",
978                  spell_title(specspell), spell_levels_required(specspell),
979                  spell_levels_required(specspell) != 1 ? "s" : "",
980                  player_spell_levels() - spell_levels_required(specspell));
981 
982         if (!yesno(prompt.c_str(), true, 'n', false))
983         {
984             canned_msg(MSG_OK);
985             return false;
986         }
987     }
988 
989     if (wizard)
990         add_spell_to_memory(specspell);
991     else
992     {
993         if (!already_learning_spell(specspell))
994             start_delay<MemoriseDelay>(spell_difficulty(specspell), specspell);
995         you.turn_is_over = true;
996 
997         did_god_conduct(DID_SPELL_CASTING, 2 + random2(5));
998     }
999 
1000     quiver::on_actions_changed();
1001 
1002     return true;
1003 }
1004 
book_has_title(const item_def & book)1005 bool book_has_title(const item_def &book)
1006 {
1007     ASSERT(book.base_type == OBJ_BOOKS);
1008 
1009     // No "A Great Wizards, Vol. II"
1010     if (book.sub_type == BOOK_BIOGRAPHIES_II
1011         || book.sub_type == BOOK_BIOGRAPHIES_VII
1012         || book.sub_type == BOOK_OZOCUBU
1013         || book.sub_type == BOOK_UNRESTRAINED)
1014     {
1015         return true;
1016     }
1017 
1018     if (!is_artefact(book))
1019         return false;
1020 
1021     return book.props.exists(BOOK_TITLED_KEY)
1022            && book.props[BOOK_TITLED_KEY].get_bool() == true;
1023 }
1024 
divine_exegesis(bool fail)1025 spret divine_exegesis(bool fail)
1026 {
1027     unwind_var<bool> dk(you.divine_exegesis, true);
1028 
1029     spell_list spells(_get_spell_list(true, true));
1030     if (spells.empty())
1031     {
1032         mpr("You don't know of any spells!");
1033         return spret::abort;
1034     }
1035 
1036     sort(spells.begin(), spells.end(), _sort_divine_spells);
1037     // If we've gotten this far, we know at least one useful spell.
1038 
1039     SpellLibraryMenu spell_menu(spells);
1040 
1041     const vector<MenuEntry*> sel = spell_menu.show();
1042     if (!crawl_state.doing_prev_cmd_again)
1043     {
1044         redraw_screen();
1045         update_screen();
1046     }
1047 
1048     if (sel.empty())
1049         return spret::abort;
1050 
1051     const spell_type spell = *static_cast<spell_type*>(sel[0]->data);
1052     if (spell == SPELL_NO_SPELL)
1053         return spret::abort;
1054 
1055     ASSERT(is_valid_spell(spell));
1056 
1057     if (fail)
1058         return spret::fail;
1059 
1060     if (cast_a_spell(false, spell))
1061         return spret::success;
1062 
1063     return spret::abort;
1064 }
1065 
1066 /// For a given dungeon depth (or item level), how much weight should we give
1067 /// to a spellbook for having a spell of the given level in it?
1068 /// We add a flat 10 to all these weights later.
1069 static const vector<random_pick_entry<int>> spell_level_weights = {
1070     {  0, 35, 100, FLAT, 1 },
1071 
1072     {  0, 35, 100, FLAT, 2 },
1073 
1074     {  0, 35, 100, FLAT, 3 },
1075 
1076     {  4,  8, 100, RISE, 4 },
1077     {  9, 35, 100, FLAT, 4 },
1078 
1079     {  6, 10, 100, RISE, 5 },
1080     { 11, 35,  80, FLAT, 5 },
1081 
1082     { 10, 14, 100, RISE, 6 },
1083     { 15, 35, 100, FLAT, 6 },
1084 
1085     { 14, 22, 100, RISE, 7 },
1086     { 23, 35, 100, FLAT, 7 },
1087 
1088     { 18, 26, 100, RISE, 8 },
1089     { 27, 35, 100, FLAT, 8 },
1090 
1091     { 22, 30, 100, RISE, 9 },
1092     { 31, 35, 100, FLAT, 9 },
1093 };
1094 
1095 /// Cap item_level at this for generation. This is roughly the bottom of the hells.
1096 static const int MAX_BOOK_LEVEL = 35;
1097 
_book_weight(book_type book,int item_level,int scale)1098 static int _book_weight(book_type book, int item_level, int scale)
1099 {
1100     item_level = min(item_level, MAX_BOOK_LEVEL);
1101     random_picker<int, 9> spell_weighter;
1102     int n_spells = 0;
1103     int weight = 0;
1104     for (spell_type spell : spellbook_template(book))
1105     {
1106         const int lvl = spell_difficulty(spell);
1107         const int spell_weight =
1108             spell_weighter.probability_at(lvl, spell_level_weights,
1109                                           item_level, scale);
1110         weight += spell_weight + scale/20; // Every spell gets a flat weight.
1111         ++n_spells;
1112     }
1113     return weight / n_spells;
1114 }
1115 
choose_book_type(int item_level)1116 book_type choose_book_type(int item_level)
1117 {
1118     map<book_type, int> book_weights;
1119     for (int i = 0; i < NUM_BOOKS; i++)
1120     {
1121         const book_type bt = (book_type)i;
1122         if (book_exists(bt))
1123             book_weights[bt] = _book_weight(bt, item_level, 1000);
1124     }
1125     return *random_choose_weighted(book_weights);
1126 }
1127