1 /**
2 * @file
3 * @brief Functions for generating random spellbooks.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "mpr.h"
9 #include "randbook.h"
10
11 #include <functional>
12
13 #include "artefact.h"
14 #include "database.h"
15 #include "english.h"
16 #include "item-name.h"
17 #include "item-status-flag-type.h"
18 #include "items.h"
19 #include "religion.h"
20 #include "spl-book.h"
21 #include "stringutil.h"
22
23 static string _gen_randbook_name(string subject, string owner,
24 spschool disc1, spschool disc2);
25 static string _gen_randbook_owner(god_type god, spschool disc1,
26 spschool disc2,
27 const vector<spell_type> &spells);
28
29 /// How many spells should be in a random theme book?
theme_book_size()30 int theme_book_size() { return random2avg(4, 3) + 2; }
31
32 /// A discipline chooser that only ever returns the given discipline.
forced_book_theme(spschool theme)33 function<spschool()> forced_book_theme(spschool theme)
34 {
35 return [theme]() { return theme; };
36 }
37
38 /// Choose a random valid discipline for a themed randbook.
random_book_theme()39 spschool random_book_theme()
40 {
41 vector<spschool> disciplines;
42 for (auto discipline : spschools_type::range())
43 disciplines.push_back(discipline);
44 return disciplines[random2(disciplines.size())];
45 }
46
47 /**
48 * Attempt to choose a valid discipline for a themed randbook which contains
49 * at least the given spells.
50 *
51 * XXX: really we should be trying to create a pair that covers the set,
52 * rather than trying to do it all with one...
53 *
54 * @param forced_spells A set of spells guaranteed to be in the book.
55 * @return A discipline which will match as many of those
56 * spells as possible.
57 */
matching_book_theme(const vector<spell_type> & forced_spells)58 spschool matching_book_theme(const vector<spell_type> &forced_spells)
59 {
60 map<spschool, int> seen_disciplines;
61 for (auto spell : forced_spells)
62 {
63 const spschools_type disciplines = get_spell_disciplines(spell);
64 for (auto discipline : spschools_type::range())
65 if (disciplines & discipline)
66 ++seen_disciplines[discipline];
67 }
68
69 bool matched = false;
70 for (auto seen : seen_disciplines)
71 {
72 if (seen.second == (int)forced_spells.size())
73 {
74 matched = true;
75 break;
76 }
77 }
78
79 if (!matched)
80 {
81 const spschool *discipline
82 = random_choose_weighted(seen_disciplines);
83 if (discipline)
84 return *discipline;
85 return random_book_theme();
86 }
87
88 for (auto seen : seen_disciplines)
89 seen.second = seen.second == (int)forced_spells.size() ? 1 : 0;
90 const spschool *discipline
91 = random_choose_weighted(seen_disciplines);
92 ASSERT(discipline);
93 return *discipline;
94 }
95
96 /**
97 * Can we include the given spell in our spellbook?
98 *
99 * @param agent The entity creating the book; possibly a god.
100 * @param spell The spell to be filtered.
101 * @return Whether the spell can be included.
102 */
_agent_spell_filter(int agent,spell_type spell)103 static bool _agent_spell_filter(int agent, spell_type spell)
104 {
105 // Only use actual player spells.
106 if (!is_player_book_spell(spell))
107 return false;
108
109 // Don't include spells a god dislikes, if this is an acquirement
110 // or a god gift.
111 const god_type god = agent >= AQ_SCROLL ? you.religion : (god_type)agent;
112 if (god_hates_spell(spell, god))
113 return false;
114
115 return true;
116 }
117
118 /**
119 * Can we include the given spell in our themed spellbook?
120 *
121 * @param discipline_1 The first spellschool of the book.
122 * @param discipline_2 The second spellschool of the book.
123 * @param agent The entity creating the book; possibly a god.
124 * @param prev A list of spells already chosen for the book.
125 * @param spell The spell to be filtered.
126 * @return Whether the spell can be included.
127 */
basic_themed_spell_filter(spschool discipline_1,spschool discipline_2,int agent,const vector<spell_type> & prev,spell_type spell)128 bool basic_themed_spell_filter(spschool discipline_1,
129 spschool discipline_2,
130 int agent,
131 const vector<spell_type> &prev,
132 spell_type spell)
133 {
134 if (!is_valid_spell(spell))
135 return false;
136
137 // Only include spells matching at least one of the book's disciplines.
138 const spschools_type disciplines = get_spell_disciplines(spell);
139 if (!(disciplines & discipline_1) && !(disciplines & discipline_2))
140 return false;
141
142 // Only include spells we haven't already.
143 if (count(prev.begin(), prev.end(), spell))
144 return false;
145
146 if (!_agent_spell_filter(agent, spell))
147 return false;
148
149 return true;
150 }
151
152 /**
153 * Build and return a spell filter that excludes spells that would push us over
154 * the maximum total spell levels allowed in the book.
155 *
156 * @param max_levels The max total spell levels allowed.
157 * @param subfilter A filter to check further.
158 */
capped_spell_filter(int max_levels,themed_spell_filter subfilter)159 themed_spell_filter capped_spell_filter(int max_levels,
160 themed_spell_filter subfilter)
161 {
162 if (max_levels < 1)
163 return subfilter; // don't even bother.
164
165 return [max_levels, subfilter](spschool discipline_1,
166 spschool discipline_2,
167 int agent,
168 const vector<spell_type> &prev,
169 spell_type spell)
170 {
171 if (!subfilter(discipline_1, discipline_2, agent, prev, spell))
172 return false;
173
174 int prev_levels = 0;
175 for (auto prev_spell : prev)
176 prev_levels += spell_difficulty(prev_spell);
177 if (spell_difficulty(spell) + prev_levels > max_levels)
178 return false;
179
180 return true;
181 };
182 }
183
184 /**
185 * Build and return a spell filter that forces the first several spells to
186 * be from the given list, disregarding other constraints
187 *
188 * @param forced_spells Spells to force.
189 * @param subfilter A filter to check after all forced spells are in.
190 */
forced_spell_filter(const vector<spell_type> & forced_spells,themed_spell_filter subfilter)191 themed_spell_filter forced_spell_filter(const vector<spell_type> &forced_spells,
192 themed_spell_filter subfilter)
193 {
194 return [&forced_spells, subfilter](spschool discipline_1,
195 spschool discipline_2,
196 int agent,
197 const vector<spell_type> &prev,
198 spell_type spell)
199 {
200 if (prev.size() < forced_spells.size())
201 return spell == forced_spells[prev.size()];
202 return subfilter(discipline_1, discipline_2, agent, prev, spell);
203 };
204 }
205
206 /**
207 * Generate a list of spells for a themebook.
208 *
209 * @param discipline_1 The first spellschool of the book.
210 * @param discipline_2 The second spellschool of the book.
211 * @param filter A filter specifying which spells can be included.
212 * @param agent The entity creating the book; possibly a god.
213 * @param num_spells How many spells should be included.
214 * @param spells[out] The list to be populated.
215 */
theme_book_spells(spschool discipline_1,spschool discipline_2,themed_spell_filter filter,int agent,int num_spells,vector<spell_type> & spells)216 void theme_book_spells(spschool discipline_1,
217 spschool discipline_2,
218 themed_spell_filter filter,
219 int agent,
220 int num_spells,
221 vector<spell_type> &spells)
222 {
223 ASSERT(num_spells >= 1);
224 for (int i = 0; i < num_spells; ++i)
225 {
226 vector<spell_type> possible_spells;
227 for (int s = 0; s < NUM_SPELLS; ++s)
228 {
229 const spell_type spell = static_cast<spell_type>(s);
230 if (filter(discipline_1, discipline_2, agent, spells, spell))
231 possible_spells.push_back(spell);
232 }
233
234 if (!possible_spells.size())
235 {
236 dprf("Couldn't find any valid spell for slot %d!", i);
237 return;
238 }
239
240 spells.push_back(possible_spells[random2(possible_spells.size())]);
241 }
242
243 ASSERT(spells.size());
244 }
245
246 /**
247 * Try to remove any discipline that's not actually being used by a given
248 * randbook, setting it to the other (used) discipline.
249 *
250 * E.g., if a cj/ne randbook is generated with only cj spells, set discipline_2
251 * to cj as well.
252 *
253 * @param discipline_1[in,out] The first book discipline.
254 * @param discipline_1[in,out] The second book discipline.
255 * @param spells[in] The list of spells the book should contain.
256 */
fixup_randbook_disciplines(spschool & discipline_1,spschool & discipline_2,const vector<spell_type> & spells)257 void fixup_randbook_disciplines(spschool &discipline_1,
258 spschool &discipline_2,
259 const vector<spell_type> &spells)
260 {
261 bool has_d1 = false, has_d2 = false;
262 for (auto spell : spells)
263 {
264 const spschools_type disciplines = get_spell_disciplines(spell);
265 if (disciplines & discipline_1)
266 has_d1 = true;
267 if (disciplines & discipline_2)
268 has_d2 = true;
269 }
270
271 if (has_d1 == has_d2)
272 return; // both schools or neither used; can't do anything regardless
273
274 if (has_d1)
275 discipline_2 = discipline_1;
276 else
277 discipline_1 = discipline_2;
278 }
279
280 /**
281 * Turn a given book into a themed spellbook.
282 *
283 * @param book[in,out] The book in question.
284 * @param filter A filter specifying which spells can be included.
285 * @param get_discipline A function to choose themes for the book.
286 * @param num_spells The number of spells the book should include.
287 * Not guaranteed, but should be fairly reliable.
288 * @param owner The name of the book's owner, if any. Cosmetic.
289 * @param subject The subject of the book, if any. Cosmetic.
290 */
build_themed_book(item_def & book,themed_spell_filter filter,function<spschool ()> get_discipline,int num_spells,string owner,string subject)291 void build_themed_book(item_def &book, themed_spell_filter filter,
292 function<spschool()> get_discipline,
293 int num_spells, string owner, string subject)
294 {
295 if (num_spells < 1)
296 num_spells = theme_book_size();
297
298 spschool discipline_1 = get_discipline();
299 spschool discipline_2 = get_discipline();
300
301 item_source_type agent;
302 if (!origin_is_acquirement(book, &agent))
303 agent = (item_source_type)origin_as_god_gift(book);
304
305 vector<spell_type> spells;
306 theme_book_spells(discipline_1, discipline_2, filter, agent, num_spells,
307 spells);
308 fixup_randbook_disciplines(discipline_1, discipline_2, spells);
309 init_book_theme_randart(book, spells);
310 name_book_theme_randart(book, discipline_1, discipline_2, owner, subject);
311 }
312
_compare_spells(spell_type a,spell_type b)313 static bool _compare_spells(spell_type a, spell_type b)
314 {
315 if (a == SPELL_NO_SPELL && b == SPELL_NO_SPELL)
316 return false;
317 else if (a != SPELL_NO_SPELL && b == SPELL_NO_SPELL)
318 return true;
319 else if (a == SPELL_NO_SPELL && b != SPELL_NO_SPELL)
320 return false;
321
322 int level_a = spell_difficulty(a);
323 int level_b = spell_difficulty(b);
324
325 if (level_a != level_b)
326 return level_a < level_b;
327
328 spschools_type schools_a = get_spell_disciplines(a);
329 spschools_type schools_b = get_spell_disciplines(b);
330
331 if (schools_a != schools_b && schools_a != spschool::none
332 && schools_b != spschool::none)
333 {
334 const char* a_type = nullptr;
335 const char* b_type = nullptr;
336
337 // Find lowest/earliest school for each spell.
338 for (const auto mask : spschools_type::range())
339 {
340 if (a_type == nullptr && (schools_a & mask))
341 a_type = spelltype_long_name(mask);
342 if (b_type == nullptr && (schools_b & mask))
343 b_type = spelltype_long_name(mask);
344 }
345 ASSERT(a_type != nullptr);
346 ASSERT(b_type != nullptr);
347 return strcmp(a_type, b_type) < 0;
348 }
349
350 return strcmp(spell_title(a), spell_title(b)) < 0;
351 }
352
_get_spell_list(vector<spell_type> & spells,int level,god_type god,bool avoid_uncastable,int & god_discard,int & uncastable_discard,bool avoid_known=false)353 static void _get_spell_list(vector<spell_type> &spells, int level,
354 god_type god, bool avoid_uncastable,
355 int &god_discard, int &uncastable_discard,
356 bool avoid_known = false)
357 {
358 for (int i = 0; i < NUM_SPELLS; ++i)
359 {
360 const spell_type spell = (spell_type) i;
361
362 if (!is_valid_spell(spell))
363 continue;
364
365 // Only use actual player spells.
366 if (!is_player_book_spell(spell))
367 continue;
368
369 if (avoid_known && you.spell_library[spell])
370 continue;
371
372 // fixed level randart: only include spells of the given level
373 if (level != -1 && spell_difficulty(spell) != level)
374 continue;
375
376 if (avoid_uncastable && !you_can_memorise(spell))
377 {
378 uncastable_discard++;
379 continue;
380 }
381
382 if (god_hates_spell(spell, god))
383 {
384 god_discard++;
385 continue;
386 }
387
388 // Passed all tests.
389 spells.push_back(spell);
390 }
391 }
392
_make_book_randart(item_def & book)393 static void _make_book_randart(item_def &book)
394 {
395 if (!is_artefact(book))
396 {
397 book.flags |= ISFLAG_RANDART;
398 if (!book.props.exists(ARTEFACT_APPEAR_KEY))
399 {
400 book.props[ARTEFACT_APPEAR_KEY].get_string() =
401 make_artefact_name(book, true);
402 }
403 }
404 }
405
406 /**
407 * Choose an owner for a randomly-generated single-level spellbook.
408 *
409 * @param god The god responsible for the book, if any.
410 * If set, will be the book's owner.
411 * @return An owner for the book; may be the empty string.
412 */
_gen_randlevel_owner(god_type god)413 static string _gen_randlevel_owner(god_type god)
414 {
415 if (god != GOD_NO_GOD)
416 return god_name(god, false);
417 if (one_chance_in(30))
418 return god_name(GOD_SIF_MUNA, false);
419 if (one_chance_in(3))
420 return make_name();
421 return "";
422 }
423
424 /// What's the DB lookup string for a given randbook spell level?
_randlevel_difficulty_name(int level)425 static string _randlevel_difficulty_name(int level)
426 {
427 if (level == 1)
428 return "starting";
429 if (level <= 3 || level == 4 && coinflip())
430 return "easy";
431 if (level <= 6)
432 return "moderate";
433 return "difficult";
434 }
435
436 /**
437 * Generate a name for a randomly-generated single-level spellbook.
438 *
439 * @param level The level of the spells in the book.
440 * @param god The god responsible for the book, if any.
441 * @return A spellbook name. May contain placeholders (@foo@).
442 */
_gen_randlevel_name(int level,god_type god)443 static string _gen_randlevel_name(int level, god_type god)
444 {
445 const string owner_name = _gen_randlevel_owner(god);
446 const bool has_owner = !owner_name.empty();
447 const string apostrophised_owner = owner_name.empty() ? "" :
448 apostrophise(owner_name) + " ";
449
450 if (god == GOD_XOM && coinflip())
451 {
452 const string xomname = getRandNameString("book_noun") + " of "
453 + getRandNameString("Xom_book_title");
454 return apostrophised_owner + xomname;
455 }
456
457 const string lookup = _randlevel_difficulty_name(level) + " level book";
458
459 // First try for names respecting the book's previous owner/author
460 // (if one exists), then check for general difficulty.
461 string bookname;
462 if (has_owner)
463 bookname = getRandNameString(lookup + " owner");
464
465 if (bookname.empty())
466 bookname = getRandNameString(lookup);
467
468 bookname = uppercase_first(bookname);
469 if (has_owner)
470 {
471 if (bookname.substr(0, 4) == "The ")
472 bookname = bookname.substr(4);
473 else if (bookname.substr(0, 2) == "A ")
474 bookname = bookname.substr(2);
475 else if (bookname.substr(0, 3) == "An ")
476 bookname = bookname.substr(3);
477 }
478
479 if (bookname.find("@level@", 0) != string::npos)
480 {
481 const string level_name = uppercase_first(number_in_words(level));
482 bookname = replace_all(bookname, "@level@", level_name);
483 }
484
485 if (bookname.empty())
486 bookname = getRandNameString("book");
487
488 return apostrophised_owner + bookname;
489 }
490
_set_book_spell_list(item_def & book,vector<spell_type> spells)491 void _set_book_spell_list(item_def &book, vector<spell_type> spells)
492 {
493 ASSERT(!spells.empty());
494 sort(begin(spells), end(spells), _compare_spells);
495 spells.resize(RANDBOOK_SIZE, SPELL_NO_SPELL);
496
497 CrawlHashTable &props = book.props;
498 props.erase(SPELL_LIST_KEY);
499 props[SPELL_LIST_KEY].new_vector(SV_INT).resize(RANDBOOK_SIZE);
500
501 CrawlVector &spell_vec = props[SPELL_LIST_KEY].get_vector();
502 spell_vec.set_max_size(RANDBOOK_SIZE);
503
504 for (int i = 0; i < RANDBOOK_SIZE; i++)
505 spell_vec[i].get_int() = spells[i];
506 }
507
508 /**
509 * Turn the given book into a randomly-generated spellbook ("randbook"),
510 * containing only spells of a given level.
511 *
512 * @param book[out] The book in question.
513 * @param level The level of the spells. If -1, choose a level randomly.
514 * @param god Is this a gift from Sif Muna?
515 * @return Whether the book was successfully transformed.
516 */
make_book_level_randart(item_def & book,int level,bool sif)517 bool make_book_level_randart(item_def &book, int level, bool sif)
518 {
519 ASSERT(book.base_type == OBJ_BOOKS);
520
521 const god_type god = origin_as_god_gift(book);
522
523 const bool completely_random =
524 god == GOD_XOM || (god == GOD_NO_GOD && !origin_is_acquirement(book));
525
526 if (level == -1)
527 {
528 int max_level =
529 (completely_random ? 9
530 : min(9, you.get_experience_level()));
531
532 level = random_range(1, max_level);
533 }
534 ASSERT_RANGE(level, 0 + 1, 9 + 1);
535 // Book level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
536 // Number of spells: 5 | 5 | 5 | 6 | 6 | 6 | 4 | 2 | 1
537 int num_spells = max(1, min(5 + (level - 1)/3,
538 18 - 2*level));
539 // Sif Muna retains the old randbook sizes.
540 // Other level randbooks shrink to modern book sizes.
541 if (!sif)
542 num_spells = max(1, div_rand_round(num_spells * 3, 5));
543 ASSERT_RANGE(num_spells, 0 + 1, RANDBOOK_SIZE + 1);
544
545 book.sub_type = BOOK_RANDART_LEVEL;
546 _make_book_randart(book);
547
548 int god_discard = 0;
549 int uncastable_discard = 0;
550
551 vector<spell_type> spells;
552 // Which spells are valid choices?
553 _get_spell_list(spells, level, god, !completely_random,
554 god_discard, uncastable_discard);
555
556 if (spells.empty())
557 {
558 if (level > 1)
559 return make_book_level_randart(book, level - 1);
560 char buf[80];
561
562 if (god_discard > 0 && uncastable_discard == 0)
563 {
564 snprintf(buf, sizeof(buf), "%s disliked all level %d spells",
565 god_name(god).c_str(), level);
566 }
567 else if (god_discard == 0 && uncastable_discard > 0)
568 sprintf(buf, "No level %d spells can be cast by you", level);
569 else if (god_discard > 0 && uncastable_discard > 0)
570 {
571 snprintf(buf, sizeof(buf),
572 "All level %d spells are either disliked by %s "
573 "or cannot be cast by you.",
574 level, god_name(god).c_str());
575 }
576 else
577 sprintf(buf, "No level %d spells?!?!?!", level);
578
579 mprf(MSGCH_ERROR, "Could not create fixed level randart spellbook: %s",
580 buf);
581
582 return false;
583 }
584 shuffle_array(spells);
585
586 if (num_spells > (int) spells.size())
587 {
588 num_spells = spells.size();
589 #if defined(DEBUG) || defined(DEBUG_DIAGNOSTICS)
590 mprf(MSGCH_WARN, "More spells requested for fixed level (%d) "
591 "randart spellbook than there are valid spells.",
592 level);
593 mprf(MSGCH_WARN, "Discarded %d spells due to being uncastable and "
594 "%d spells due to being disliked by %s.",
595 uncastable_discard, god_discard, god_name(god).c_str());
596 #endif
597 }
598
599 vector<bool> spell_used(spells.size(), false);
600 vector<bool> avoid_memorised(spells.size(), !completely_random);
601 vector<bool> avoid_seen(spells.size(), !completely_random);
602
603 vector<spell_type> chosen_spells(RANDBOOK_SIZE, SPELL_NO_SPELL);
604
605 int book_pos = 0;
606 while (book_pos < num_spells)
607 {
608 int spell_pos = random2(spells.size());
609
610 if (spell_used[spell_pos])
611 continue;
612
613 spell_type spell = spells[spell_pos];
614 ASSERT(spell != SPELL_NO_SPELL);
615
616 if (avoid_memorised[spell_pos] && you.has_spell(spell))
617 {
618 // Only once.
619 avoid_memorised[spell_pos] = false;
620 continue;
621 }
622
623 if (avoid_seen[spell_pos] && you.spell_library[spell] && coinflip())
624 {
625 // Only once.
626 avoid_seen[spell_pos] = false;
627 continue;
628 }
629
630 spell_used[spell_pos] = true;
631 chosen_spells.push_back(spell);
632 book_pos++;
633 }
634
635 _set_book_spell_list(book, chosen_spells);
636
637 const string name = _gen_randlevel_name(level, god);
638 set_artefact_name(book, replace_name_parts(name, book));
639 // None of these books need a definite article prepended.
640 book.props[BOOK_TITLED_KEY].get_bool() = true;
641
642 return true;
643 }
644
645 /**
646 * Initialize a themed randbook, & fill it with the given spells.
647 *
648 * @param book[in,out] The book to be initialized.
649 * @param spells The spells to fill the book with.
650 * Not passed by reference since we want to sort it.
651 */
init_book_theme_randart(item_def & book,vector<spell_type> spells)652 void init_book_theme_randart(item_def &book, vector<spell_type> spells)
653 {
654 book.sub_type = BOOK_RANDART_THEME;
655 _make_book_randart(book);
656 _set_book_spell_list(book, move(spells));
657 }
658
659 /**
660 * Generate and apply a name for a themed randbook.
661 *
662 * @param book[in,out] The book to be named.
663 * @param discipline_1 The first spellschool.
664 * @param discipline_2 The second spellschool.
665 * @param owner The book's owner; e.g. "Xom". May be empty.
666 * @param subject The subject of the book. May be empty.
667 */
name_book_theme_randart(item_def & book,spschool discipline_1,spschool discipline_2,string owner,string subject)668 void name_book_theme_randart(item_def &book, spschool discipline_1,
669 spschool discipline_2,
670 string owner, string subject)
671 {
672 if (owner.empty())
673 {
674 const vector<spell_type> spells = spells_in_book(book);
675 owner = _gen_randbook_owner(origin_as_god_gift(book), discipline_1,
676 discipline_2, spells);
677 }
678
679 book.props[BOOK_TITLED_KEY].get_bool() = !owner.empty();
680 const string name = _gen_randbook_name(subject, owner,
681 discipline_1, discipline_2);
682 set_artefact_name(book, replace_name_parts(name, book));
683 }
684
685 /**
686 * Possibly generate a 'subject' for a book based on its owner.
687 *
688 * @param owner The book's owner; e.g. "Xom".
689 * @return A random book subject, or the empty string.
690 * May contain placeholders (@foo@).
691 */
_maybe_gen_book_subject(string owner)692 static string _maybe_gen_book_subject(string owner)
693 {
694 // Sometimes use a completely random title.
695 if (owner == "Xom" && !one_chance_in(20))
696 return getRandNameString("Xom_book_title");
697 if (one_chance_in(20) && (owner.empty() || one_chance_in(3)))
698 return getRandNameString("random_book_title");
699 return "";
700 }
701
702 /**
703 * Generates a random, vaguely appropriate name for a randbook.
704 *
705 * @param subject The subject of the book. If non-empty, the book will
706 * have a name of the form "[Foo] of <subject>".
707 * @param owner The name of the book's 'owner', if any.
708 * (E.g., Xom, Cerebov, Boris...)
709 * Prepended to the book's name (Foo's...); "Xom" has
710 * further effects.
711 * @param disc1 A spellschool (discipline) associated with the book.
712 * @param disc2 A spellschool (discipline) associated with the book.
713 * @return A book name. May contain placeholders (@foo@).
714 */
_gen_randbook_name(string subject,string owner,spschool disc1,spschool disc2)715 static string _gen_randbook_name(string subject, string owner,
716 spschool disc1,
717 spschool disc2)
718 {
719 const string apostrophised_owner = owner.empty() ?
720 "" :
721 apostrophise(owner) + " ";
722
723 const string real_subject = subject.empty() ?
724 _maybe_gen_book_subject(owner) :
725 subject;
726
727 if (!real_subject.empty())
728 {
729 return make_stringf("%s%s of %s",
730 apostrophised_owner.c_str(),
731 getRandNameString("book_noun").c_str(),
732 real_subject.c_str());
733 }
734
735 string name = apostrophised_owner;
736
737 // Give a name that reflects the primary and secondary
738 // spell disciplines of the spells contained in the book.
739 name += getRandNameString("book_name") + " ";
740
741 // For the actual name there's a 66% chance of getting something like
742 // <book> of the Fiery Traveller (Translocation/Fire), else
743 // <book> of Displacement and Flames.
744 string type_name;
745 if (disc1 != disc2 && !one_chance_in(3))
746 {
747 string lookup = spelltype_long_name(disc2);
748 type_name = getRandNameString(lookup + " adj");
749 }
750
751 if (type_name.empty())
752 {
753 // No adjective found, use the normal method of combining two nouns.
754 type_name = getRandNameString(spelltype_long_name(disc1));
755 if (type_name.empty())
756 name += spelltype_long_name(disc1);
757 else
758 name += type_name;
759
760 if (disc1 != disc2)
761 {
762 name += " and ";
763 type_name = getRandNameString(spelltype_long_name(disc2));
764
765 if (type_name.empty())
766 name += spelltype_long_name(disc2);
767 else
768 name += type_name;
769 }
770 }
771 else
772 {
773 string bookname = type_name + " ";
774
775 // Add the noun for the first discipline.
776 type_name = getRandNameString(spelltype_long_name(disc1));
777 if (type_name.empty())
778 bookname += spelltype_long_name(disc1);
779 else
780 {
781 if (type_name.find("the ", 0) != string::npos)
782 {
783 type_name = replace_all(type_name, "the ", "");
784 bookname = "the " + bookname;
785 }
786 bookname += type_name;
787 }
788 name += bookname;
789 }
790
791 return name;
792 }
793
794 /**
795 * Possibly choose a random 'owner' for a themed random spellbook.
796 *
797 * @param god The god responsible for gifting the book, if any.
798 * @param disc1 A spellschool (discipline) associated with the book.
799 * @param disc2 A spellschool (discipline) associated with the book.
800 * @param spells The spells in the book.
801 * @return The name of the book's 'owner', or the empty string.
802 */
_gen_randbook_owner(god_type god,spschool disc1,spschool disc2,const vector<spell_type> & spells)803 static string _gen_randbook_owner(god_type god, spschool disc1,
804 spschool disc2,
805 const vector<spell_type> &spells)
806 {
807 // If the owner hasn't been set already use
808 // a) the god's name for god gifts (only applies to Sif Muna and Xom),
809 // b) a name depending on the spell disciplines, for pure books
810 // c) a random name (all god gifts not named earlier)
811 // d) an applicable god's name
812 // ... else leave it unnamed (around 57% chance for non-god gifts)
813
814 int highest_level = 0;
815 int lowest_level = 27;
816 bool all_spells_disc1 = true;
817 for (auto spell : spells)
818 {
819 const int level = spell_difficulty(spell);
820 highest_level = max(level, highest_level);
821 lowest_level = min(level, lowest_level);
822
823 if (!(get_spell_disciplines(spell) & disc1))
824 all_spells_disc1 = false;
825 }
826
827 // this logic is very odd...
828 const bool highlevel = highest_level >= 7 + random2(3)
829 && (lowest_level > 1 || coinflip());
830
831
832 // name of gifting god?
833 const bool god_gift = god != GOD_NO_GOD;
834 if (god_gift && !one_chance_in(4))
835 return god_name(god, false);
836
837 // thematically appropriate name?
838 if (god_gift && one_chance_in(3) || one_chance_in(5))
839 {
840 vector<string> lookups;
841 const string d1_name = spelltype_long_name(disc1);
842
843 if (disc1 != disc2)
844 {
845 const string lookup = d1_name + " " + spelltype_long_name(disc2);
846 if (highlevel)
847 lookups.push_back("highlevel " + lookup + " owner");
848 lookups.push_back(lookup + " owner");
849 }
850
851 if (all_spells_disc1)
852 {
853 if (highlevel)
854 lookups.push_back("highlevel " + d1_name + " owner");
855 lookups.push_back(d1_name + "owner");
856 }
857
858 for (string &lookup : lookups)
859 {
860 const string owner = getRandNameString(lookup);
861 if (!owner.empty() && owner != "__NONE")
862 return owner;
863 }
864 }
865
866 // random name?
867 if (god_gift || one_chance_in(5))
868 return make_name();
869
870 // applicable god's name?
871 if (!god_gift && one_chance_in(9))
872 {
873 switch (disc1)
874 {
875 case spschool::necromancy:
876 if (all_spells_disc1 && !one_chance_in(6))
877 return god_name(GOD_KIKUBAAQUDGHA, false);
878 break;
879 case spschool::conjuration:
880 if (all_spells_disc1 && !one_chance_in(4))
881 return god_name(GOD_VEHUMET, false);
882 break;
883 default:
884 break;
885 }
886 return god_name(GOD_SIF_MUNA, false);
887 }
888
889 return "";
890 }
891
892 // Give Roxanne a randart spellbook of the disciplines Transmutations/Earth
893 // that includes Statue Form and is named after her.
make_book_roxanne_special(item_def * book)894 void make_book_roxanne_special(item_def *book)
895 {
896 spschool disc = random_choose(spschool::transmutation, spschool::earth);
897 vector<spell_type> forced_spell = {SPELL_STATUE_FORM};
898 build_themed_book(*book,
899 forced_spell_filter(forced_spell,
900 capped_spell_filter(19)),
901 forced_book_theme(disc), 5, "Roxanne");
902 }
903
904 /// Does the given acq source generate books totally randomly?
_completely_random_books(int agent)905 static bool _completely_random_books(int agent)
906 {
907 // only acq & god gifts from sane gods weight spells/disciplines
908 // for player utility.
909 return agent == GOD_XOM || agent == GOD_NO_GOD;
910 }
911
912 /// How desireable is the given spell for inclusion in an acquired randbook?
_randbook_spell_weight(spell_type spell,int agent)913 static int _randbook_spell_weight(spell_type spell, int agent)
914 {
915 if (_completely_random_books(agent))
916 return 1;
917
918 // prefer unseen spells
919 const int seen_weight = you.spell_library[spell] ? 1 : 4;
920
921 // prefer spells roughly approximating the player's overall spellcasting
922 // ability (?????)
923 const int Spc = div_rand_round(you.skill(SK_SPELLCASTING, 256, true), 256);
924 const int difficult_weight = 5 - abs(3 * spell_difficulty(spell) - Spc) / 7;
925
926 // prefer spells in disciplines the player is skilled with
927 const spschools_type disciplines = get_spell_disciplines(spell);
928 int total_skill = 0;
929 int num_skills = 0;
930 for (const auto disc : spschools_type::range())
931 {
932 if (disciplines & disc)
933 {
934 const skill_type sk = spell_type2skill(disc);
935 total_skill += div_rand_round(you.skill(sk, 256, true), 256);
936 num_skills++;
937 }
938 }
939 int skill_weight = 1;
940 if (num_skills > 0)
941 skill_weight = (2 + (total_skill / num_skills)) / 3;
942 skill_weight = max(1, skill_weight);
943
944 const int weight = seen_weight * skill_weight * difficult_weight;
945 ASSERT(weight > 0);
946 return weight;
947 /// XXX: I'm not sure how much impact all this actually has.
948 }
949
950 typedef map<spell_type, int> weighted_spells;
951
952 /**
953 * Populate a list of possible spells to be included in acquired books,
954 * weighted by desireability.
955 *
956 * @param possible_spells[out] The list to be populated.
957 * @param agent The entity creating the item; possibly a god.
958 */
_get_weighted_randbook_spells(weighted_spells & possible_spells,int agent)959 static void _get_weighted_randbook_spells(weighted_spells &possible_spells,
960 int agent)
961 {
962 for (int i = 0; i < NUM_SPELLS; ++i)
963 {
964 const spell_type spell = (spell_type) i;
965
966 if (!is_valid_spell(spell)
967 || !_agent_spell_filter(agent, spell)
968 || !you_can_memorise(spell))
969 {
970 continue;
971 }
972
973 // Passed all tests.
974 const int weight = _randbook_spell_weight(spell, agent);
975 possible_spells[spell] = weight;
976 }
977 }
978
979 /**
980 * Choose a spell discipline for a randbook, weighted by the the value of all
981 * possible spells in that discipline.
982 *
983 * @param possibles A weighted list of all possible spells to include in
984 * the book.
985 * @param agent The entity creating the item; possibly a god.
986 * @return An appropriate spell school; e.g. spschool::fire.
987 */
_choose_randbook_discipline(weighted_spells & possible_spells,int agent)988 static spschool _choose_randbook_discipline(weighted_spells
989 &possible_spells,
990 int agent)
991 {
992 map<spschool, int> discipline_weights;
993 for (auto weighted_spell : possible_spells)
994 {
995 const spell_type spell = weighted_spell.first;
996 const int weight = weighted_spell.second;
997 const spschools_type disciplines = get_spell_disciplines(spell);
998 for (const auto disc : spschools_type::range())
999 {
1000 if (disciplines & disc)
1001 {
1002 if (_completely_random_books(agent))
1003 discipline_weights[disc] = 1;
1004 else
1005 discipline_weights[disc] += weight;
1006 }
1007 }
1008 }
1009
1010 const spschool *discipline
1011 = random_choose_weighted(discipline_weights);
1012 ASSERT(discipline);
1013 return *discipline;
1014 }
1015
1016 /**
1017 * From a given weighted list of possible spells, choose a set to include in
1018 * a randbook, filtered by discipline.
1019 *
1020 * @param[in,out] possible_spells All possible spells, weighted by value.
1021 Modified in-place for efficiency.
1022 * @param discipline_1 The first spellschool.
1023 * @param discipline_2 The second spellschool.
1024 * @param size The number of spells to include.
1025 * @param[out] spells The chosen spells.
1026 */
_choose_themed_randbook_spells(weighted_spells & possible_spells,spschool discipline_1,spschool discipline_2,int size,vector<spell_type> & spells)1027 static void _choose_themed_randbook_spells(weighted_spells &possible_spells,
1028 spschool discipline_1,
1029 spschool discipline_2,
1030 int size, vector<spell_type> &spells)
1031 {
1032 for (auto &weighted_spell : possible_spells)
1033 {
1034 const spell_type spell = weighted_spell.first;
1035 const spschools_type disciplines = get_spell_disciplines(spell);
1036 if (!(disciplines & discipline_1) && !(disciplines & discipline_2))
1037 weighted_spell.second = 0; // filter it out
1038 }
1039
1040 for (int i = 0; i < size; ++i)
1041 {
1042 const spell_type *spell = random_choose_weighted(possible_spells);
1043 if (!spell)
1044 break;
1045 spells.push_back(*spell);
1046 possible_spells[*spell] = 0; // don't choose the same one twice!
1047 }
1048 // `size` is guaranteed to be >0 by an ASSERT in the calling function
1049 ASSERT(spells.size() > 0);
1050 }
1051
1052 /**
1053 * Turn a given book into an acquirement-quality themed spellbook.
1054 *
1055 * @param book[out] The book to be turned into a randbook.
1056 * @param agent The entity creating the item; possibly a god.
1057 */
acquire_themed_randbook(item_def & book,int agent)1058 void acquire_themed_randbook(item_def &book, int agent)
1059 {
1060 weighted_spells possible_spells;
1061 _get_weighted_randbook_spells(possible_spells, agent);
1062
1063 // include 2-8 spells in the book, leaning heavily toward 5
1064 const int size = min(2 + random2avg(7, 3),
1065 (int)possible_spells.size());
1066 ASSERT(size);
1067
1068 // XXX: we could cache this...
1069 spschool discipline_1
1070 = _choose_randbook_discipline(possible_spells, agent);
1071 spschool discipline_2
1072 = _choose_randbook_discipline(possible_spells, agent);
1073
1074 vector<spell_type> spells;
1075 _choose_themed_randbook_spells(possible_spells, discipline_1, discipline_2,
1076 size, spells);
1077
1078 fixup_randbook_disciplines(discipline_1, discipline_2, spells);
1079
1080 // Acquired randart books have a chance of being named after the player.
1081 const string owner = agent == AQ_SCROLL && one_chance_in(12) ?
1082 you.your_name :
1083 "";
1084
1085 init_book_theme_randart(book, spells);
1086 name_book_theme_randart(book, discipline_1, discipline_2, owner);
1087 }
1088