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