/** * @file * @brief Spellbook/rod contents array and management functions **/ #include "AppHdr.h" #include "spl-book.h" #include #include #include #include #include #include "artefact.h" #include "cio.h" #include "colour.h" #include "database.h" #include "delay.h" #include "describe.h" #include "directn.h" #include "effects.h" #include "end.h" #include "externs.h" #include "food.h" #include "format.h" #include "godconduct.h" #include "goditem.h" #include "hints.h" #include "invent.h" #include "itemname.h" #include "itemprop.h" #include "items.h" #include "libutil.h" #include "macro.h" #include "message.h" #include "options.h" #include "output.h" #include "player.h" #include "prompt.h" #include "religion.h" #include "species.h" #include "spl-cast.h" #include "spl-util.h" #include "state.h" #include "strings.h" #include "target.h" #ifdef USE_TILE #include "tilepick.h" #endif #include "transform.h" #include "unicode.h" #define SPELL_LIST_KEY "spell_list" #define RANDART_BOOK_TYPE_KEY "randart_book_type" #define RANDART_BOOK_LEVEL_KEY "randart_book_level" #define RANDART_BOOK_TYPE_LEVEL "level" #define RANDART_BOOK_TYPE_THEME "theme" // The list of spells in spellbooks: #include "book-data.h" spell_type which_spell_in_book(const item_def &book, int spl) { ASSERT(book.base_type == OBJ_BOOKS || book.base_type == OBJ_RODS); const CrawlHashTable &props = book.props; if (!props.exists(SPELL_LIST_KEY)) return which_spell_in_book(book.book_number(), spl); const CrawlVector &spells = props[SPELL_LIST_KEY].get_vector(); ASSERT(spells.get_type() == SV_INT); ASSERT(spells.size() == SPELLBOOK_SIZE); return static_cast(spells[spl].get_int()); } spell_type which_spell_in_book(int sbook_type, int spl) { ASSERT_RANGE(sbook_type, 0, (int)ARRAYSZ(spellbook_template_array)); return spellbook_template_array[sbook_type][spl]; } // If fs is not NULL, updates will be to the formatted_string instead of // the display. int spellbook_contents(item_def &book, read_book_action_type action, formatted_string *fs) { int spelcount = 0; int i, j; bool update_screen = !fs; const int spell_levels = player_spell_levels(); formatted_string out; out.textcolor(LIGHTGREY); out.cprintf("%s", book.name(DESC_THE).c_str()); out.cprintf("\n\n Spells Type Level\n"); for (j = 0; j < SPELLBOOK_SIZE; j++) { spell_type stype = which_spell_in_book(book, j); if (stype == SPELL_NO_SPELL) continue; out.cprintf(" "); const int level_diff = spell_difficulty(stype); const int levels_req = spell_levels_required(stype); int colour = DARKGREY; if (action == RBOOK_USE_ROD) { ASSERT(book.base_type == OBJ_RODS); if (book.plus >= level_diff * ROD_CHARGE_MULT) colour = spell_highlight_by_utility(stype, COL_UNKNOWN, false, true); else colour = COL_USELESS; } else { if (you.has_spell(stype)) colour = COL_MEMORIZED; else if (you_cannot_memorise(stype) || you.experience_level < level_diff || spell_levels < levels_req || book.base_type == OBJ_BOOKS && !player_can_memorise_from_spellbook(book)) { colour = COL_USELESS; } else if (!you.has_spell(stype)) colour = COL_UNMEMORIZED; else colour = spell_highlight_by_utility(stype); } out.textcolor(colour); char strng[2]; strng[0] = index_to_letter(spelcount); strng[1] = 0; out.cprintf("%s", strng); out.cprintf(" - "); out.cprintf("%s", chop_string(spell_title(stype), 29).c_str()); string schools; if (action == RBOOK_USE_ROD) schools = "Evocations"; else { bool first = true; for (i = 0; i <= SPTYP_LAST_EXPONENT; i++) { if (spell_typematch(stype, 1 << i)) { if (!first) schools += "/"; schools += spelltype_long_name(1 << i); first = false; } } } out.cprintf("%s%d\n", chop_string(schools, 30).c_str(), level_diff); spelcount++; } out.textcolor(LIGHTGREY); out.cprintf("\n"); switch (action) { case RBOOK_USE_ROD: out.cprintf("Select a spell to cast.\n"); break; case RBOOK_READ_SPELL: if (book.base_type == OBJ_BOOKS && in_inventory(book) && item_type_known(book) && player_can_memorise_from_spellbook(book)) { out.cprintf("Select a spell to read its description, to " "memorise it or to forget it.\n"); } else out.cprintf("Select a spell to read its description.\n"); break; default: break; } if (fs) *fs = out; int keyn = 0; if (update_screen && !crawl_state.is_replaying_keys()) { cursor_control coff(false); clrscr(); out.display(); } if (update_screen) keyn = toalower(getchm(KMC_MENU)); return keyn; // either ignored or spell letter } // Rarity 100 is reserved for unused books. int book_rarity(uint8_t which_book) { switch (which_book) { case BOOK_MINOR_MAGIC: case BOOK_HINDERANCE: case BOOK_CANTRIPS: return 1; case BOOK_CHANGES: case BOOK_MALEDICT: return 2; case BOOK_CONJURATIONS: case BOOK_NECROMANCY: case BOOK_CALLINGS: case BOOK_WIZARDRY: return 3; case BOOK_FLAMES: case BOOK_FROST: case BOOK_AIR: case BOOK_GEOMANCY: return 4; case BOOK_YOUNG_POISONERS: case BOOK_BATTLE: case BOOK_DEBILITATION: return 5; case BOOK_CLOUDS: case BOOK_POWER: return 6; case BOOK_ENCHANTMENTS: case BOOK_PARTY_TRICKS: return 7; case BOOK_TRANSFIGURATIONS: case BOOK_BEASTS: return 8; case BOOK_FIRE: case BOOK_ICE: case BOOK_SKY: case BOOK_EARTH: case BOOK_UNLIFE: case BOOK_CONTROL: case BOOK_SPATIAL_TRANSLOCATIONS: return 10; case BOOK_TEMPESTS: case BOOK_DEATH: case BOOK_SUMMONINGS: return 11; case BOOK_BURGLARY: case BOOK_ALCHEMY: case BOOK_DREAMS: return 12; case BOOK_ENVENOMATIONS: case BOOK_WARP: case BOOK_DRAGON: return 15; case BOOK_ANNIHILATIONS: case BOOK_GRAND_GRIMOIRE: case BOOK_NECRONOMICON: // Kikubaaqudgha special case BOOK_MANUAL: return 20; case BOOK_DESTRUCTION: return 30; #if TAG_MAJOR_VERSION == 34 case BOOK_STALKING: case BOOK_WAR_CHANTS: return 100; #endif default: return 1; } } static uint8_t _lowest_rarity[NUM_SPELLS]; void init_spell_rarities() { for (int i = 0; i < NUM_SPELLS; ++i) _lowest_rarity[i] = 255; for (int i = 0; i < NUM_FIXED_BOOKS; ++i) { // Manuals and books of destruction are not even part of this loop. if (i >= MIN_RARE_BOOK && i <= MAX_RARE_BOOK) continue; for (int j = 0; j < SPELLBOOK_SIZE; ++j) { spell_type spell = which_spell_in_book(i, j); if (spell == SPELL_NO_SPELL) continue; #ifdef DEBUG unsigned int flags = get_spell_flags(spell); if (flags & (SPFLAG_MONSTER | SPFLAG_TESTING)) { item_def item; item.base_type = OBJ_BOOKS; item.sub_type = i; end(1, false, "Spellbook '%s' contains invalid spell " "'%s'", item.name(DESC_PLAIN, false, true).c_str(), spell_title(spell)); } #endif const int rarity = book_rarity(i); if (rarity < _lowest_rarity[spell]) _lowest_rarity[spell] = rarity; } } } bool is_player_spell(spell_type which_spell) { for (int i = 0; i < NUM_FIXED_BOOKS; ++i) { for (int j = 0; j < SPELLBOOK_SIZE; ++j) { if (which_spell_in_book(i, j) == which_spell) return true; } } return false; } int spell_rarity(spell_type which_spell) { const int rarity = _lowest_rarity[which_spell]; if (rarity == 255) return -1; return rarity; } static bool _is_valid_spell_in_book(const item_def &book, int spell) { return which_spell_in_book(book, spell) != SPELL_NO_SPELL; } // Returns false if the player cannot memorise from the book, // and true otherwise. -- bwr bool player_can_memorise_from_spellbook(const item_def &book) { if (book.base_type != OBJ_BOOKS) return true; if (book.props.exists(SPELL_LIST_KEY)) return true; if ((book.sub_type == BOOK_ANNIHILATIONS && (you.skill(SK_CONJURATIONS) < 10 || you.skill(SK_SPELLCASTING) < 6)) || (book.sub_type == BOOK_GRAND_GRIMOIRE && (you.skill(SK_SUMMONINGS) < 10 || you.skill(SK_SPELLCASTING) < 6)) || (book.sub_type == BOOK_NECRONOMICON && !you_worship(GOD_KIKUBAAQUDGHA) && (you.skill(SK_NECROMANCY) < 10 || you.skill(SK_SPELLCASTING) < 6))) { return false; } return true; } void mark_had_book(const item_def &book) { ASSERT(book.base_type == OBJ_BOOKS); if (!item_is_spellbook(book)) return; for (int i = 0; i < SPELLBOOK_SIZE; i++) { spell_type stype = which_spell_in_book(book, i); if (stype == SPELL_NO_SPELL) continue; you.seen_spell.set(stype); } if (book.sub_type == BOOK_RANDART_LEVEL) ASSERT_RANGE(book.plus, 1, 10); // book's level if (!book.props.exists(SPELL_LIST_KEY)) mark_had_book(book.book_number()); } void mark_had_book(int booktype) { ASSERT_RANGE(booktype, 0, MAX_FIXED_BOOK + 1); you.had_book.set(booktype); } void inscribe_book_highlevel(item_def &book) { if (!item_type_known(book) && book.inscription.find("highlevel") == string::npos) { add_inscription(book, "highlevel"); } } /** * Identify a held book/rod, if appropriate. * @return whether we can see its spells */ bool maybe_id_book(item_def &book, bool silent) { if (book.base_type != OBJ_BOOKS && book.base_type != OBJ_RODS) return false; if (book.base_type == OBJ_BOOKS && book.sub_type == BOOK_DESTRUCTION) { ASSERT(fully_identified(book)); return false; } if (book.base_type == OBJ_BOOKS && book.sub_type == BOOK_MANUAL) { set_ident_flags(book, ISFLAG_IDENT_MASK); return false; } if (book.base_type == OBJ_BOOKS && !item_type_known(book) && !player_can_memorise_from_spellbook(book)) { if (!silent) { mpr("This book is beyond your current level of understanding."); more(); } inscribe_book_highlevel(book); return false; } set_ident_flags(book, ISFLAG_IDENT_MASK); if (book.base_type == OBJ_BOOKS) mark_had_book(book); return true; } int read_book(item_def &book, read_book_action_type action) { if (!maybe_id_book(book)) return 0; #ifdef USE_TILE_WEB tiles_crt_control show_as_menu(CRT_MENU, "read_book"); #endif // Remember that this function is called for rods as well. const int keyin = spellbook_contents(book, action); if (!crawl_state.is_replaying_keys()) redraw_screen(); return keyin; } bool you_cannot_memorise(spell_type spell) { bool temp; return you_cannot_memorise(spell, temp); } // form is set to true if a form (lich) prevents us from // memorising the spell. bool you_cannot_memorise(spell_type spell, bool &form) { bool rc = false; if (you.is_undead) { switch (spell) { case SPELL_BORGNJORS_REVIVIFICATION: case SPELL_DEATHS_DOOR: case SPELL_NECROMUTATION: // Prohibited to all undead. rc = true; break; case SPELL_CURE_POISON: case SPELL_STONESKIN: case SPELL_BEASTLY_APPENDAGE: case SPELL_BLADE_HANDS: case SPELL_DRAGON_FORM: case SPELL_ICE_FORM: case SPELL_SPIDER_FORM: case SPELL_STATUE_FORM: // Allowed for vampires (depending on hunger). rc = (you.is_undead != US_SEMI_UNDEAD); break; case SPELL_INTOXICATE: case SPELL_REGENERATION: // Only prohibited for liches and mummies. rc = (you.is_undead == US_UNDEAD); break; default: break; } // If our undeath is only temporary, mark that fact. This // assumes that the already undead cannot enter lich form. if (rc && you.form == TRAN_LICH) form = true; } if (you.species == SP_GARGOYLE && spell == SPELL_CURE_POISON) rc = true, form = false; if (you.species == SP_DEEP_DWARF && spell == SPELL_REGENERATION) rc = true, form = false; if (you.species == SP_FELID && (spell == SPELL_PORTAL_PROJECTILE // weapon branding is useless || spell == SPELL_WARP_BRAND || spell == SPELL_EXCRUCIATING_WOUNDS || spell == SPELL_SURE_BLADE // could be useful if it didn't require wielding || spell == SPELL_SPECTRAL_WEAPON)) { rc = true, form = false; } #if TAG_MAJOR_VERSION == 34 if (you.species == SP_DJINNI && (spell == SPELL_ICE_FORM || spell == SPELL_OZOCUBUS_ARMOUR || spell == SPELL_LEDAS_LIQUEFACTION)) { rc = true, form = false; } if (you.species == SP_LAVA_ORC && (spell == SPELL_STONESKIN || spell == SPELL_OZOCUBUS_ARMOUR)) { rc = true, form = false; } #endif if (you.species == SP_FORMICID && (spell == SPELL_BLINK || spell == SPELL_CONTROL_TELEPORT || spell == SPELL_CONTROLLED_BLINK)) { rc = true, form = false; } if (spell == SPELL_SUBLIMATION_OF_BLOOD) { // XXX: Using player::cannot_bleed will incorrectly // catch statue- or lich-formed players. if (you.species == SP_GARGOYLE || you.species == SP_GHOUL || you.species == SP_MUMMY) { rc = true; form = false; } else if (!form_can_bleed(you.form)) { rc = true; form = true; } } return rc; } bool player_can_memorise(const item_def &book) { if (!item_is_spellbook(book) || !player_spell_levels()) return false; for (int j = 0; j < SPELLBOOK_SIZE; j++) { const spell_type stype = which_spell_in_book(book, j); if (stype == SPELL_NO_SPELL) continue; // Easiest spell already too difficult? if (spell_difficulty(stype) > you.experience_level || player_spell_levels() < spell_levels_required(stype)) { return false; } bool knows_spell = false; for (int i = 0; i < MAX_KNOWN_SPELLS && !knows_spell; i++) knows_spell = (you.spells[i] == stype); // You don't already know this spell. if (!knows_spell) return true; } return false; } typedef vector spell_list; typedef map spells_to_books; static void _index_book(item_def& book, spells_to_books &book_hash, unsigned int &num_unreadable, bool &book_errors) { if (!player_can_memorise_from_spellbook(book)) { inscribe_book_highlevel(book); num_unreadable++; return; } mark_had_book(book); set_ident_flags(book, ISFLAG_KNOW_TYPE); set_ident_flags(book, ISFLAG_IDENT_MASK); int spells_in_book = 0; for (int j = 0; j < SPELLBOOK_SIZE; j++) { if (!_is_valid_spell_in_book(book, j)) continue; const spell_type spell = which_spell_in_book(book, j); spells_in_book++; spells_to_books::iterator it = book_hash.find(spell); if (it == book_hash.end()) book_hash[spell] = book.sub_type; } if (spells_in_book == 0) { mprf(MSGCH_ERROR, "Spellbook \"%s\" contains no spells! Please " "file a bug report.", book.name(DESC_PLAIN).c_str()); book_errors = true; } } static bool _get_mem_list(spell_list &mem_spells, spells_to_books &book_hash, unsigned int &num_unreadable, unsigned int &num_race, bool just_check = false, spell_type current_spell = SPELL_NO_SPELL) { bool book_errors = false; unsigned int num_on_ground = 0; unsigned int num_books = 0; unsigned int num_unknown = 0; num_unreadable = 0; // Collect the list of all spells in all available spellbooks. for (int i = 0; i < ENDOFPACK; i++) { item_def& book(you.inv[i]); if (!item_is_spellbook(book)) continue; num_books++; _index_book(book, book_hash, num_unreadable, book_errors); } // We also check the ground vector items; item_list_on_square(items, you.visible_igrd(you.pos())); for (unsigned int i = 0; i < items.size(); ++i) { item_def book(*items[i]); if (!item_is_spellbook(book)) continue; if (!item_type_known(book)) { num_unknown++; continue; } num_books++; num_on_ground++; _index_book(book, book_hash, num_unreadable, book_errors); } // Handle Vehumet gifts set::iterator gift_iterator = you.vehumet_gifts.begin(); if (gift_iterator != you.vehumet_gifts.end()) { num_books++; while (gift_iterator != you.vehumet_gifts.end()) book_hash[*gift_iterator++] = NUM_BOOKS; } if (book_errors) more(); if (num_books == 0) { if (!just_check) { if (num_unknown > 1) mprf(MSGCH_PROMPT, "You must pick up those books before reading them."); else if (num_unknown == 1) mprf(MSGCH_PROMPT, "You must pick up this book before reading it."); else mprf(MSGCH_PROMPT, "You aren't carrying or standing over any spellbooks."); } return false; } else if (num_unreadable == num_books) { if (!just_check) { mprf(MSGCH_PROMPT, "All of the spellbooks%s are beyond your " "current level of comprehension.", num_on_ground == 0 ? " you're carrying" : ""); } return false; } else if (book_hash.empty()) { if (!just_check) mprf(MSGCH_PROMPT, "None of the spellbooks you are carrying contain any spells."); return false; } unsigned int num_known = 0; num_race = 0; unsigned int num_low_xl = 0; unsigned int num_low_levels = 0; unsigned int num_memable = 0; bool form = false; for (spells_to_books::iterator i = book_hash.begin(); i != book_hash.end(); ++i) { const spell_type spell = i->first; if (spell == current_spell || you.has_spell(spell)) num_known++; else if (you_cannot_memorise(spell, form)) num_race++; else { mem_spells.push_back(spell); int avail_slots = player_spell_levels(); if (current_spell != SPELL_NO_SPELL) avail_slots -= spell_levels_required(current_spell); if (spell_difficulty(spell) > you.experience_level) num_low_xl++; else if (avail_slots < spell_levels_required(spell)) num_low_levels++; else num_memable++; } } if (num_memable) return true; // Return true even if there are only spells we can't memorise _yet_. if (just_check) return num_low_levels > 0 || num_low_xl > 0; unsigned int total = num_known + num_race + num_low_xl + num_low_levels; if (num_known == total) mprf(MSGCH_PROMPT, "You already know all available spells."); else if (num_race == total || (num_known + num_race) == total) { if (form) { mprf(MSGCH_PROMPT, "You cannot currently memorise any of the " "available spells because you are in %s form.", uppercase_first(transform_name()).c_str()); } else { mprf(MSGCH_PROMPT, "You cannot memorise any of the available " "spells because you are %s.", article_a(species_name(you.species)).c_str()); } } else if (num_low_levels > 0 || num_low_xl > 0) { // Just because we can't memorise them doesn't mean we don't want to // see what we have available. See FR #235. {due} return true; } else { mprf(MSGCH_PROMPT, "You can't memorise any new spells for an unknown " "reason; please file a bug report."); } if (num_unreadable) { mprf(MSGCH_PROMPT, "Additionally, %u of your spellbooks are beyond " "your current level of understanding, and thus none of the " "spells in them are available to you.", num_unreadable); } return false; } // If current_spell is a valid spell, returns whether you'll be able to // memorise any further spells once this one is committed to memory. bool has_spells_to_memorise(bool silent, int current_spell) { spell_list mem_spells; spells_to_books book_hash; unsigned int num_unreadable; unsigned int num_race; return _get_mem_list(mem_spells, book_hash, num_unreadable, num_race, silent, (spell_type) current_spell); } static bool _sort_mem_spells(spell_type a, spell_type b) { // List the Vehumet gifts at the very top. bool offering_a = vehumet_is_offering(a); bool offering_b = vehumet_is_offering(b); if (offering_a != offering_b) return offering_a; // List spells we can memorize right away first. if (player_spell_levels() >= spell_levels_required(a) && player_spell_levels() < spell_levels_required(b)) { return true; } else if (player_spell_levels() < spell_levels_required(a) && player_spell_levels() >= spell_levels_required(b)) { return false; } // Don't sort by failure rate beyond what the player can see in the // success descriptions. const int fail_rate_a = failure_rate_to_int(spell_fail(a)); const int fail_rate_b = failure_rate_to_int(spell_fail(b)); if (fail_rate_a != fail_rate_b) return fail_rate_a < fail_rate_b; if (spell_difficulty(a) != spell_difficulty(b)) return spell_difficulty(a) < spell_difficulty(b); return strcasecmp(spell_title(a), spell_title(b)) < 0; } vector get_mem_spell_list(vector &books) { vector spells; spell_list mem_spells; spells_to_books book_hash; unsigned int num_unreadable; unsigned int num_race; if (!_get_mem_list(mem_spells, book_hash, num_unreadable, num_race)) return spells; sort(mem_spells.begin(), mem_spells.end(), _sort_mem_spells); for (unsigned int i = 0; i < mem_spells.size(); i++) { spell_type spell = mem_spells[i]; spells.push_back(spell); spells_to_books::iterator it = book_hash.find(spell); books.push_back(it->second); } return spells; } static spell_type _choose_mem_spell(spell_list &spells, spells_to_books &book_hash, unsigned int num_unreadable, unsigned int num_race) { sort(spells.begin(), spells.end(), _sort_mem_spells); #ifdef USE_TILE_LOCAL const bool text_only = false; #else const bool text_only = true; #endif ToggleableMenu spell_menu(MF_SINGLESELECT | MF_ANYPRINTABLE | MF_ALWAYS_SHOW_MORE | MF_ALLOW_FORMATTING, text_only); #ifdef USE_TILE_LOCAL // [enne] Hack. Use a separate title, so the column headers are aligned. spell_menu.set_title( new MenuEntry(" Your Spells - Memorisation (toggle to descriptions with '!')", MEL_TITLE)); spell_menu.set_title( new MenuEntry(" Your Spells - Descriptions (toggle to memorisation with '!')", MEL_TITLE), false); { MenuEntry* me = new MenuEntry(" Spells Type " " Failure Level", MEL_ITEM); me->colour = BLUE; spell_menu.add_entry(me); } #else spell_menu.set_title( new MenuEntry(" Spells (Memorisation) Type " " Failure Level", MEL_TITLE)); spell_menu.set_title( new MenuEntry(" Spells (Description) Type " " Failure Level", MEL_TITLE), false); #endif spell_menu.set_highlighter(NULL); spell_menu.set_tag("spell"); spell_menu.action_cycle = Menu::CYCLE_TOGGLE; spell_menu.menu_action = Menu::ACT_EXECUTE; const bool shortmsg = num_unreadable > 0 && num_race > 0; string more_str = make_stringf("%d %slevel%s left" "", player_spell_levels(), shortmsg ? "" : "spell ", (player_spell_levels() > 1 || player_spell_levels() == 0) ? "s" : ""); if (num_unreadable > 0) { more_str += make_stringf(", %u %sbook%s", num_unreadable, shortmsg ? "difficult " : "overly difficult spell", num_unreadable > 1 ? "s" : ""); } if (num_race > 0) { more_str += make_stringf(", %u%s%s unmemorisable" "", num_race, shortmsg ? "" : " spell", // shorter message if we have both annotations !shortmsg && num_race > 1 ? "s" : ""); } #ifndef USE_TILE_LOCAL // Tiles menus get this information in the title. more_str += " Toggle display with '!'"; #endif spell_menu.set_more(formatted_string::parse_string(more_str)); // Don't make a menu so tall that we recycle hotkeys on the same page. if (spells.size() > 52 && (spell_menu.maxpagesize() > 52 || spell_menu.maxpagesize() == 0)) { spell_menu.set_maxpagesize(52); } for (unsigned int i = 0; i < spells.size(); i++) { const spell_type spell = spells[i]; ostringstream desc; int colour = LIGHTGRAY; if (vehumet_is_offering(spell)) colour = LIGHTBLUE; // Grey out spells for which you lack experience or spell levels. else if (spell_difficulty(spell) > you.experience_level || player_spell_levels() < spell_levels_required(spell)) colour = DARKGRAY; else colour = spell_highlight_by_utility(spell); desc << "<" << colour_to_str(colour) << ">"; desc << left; desc << chop_string(spell_title(spell), 30); desc << spell_schools_string(spell); int so_far = strwidth(desc.str()) - (colour_to_str(colour).length()+2); if (so_far < 60) desc << string(60 - so_far, ' '); desc << ""; colour = failure_rate_colour(spell); desc << "<" << colour_to_str(colour) << ">"; char* failure = failure_rate_to_string(spell_fail(spell)); desc << chop_string(failure, 12); free(failure); desc << ""; desc << spell_difficulty(spell); MenuEntry* me = new MenuEntry(desc.str(), MEL_ITEM, 1, index_to_letter(i % 52)); #ifdef USE_TILE me->add_tile(tile_def(tileidx_spell(spell), TEX_GUI)); #endif me->data = &spells[i]; spell_menu.add_entry(me); } while (true) { vector sel = spell_menu.show(); if (!crawl_state.doing_prev_cmd_again) redraw_screen(); if (sel.empty()) return SPELL_NO_SPELL; ASSERT(sel.size() == 1); const spell_type spell = *static_cast(sel[0]->data); ASSERT(is_valid_spell(spell)); if (spell_menu.menu_action == Menu::ACT_EXAMINE) describe_spell(spell); else return spell; } } bool can_learn_spell(bool silent) { if (you.form == TRAN_BAT) { if (!silent) canned_msg(MSG_PRESENT_FORM); return false; } if (you.stat_zero[STAT_INT]) { if (!silent) mpr("Your brain is not functional enough to learn spells."); return false; } if (you.confused()) { if (!silent) canned_msg(MSG_TOO_CONFUSED); return false; } if (you.berserk()) { if (!silent) canned_msg(MSG_TOO_BERSERK); return false; } return true; } bool learn_spell() { if (!can_learn_spell()) return false; spell_list mem_spells; spells_to_books book_hash; unsigned int num_unreadable, num_race; if (!_get_mem_list(mem_spells, book_hash, num_unreadable, num_race)) return false; spell_type specspell = _choose_mem_spell(mem_spells, book_hash, num_unreadable, num_race); if (specspell == SPELL_NO_SPELL) { canned_msg(MSG_OK); return false; } return learn_spell(specspell); } // Returns a string about why a character can't memorise a spell. string desc_cannot_memorise_reason(bool form) { string desc = "You cannot "; if (form) desc += "currently "; desc += "memorise or cast this spell because you are "; if (form) desc += "in " + uppercase_first(transform_name()) + " form"; else desc += article_a(species_name(you.species)); desc += "."; return desc; } /** * Can the player learn the given spell? * * @param specspell The spell to be learned. * @return false if the player can't learn the spell for any * reason, true otherwise. */ static bool _learn_spell_checks(spell_type specspell) { if (!can_learn_spell()) return false; if (already_learning_spell((int) specspell)) return false; bool form = false; if (you_cannot_memorise(specspell, form)) { mpr(desc_cannot_memorise_reason(form).c_str()); return false; } if (you.has_spell(specspell)) { mpr("You already know that spell!"); return false; } if (you.spell_no >= MAX_KNOWN_SPELLS) { mpr("Your head is already too full of spells!"); return false; } if (you.experience_level < spell_difficulty(specspell)) { mpr("You're too inexperienced to learn that spell!"); return false; } if (player_spell_levels() < spell_levels_required(specspell)) { mpr("You can't memorise that many levels of magic yet!"); return false; } return true; } /** * Attempt to make the player learn the given spell. * * @param specspell The spell to be learned. * @return true if the player learned the spell, false * otherwise. */ bool learn_spell(spell_type specspell) { if (!_learn_spell_checks(specspell)) return false; double chance = get_miscast_chance(specspell); if (spell_fail(specspell) >= 100 && !vehumet_is_offering(specspell)) mprf(MSGCH_WARN, "This spell is impossible to cast!"); else if (chance >= 0.025) mprf(MSGCH_WARN, "This spell is very dangerous to cast!"); else if (chance >= 0.005) mprf(MSGCH_WARN, "This spell is quite dangerous to cast!"); else if (chance >= 0.001) mprf(MSGCH_WARN, "This spell is slightly dangerous to cast."); snprintf(info, INFO_SIZE, "Memorise %s, consuming %d spell level%s and leaving %d?", spell_title(specspell), spell_levels_required(specspell), spell_levels_required(specspell) != 1 ? "s" : "", player_spell_levels() - spell_levels_required(specspell)); // Deactivate choice from tile inventory. mouse_control mc(MOUSE_MODE_MORE); if (!yesno(info, true, 'n', false)) { canned_msg(MSG_OK); return false; } start_delay(DELAY_MEMORISE, spell_difficulty(specspell), specspell); you.turn_is_over = true; did_god_conduct(DID_SPELL_CASTING, 2 + random2(5)); return true; } bool forget_spell_from_book(spell_type spell, const item_def* book) { string prompt; prompt += make_stringf("Forgetting %s from %s will destroy the book! " "Are you sure?", spell_title(spell), book->name(DESC_THE).c_str()); // Deactivate choice from tile inventory. mouse_control mc(MOUSE_MODE_MORE); if (!yesno(prompt.c_str(), false, 'n')) { canned_msg(MSG_OK); return false; } mprf("As you tear out the page describing %s, the book crumbles to dust.", spell_title(spell)); if (del_spell_from_memory(spell)) { item_was_destroyed(*book); destroy_spellbook(*book); dec_inv_item_quantity(book->link, 1); you.turn_is_over = true; return true; } else { // This shouldn't happen. mprf("A bug prevents you from forgetting %s.", spell_title(spell)); return false; } } int count_rod_spells(const item_def &item, bool need_id) { if (item.base_type != OBJ_RODS) return -1; if (need_id && !item_type_known(item)) return 0; const int type = item.book_number(); if (type == -1) return 0; int nspel = 0; while (nspel < SPELLBOOK_SIZE && _is_valid_spell_in_book(item, nspel)) ++nspel; return nspel; } int rod_spell(int rod, bool check_range) { item_def& irod(you.inv[rod]); if (irod.base_type != OBJ_RODS) { canned_msg(MSG_NOTHING_HAPPENS); return -1; } // ID code got moved to item_use::wield_effects. {due} const int num_spells = count_rod_spells(irod, false); int keyin = 0; if (num_spells == 0) { canned_msg(MSG_NOTHING_HAPPENS); // shouldn't happen return 0; } else if (num_spells == 1) keyin = 'a'; // automatically selected if it's the only option else { mprf(MSGCH_PROMPT, "Evoke which spell from the rod ([a-%c] spell [?*] list)? ", 'a' + num_spells - 1); // Note that the list of spells is not presented here. keyin = get_ch(); if (keyin == '?' || keyin == '*') { keyin = read_book(you.inv[rod], RBOOK_USE_ROD); // [ds] read_book sets turn_is_over. you.turn_is_over = false; } } if (key_is_escape(keyin) || keyin == ' ' || keyin == '\r' || keyin == '\n') { canned_msg(MSG_OK); return -1; } if (!isaalpha(keyin)) { canned_msg(MSG_HUH); return -1; } const int idx = letter_to_index(keyin); if (idx >= SPELLBOOK_SIZE || !_is_valid_spell_in_book(irod, idx)) { canned_msg(MSG_HUH); return -1; } const spell_type spell = which_spell_in_book(irod, idx); int mana = spell_mana(spell) * ROD_CHARGE_MULT; int power = calc_spell_power(spell, false, false, true, true); int food = spell_hunger(spell, true); if (you.is_undead == US_UNDEAD) food = 0; if (food && (you.hunger_state == HS_STARVING || you.hunger <= food) && !you.is_undead) { canned_msg(MSG_NO_ENERGY); crawl_state.zero_turns_taken(); return -1; } if (spell == SPELL_THUNDERBOLT && you.props.exists("thunderbolt_last") && you.props["thunderbolt_last"].get_int() + 1 == you.num_turns) { // Starting it up takes 2 mana, continuing any amount up to 5. // You don't get to expend less (other than stopping the zap completely). mana = min(5 * ROD_CHARGE_MULT, (int)irod.plus); // Never allow using less than a whole point of charge. mana = max(mana, ROD_CHARGE_MULT); you.props["thunderbolt_mana"].get_int() = mana; } if (irod.plus < mana) { mpr("The rod doesn't have enough magic points."); crawl_state.zero_turns_taken(); // Don't lose a turn for trying to evoke without enough MP - that's // needlessly cruel for an honest error. return -1; } if (check_range && spell_no_hostile_in_range(spell, true)) { // Abort if there are no hostiles within range, but flash the range // markers for a short while. mpr("You can't see any susceptible monsters within range! " "(Use V to cast anyway.)"); if (Options.darken_beyond_range) { targetter_smite range(&you, calc_spell_range(spell, 0, true), 0, 0, true); range_view_annotator show_range(&range); delay(50); } crawl_state.zero_turns_taken(); return -1; } // All checks passed, we can cast the spell. if (you.confused()) random_uselessness(); else if (your_spells(spell, power, false) == SPRET_ABORT) { crawl_state.zero_turns_taken(); return -1; } make_hungry(food, true, true); irod.plus -= mana; you.wield_change = true; you.turn_is_over = true; return roll_dice(1, 1 + spell_difficulty(spell) / 2); } static bool _compare_spells(spell_type a, spell_type b) { if (a == SPELL_NO_SPELL && b == SPELL_NO_SPELL) return false; else if (a != SPELL_NO_SPELL && b == SPELL_NO_SPELL) return true; else if (a == SPELL_NO_SPELL && b != SPELL_NO_SPELL) return false; int level_a = spell_difficulty(a); int level_b = spell_difficulty(b); if (level_a != level_b) return level_a < level_b; unsigned int schools_a = get_spell_disciplines(a); unsigned int schools_b = get_spell_disciplines(b); if (schools_a != schools_b && schools_a != 0 && schools_b != 0) { const char* a_type = NULL; const char* b_type = NULL; // Find lowest/earliest school for each spell. for (int i = 0; i <= SPTYP_LAST_EXPONENT; i++) { int mask = 1 << i; if (a_type == NULL && (schools_a & mask)) a_type = spelltype_long_name(mask); if (b_type == NULL && (schools_b & mask)) b_type = spelltype_long_name(mask); } ASSERT(a_type != NULL); ASSERT(b_type != NULL); return strcmp(a_type, b_type) < 0; } return strcmp(spell_title(a), spell_title(b)) < 0; } bool is_memorised(spell_type spell) { for (int i = 0; i < MAX_KNOWN_SPELLS; i++) if (you.spells[i] == spell) return true; return false; } static void _get_spell_list(vector &spells, int level, unsigned int disc1, unsigned int disc2, god_type god, bool avoid_uncastable, int &god_discard, int &uncastable_discard, bool avoid_known = false) { // For randarts handed out by Sif Muna, spells contained in the // special books are fair game. // We store them in an extra vector that (once sorted) can later // be checked for each spell with a rarity -1 (i.e. not normally // appearing randomly). vector special_spells; if (god == GOD_SIF_MUNA) { for (int i = MIN_RARE_BOOK; i <= MAX_RARE_BOOK; ++i) for (int j = 0; j < SPELLBOOK_SIZE; ++j) { spell_type spell = which_spell_in_book(i, j); if (spell == SPELL_NO_SPELL) continue; if (spell_rarity(spell) != -1) continue; special_spells.push_back(spell); } sort(special_spells.begin(), special_spells.end()); } int specnum = 0; for (int i = 0; i < NUM_SPELLS; ++i) { const spell_type spell = (spell_type) i; if (!is_valid_spell(spell)) continue; // Only use spells available in books you might find lying about // the dungeon. if (spell_rarity(spell) == -1) { bool skip_spell = true; while ((unsigned int) specnum < special_spells.size() && spell == special_spells[specnum]) { specnum++; skip_spell = false; } if (skip_spell) continue; } if (avoid_known && you.seen_spell[spell]) continue; if (level != -1) { // fixed level randart: only include spells of the given level if (spell_difficulty(spell) != level) continue; } else { // themed randart: only include spells of the given disciplines const unsigned int disciplines = get_spell_disciplines(spell); if ((!(disciplines & disc1) && !(disciplines & disc2)) || disciplines_conflict(disc1, disciplines) || disciplines_conflict(disc2, disciplines)) { continue; } } if (avoid_uncastable && you_cannot_memorise(spell)) { uncastable_discard++; continue; } if (god_dislikes_spell_type(spell, god)) { god_discard++; continue; } // Passed all tests. spells.push_back(spell); } } static void _get_spell_list(vector &spells, unsigned int disc1, unsigned int disc2, god_type god, bool avoid_uncastable, int &god_discard, int &uncastable_discard, bool avoid_known = false) { _get_spell_list(spells, -1, disc1, disc2, god, avoid_uncastable, god_discard, uncastable_discard, avoid_known); } static void _get_spell_list(vector &spells, int level, god_type god, bool avoid_uncastable, int &god_discard, int &uncastable_discard, bool avoid_known = false) { _get_spell_list(spells, level, SPTYP_NONE, SPTYP_NONE, god, avoid_uncastable, god_discard, uncastable_discard, avoid_known); } static void _make_book_randart(item_def &book) { if (!is_artefact(book)) { book.flags |= ISFLAG_RANDART; if (!book.props.exists(ARTEFACT_APPEAR_KEY)) { book.props[ARTEFACT_APPEAR_KEY].get_string() = make_artefact_name(book, true); } } } bool make_book_level_randart(item_def &book, int level, int num_spells, string owner) { ASSERT(book.base_type == OBJ_BOOKS); god_type god; (void) origin_is_god_gift(book, &god); const bool completely_random = god == GOD_XOM || (god == GOD_NO_GOD && !origin_is_acquirement(book)); if (level == -1) { int max_level = (completely_random ? 9 : min(9, you.get_experience_level())); level = random_range(1, max_level); } ASSERT_RANGE(level, 0 + 1, 9 + 1); if (num_spells == -1) { // Book level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 // Number of spells: 5 | 5 | 5 | 6 | 6 | 6 | 4 | 2 | 1 num_spells = min(5 + (level - 1)/3, 18 - 2*level); num_spells = max(1, num_spells); } ASSERT_RANGE(num_spells, 0 + 1, SPELLBOOK_SIZE + 1); book.plus = level; book.plus2 = num_spells; book.sub_type = BOOK_RANDART_LEVEL; _make_book_randart(book); int god_discard = 0; int uncastable_discard = 0; vector spells; _get_spell_list(spells, level, god, !completely_random, god_discard, uncastable_discard); if (spells.empty()) { if (level > 1) return make_book_level_randart(book, level - 1); char buf[80]; if (god_discard > 0 && uncastable_discard == 0) { snprintf(buf, sizeof(buf), "%s disliked all level %d spells", god_name(god).c_str(), level); } else if (god_discard == 0 && uncastable_discard > 0) sprintf(buf, "No level %d spells can be cast by you", level); else if (god_discard > 0 && uncastable_discard > 0) { snprintf(buf, sizeof(buf), "All level %d spells are either disliked by %s " "or cannot be cast by you.", level, god_name(god).c_str()); } else sprintf(buf, "No level %d spells?!?!?!", level); mprf(MSGCH_ERROR, "Could not create fixed level randart spellbook: %s", buf); return false; } shuffle_array(spells); if (num_spells > (int) spells.size()) { num_spells = spells.size(); #if defined(DEBUG) || defined(DEBUG_DIAGNOSTICS) mprf(MSGCH_WARN, "More spells requested for fixed level (%d) " "randart spellbook than there are valid spells.", level); mprf(MSGCH_WARN, "Discarded %d spells due to being uncastable and " "%d spells due to being disliked by %s.", uncastable_discard, god_discard, god_name(god).c_str()); #endif } vector spell_used(spells.size(), false); vector avoid_memorised(spells.size(), !completely_random); vector avoid_seen(spells.size(), !completely_random); spell_type chosen_spells[SPELLBOOK_SIZE]; for (int i = 0; i < SPELLBOOK_SIZE; i++) chosen_spells[i] = SPELL_NO_SPELL; int book_pos = 0; while (book_pos < num_spells) { int spell_pos = random2(spells.size()); if (spell_used[spell_pos]) continue; spell_type spell = spells[spell_pos]; ASSERT(spell != SPELL_NO_SPELL); if (avoid_memorised[spell_pos] && is_memorised(spell)) { // Only once. avoid_memorised[spell_pos] = false; continue; } if (avoid_seen[spell_pos] && you.seen_spell[spell] && coinflip()) { // Only once. avoid_seen[spell_pos] = false; continue; } spell_used[spell_pos] = true; chosen_spells[book_pos++] = spell; } sort(chosen_spells, chosen_spells + SPELLBOOK_SIZE, _compare_spells); ASSERT(chosen_spells[0] != SPELL_NO_SPELL); CrawlHashTable &props = book.props; props.erase(SPELL_LIST_KEY); props[SPELL_LIST_KEY].new_vector(SV_INT).resize(SPELLBOOK_SIZE); CrawlVector &spell_vec = props[SPELL_LIST_KEY].get_vector(); spell_vec.set_max_size(SPELLBOOK_SIZE); for (int i = 0; i < SPELLBOOK_SIZE; i++) spell_vec[i].get_int() = chosen_spells[i]; bool has_owner = true; string name = ""; if (!owner.empty()) name = owner; else if (god != GOD_NO_GOD) name = god_name(god, false); else if (one_chance_in(30)) name = god_name(GOD_SIF_MUNA, false); else if (one_chance_in(3)) name = make_name(random_int(), false); else has_owner = false; if (has_owner) name = apostrophise(name) + " "; // None of these books need a definite article prepended. book.props["is_named"].get_bool() = true; string bookname; if (god == GOD_XOM && coinflip()) { bookname = getRandNameString("book_noun") + " of " + getRandNameString("Xom_book_title"); bookname = replace_name_parts(bookname, book); } else { string lookup; if (level == 1) lookup = "starting"; else if (level <= 3 || level == 4 && coinflip()) lookup = "easy"; else if (level <= 6) lookup = "moderate"; else lookup = "difficult"; lookup += " level book"; // First try for names respecting the book's previous owner/author // (if one exists), then check for general difficulty. if (has_owner) bookname = getRandNameString(lookup + " owner"); if (!has_owner || bookname.empty()) bookname = getRandNameString(lookup); bookname = uppercase_first(bookname); if (has_owner) { if (bookname.substr(0, 4) == "The ") bookname = bookname.substr(4); else if (bookname.substr(0, 2) == "A ") bookname = bookname.substr(2); else if (bookname.substr(0, 3) == "An ") bookname = bookname.substr(3); } if (bookname.find("@level@", 0) != string::npos) { string number; switch (level) { case 1: number = "One"; break; case 2: number = "Two"; break; case 3: number = "Three"; break; case 4: number = "Four"; break; case 5: number = "Five"; break; case 6: number = "Six"; break; case 7: number = "Seven"; break; case 8: number = "Eight"; break; case 9: number = "Nine"; break; default: number = ""; break; } bookname = replace_all(bookname, "@level@", number); } } if (bookname.empty()) bookname = getRandNameString("book"); name += bookname; set_artefact_name(book, name); return true; } static bool _get_weighted_discs(bool completely_random, god_type god, int &disc1, int &disc2) { // Eliminate disciplines that the god dislikes or from which all // spells are discarded. vector ok_discs; vector skills; vector spellcount; for (int i = 0; i <= SPTYP_LAST_EXPONENT; i++) { int disc = 1 << i; if (disc & SPTYP_DIVINATION) continue; if (god_dislikes_spell_discipline(disc, god)) continue; int junk1 = 0, junk2 = 0; vector spells; _get_spell_list(spells, disc, disc, god, !completely_random, junk1, junk2, !completely_random); if (spells.empty()) continue; ok_discs.push_back(disc); skills.push_back(spell_type2skill(disc)); spellcount.push_back(spells.size()); } int num_discs = ok_discs.size(); if (num_discs == 0) { #ifdef DEBUG mprf(MSGCH_ERROR, "No valid disciplines with which to make a themed " "randart spellbook."); #endif // Only happens if !completely_random and the player already knows // all available spells. We could simply re-allow all disciplines // but the player isn't going to get any new spells, anyway, so just // consider this acquirement failed. (jpeg) return false; } int skill_weights[SPTYP_LAST_EXPONENT + 1]; memset(skill_weights, 0, (SPTYP_LAST_EXPONENT + 1) * sizeof(int)); if (!completely_random) { int total_skills = 0; for (int i = 0; i < num_discs; i++) { skill_type skill = skills[i]; int weight = 2 * you.skills[skill] + 1; if (spellcount[i] < 3) weight *= spellcount[i]/3; skill_weights[i] = max(0, weight); total_skills += skill_weights[i]; } if (total_skills == 0) completely_random = true; } if (completely_random) { for (int i = 0; i < num_discs; i++) skill_weights[i] = 1; } do { disc1 = ok_discs[choose_random_weighted(skill_weights, skill_weights + num_discs)]; disc2 = ok_discs[choose_random_weighted(skill_weights, skill_weights + num_discs)]; } while (disciplines_conflict(disc1, disc2)); return true; } static bool _get_weighted_spells(bool completely_random, god_type god, int disc1, int disc2, int num_spells, int max_levels, const vector &spells, spell_type chosen_spells[]) { ASSERT(num_spells <= (int) spells.size()); ASSERT(num_spells <= SPELLBOOK_SIZE); ASSERT(num_spells > 0); ASSERT(max_levels > 0); int spell_weights[NUM_SPELLS]; memset(spell_weights, 0, NUM_SPELLS * sizeof(int)); if (completely_random) { for (unsigned int i = 0; i < spells.size(); i++) { spell_type spl = spells[i]; if (god == GOD_XOM) spell_weights[spl] = count_bits(get_spell_disciplines(spl)); else spell_weights[spl] = 1; } } else { const int Spc = you.skills[SK_SPELLCASTING]; for (unsigned int i = 0; i < spells.size(); i++) { spell_type spell = spells[i]; unsigned int disciplines = get_spell_disciplines(spell); int d = 1; if ((disciplines & disc1) && (disciplines & disc2)) d = 2; int c = 1; if (!you.seen_spell[spell]) c = 4; else if (!is_memorised(spell)) c = 2; int total_skill = 0; int num_skills = 0; for (int j = 0; j <= SPTYP_LAST_EXPONENT; j++) { int disc = 1 << j; if (disciplines & disc) { total_skill += you.skills[spell_type2skill(disc)]; num_skills++; } } int w = 1; if (num_skills > 0) w = (2 + (total_skill / num_skills)) / 3; w = max(1, w); int l = 5 - abs(3 * spell_difficulty(spell) - Spc) / 7; int weight = d * c * w * l; ASSERT(weight > 0); spell_weights[spell] = weight; } } int book_pos = 0; int spells_left = spells.size(); while (book_pos < num_spells && max_levels > 0 && spells_left > 0) { if (chosen_spells[book_pos] != SPELL_NO_SPELL) { spell_type spell = chosen_spells[book_pos]; spell_weights[spell] = 0; max_levels -= spell_difficulty(spell); spells_left--; book_pos++; continue; } spell_type spell = (spell_type) choose_random_weighted(spell_weights, spell_weights + NUM_SPELLS); ASSERT(is_valid_spell(spell)); ASSERT(spell_weights[spell] > 0); int levels = spell_difficulty(spell); if (levels > max_levels) { spell_weights[spell] = 0; spells_left--; continue; } chosen_spells[book_pos++] = spell; spell_weights[spell] = 0; max_levels -= levels; spells_left--; } ASSERT(max_levels >= 0); return book_pos > 0; } static void _remove_nondiscipline_spells(spell_type chosen_spells[], int d1, int d2, spell_type exclude = SPELL_NO_SPELL) { int replace = -1; for (int i = 0; i < SPELLBOOK_SIZE; i++) { if (chosen_spells[i] == SPELL_NO_SPELL) break; if (chosen_spells[i] == exclude) continue; // If a spell matches neither the first nor the second type // (that may be the same) remove it. if (!spell_typematch(chosen_spells[i], d1) && !spell_typematch(chosen_spells[i], d2)) { chosen_spells[i] = SPELL_NO_SPELL; if (replace == -1) replace = i; } else if (replace != -1) { chosen_spells[replace] = chosen_spells[i]; chosen_spells[i] = SPELL_NO_SPELL; replace++; } } } static void _add_included_spells(spell_type chosen_spells[SPELLBOOK_SIZE], vector incl_spells) { for (unsigned int i = 0; i < incl_spells.size(); ++i) { spell_type incl_spell = incl_spells[i]; if (incl_spell == SPELL_NO_SPELL) continue; for (int j = 0; j < SPELLBOOK_SIZE; ++j) { // Already included. if (chosen_spells[j] == incl_spell) break; if (chosen_spells[j] == SPELL_NO_SPELL) { // Add to spells. chosen_spells[j] = incl_spell; break; } } } } // Takes a book of any type, a spell discipline or two, the number of spells // (up to 8), the total spell levels of all spells, a spell that absolutely // has to be included, and the name of whomever the book should be named after. // With all that information the book is turned into a random artefact // containing random spells of the given disciplines (random if none set). bool make_book_theme_randart(item_def &book, int disc1, int disc2, int num_spells, int max_levels, spell_type incl_spell, string owner, string title) { vector spells; if (incl_spell != SPELL_NO_SPELL) spells.push_back(incl_spell); return make_book_theme_randart(book, spells, disc1, disc2, num_spells, max_levels, owner, title); } bool make_book_theme_randart(item_def &book, vector incl_spells, int disc1, int disc2, int num_spells, int max_levels, string owner, string title) { ASSERT(book.base_type == OBJ_BOOKS); god_type god; (void) origin_is_god_gift(book, &god); const bool completely_random = god == GOD_XOM || (god == GOD_NO_GOD && !origin_is_acquirement(book)); if (num_spells == -1) num_spells = SPELLBOOK_SIZE; ASSERT_RANGE(num_spells, 0 + 1, SPELLBOOK_SIZE + 1); if (max_levels == -1) max_levels = 255; if (disc1 == 0 && disc2 == 0) { if (!_get_weighted_discs(completely_random, god, disc1, disc2)) { if (completely_random) return false; // Rather than give up at this point, choose schools randomly. // This way, an acquirement won't fail once the player has // seen all spells. if (!_get_weighted_discs(true, god, disc1, disc2)) return false; } } else if (disc2 == 0) disc2 = disc1; ASSERT(disc1 < (1 << (SPTYP_LAST_EXPONENT + 1))); ASSERT(disc2 < (1 << (SPTYP_LAST_EXPONENT + 1))); ASSERT(count_bits(disc1) == 1); ASSERT(count_bits(disc2) == 1); int disc1_pos = 0, disc2_pos = 0; for (int i = 0; i <= SPTYP_LAST_EXPONENT; i++) { if (disc1 & (1 << i)) disc1_pos = i; if (disc2 & (1 << i)) disc2_pos = i; } book.plus = num_spells | (max_levels << 8); book.plus2 = disc1_pos | (disc2_pos << 8); book.sub_type = BOOK_RANDART_THEME; _make_book_randart(book); int god_discard = 0; int uncastable_discard = 0; vector spells; _get_spell_list(spells, disc1, disc2, god, !completely_random, god_discard, uncastable_discard); if (num_spells > (int) spells.size()) num_spells = spells.size(); spell_type chosen_spells[SPELLBOOK_SIZE]; for (int i = 0; i < SPELLBOOK_SIZE; i++) chosen_spells[i] = SPELL_NO_SPELL; _add_included_spells(chosen_spells, incl_spells); // If max_levels is 1, there might not be any suitable spells (for // example, in Charms). Try one more time with max_levels = 2. while (!_get_weighted_spells(completely_random, god, disc1, disc2, num_spells, max_levels, spells, chosen_spells)) { if (max_levels != 1) die("_get_weighted_spells() failed"); ++max_levels; } sort(chosen_spells, chosen_spells + SPELLBOOK_SIZE, _compare_spells); ASSERT(chosen_spells[0] != SPELL_NO_SPELL); CrawlHashTable &props = book.props; props.erase(SPELL_LIST_KEY); props[SPELL_LIST_KEY].new_vector(SV_INT).resize(SPELLBOOK_SIZE); CrawlVector &spell_vec = props[SPELL_LIST_KEY].get_vector(); spell_vec.set_max_size(SPELLBOOK_SIZE); // Count how often each spell school appears in the book. int count[SPTYP_LAST_EXPONENT+1]; for (int k = 0; k <= SPTYP_LAST_EXPONENT; k++) count[k] = 0; for (int i = 0; i < SPELLBOOK_SIZE; i++) { if (chosen_spells[i] == SPELL_NO_SPELL) continue; for (int k = 0; k <= SPTYP_LAST_EXPONENT; k++) if (spell_typematch(chosen_spells[i], 1 << k)) count[k]++; } // Remember the two dominant spell schools ... int max1 = 0; int max2 = 0; int num1 = 1; int num2 = 0; for (int k = 1; k <= SPTYP_LAST_EXPONENT; k++) { if (count[k] > count[max1]) { max2 = max1; num2 = num1; max1 = k; num1 = 1; } else { if (count[k] == count[max1]) num1++; if (max2 == max1 || count[k] > count[max2]) { max2 = k; if (count[k] == count[max1]) num2 = num1; else num2 = 1; } else if (count[k] == count[max2]) num2++; } } // If there are several "secondary" disciplines with the same count // ignore all of them. Same, if the secondary discipline appears only once. if (num2 > 1 && count[max1] > count[max2] || count[max2] < 2) max2 = max1; // Remove spells that don't fit either discipline. _remove_nondiscipline_spells(chosen_spells, 1 << max1, 1 << max2); _add_included_spells(chosen_spells, incl_spells); // Resort spells. if (!incl_spells.empty()) sort(chosen_spells, chosen_spells + SPELLBOOK_SIZE, _compare_spells); ASSERT(chosen_spells[0] != SPELL_NO_SPELL); // ... and change disc1 and disc2 accordingly. disc1 = 1 << max1; if (max1 == max2) disc2 = disc1; else disc2 = 1 << max2; int highest_level = 0; int lowest_level = 10; bool all_spells_disc1 = true; // Finally fill the spell vector. for (int i = 0; i < SPELLBOOK_SIZE; i++) { spell_vec[i].get_int() = chosen_spells[i]; int diff = spell_difficulty(chosen_spells[i]); if (diff > highest_level) highest_level = diff; else if (diff < lowest_level) lowest_level = diff; if (all_spells_disc1 && is_valid_spell(chosen_spells[i]) && !spell_typematch(chosen_spells[i], disc1)) { all_spells_disc1 = false; } } // Every spell in the book is of school disc1. if (disc1 == disc2) all_spells_disc1 = true; // If the owner hasn't been set already use // a) the god's name for god gifts (only applies to Sif Muna and Xom), // b) a name depending on the spell disciplines, for pure books // c) a random name (all god gifts not named earlier) // d) an applicable god's name // ... else leave it unnamed (around 57% chance for non-god gifts) if (owner.empty()) { const bool god_gift = (god != GOD_NO_GOD); if (god_gift && !one_chance_in(4)) owner = god_name(god, false); else if (god_gift && one_chance_in(3) || one_chance_in(5)) { bool highlevel = (highest_level >= 7 + random2(3) && (lowest_level > 1 || coinflip())); if (disc1 != disc2) { string schools[2]; schools[0] = spelltype_long_name(disc1); schools[1] = spelltype_long_name(disc2); sort(schools, schools + 2); string lookup = schools[0] + " " + schools[1]; if (highlevel) owner = getRandNameString("highlevel " + lookup + " owner"); if (owner.empty() || owner == "__NONE") owner = getRandNameString(lookup + " owner"); if (owner == "__NONE") owner = ""; } if (owner.empty() && all_spells_disc1) { string lookup = spelltype_long_name(disc1); if (highlevel && disc1 == disc2) owner = getRandNameString("highlevel " + lookup + " owner"); if (owner.empty() || owner == "__NONE") owner = getRandNameString(lookup + " owner"); if (owner == "__NONE") owner = ""; } } if (owner.empty()) { if (god_gift || one_chance_in(5)) // Use a random name. owner = make_name(random_int(), false); else if (!god_gift && one_chance_in(9)) { god = GOD_SIF_MUNA; switch (disc1) { case SPTYP_NECROMANCY: if (all_spells_disc1 && !one_chance_in(6)) god = GOD_KIKUBAAQUDGHA; break; case SPTYP_CONJURATION: if (all_spells_disc1 && !one_chance_in(4)) god = GOD_VEHUMET; break; default: break; } owner = god_name(god, false); } } } string name = ""; if (!owner.empty()) { name = apostrophise(owner) + " "; book.props["is_named"].get_bool() = true; } else book.props["is_named"].get_bool() = false; string bookname = ""; if (!title.empty()) bookname = title; else { // Sometimes use a completely random title. if (owner == "Xom" && !one_chance_in(20)) bookname = getRandNameString("Xom_book_title"); else if (one_chance_in(20) && (owner.empty() || one_chance_in(3))) bookname = getRandNameString("random_book_title"); bookname = replace_name_parts(bookname, book); } if (!bookname.empty()) name += getRandNameString("book_noun") + " of " + bookname; else { // Give a name that reflects the primary and secondary // spell disciplines of the spells contained in the book. name += getRandNameString("book_name") + " "; // For the actual name there's a 66% chance of getting something like // of the Fiery Traveller (Translocation/Fire), else // of Displacement and Flames. string type_name; if (disc1 != disc2 && !one_chance_in(3)) { string lookup = spelltype_long_name(disc2); type_name = getRandNameString(lookup + " adj"); } if (type_name.empty()) { // No adjective found, use the normal method of combining two nouns. type_name = getRandNameString(spelltype_long_name(disc1)); if (type_name.empty()) name += spelltype_long_name(disc1); else name += type_name; if (disc1 != disc2) { name += " and "; type_name = getRandNameString(spelltype_long_name(disc2)); if (type_name.empty()) name += spelltype_long_name(disc2); else name += type_name; } } else { bookname = type_name + " "; // Add the noun for the first discipline. type_name = getRandNameString(spelltype_long_name(disc1)); if (type_name.empty()) bookname += spelltype_long_name(disc1); else { if (type_name.find("the ", 0) != string::npos) { type_name = replace_all(type_name, "the ", ""); bookname = "the " + bookname; } bookname += type_name; } name += bookname; } } set_artefact_name(book, name); // Save primary/secondary disciplines back into the book. book.plus = max1; book.plus2 = max2; return true; } // Give Roxanne a randart spellbook of the disciplines Transmutations/Earth // that includes Statue Form and is named after her. void make_book_Roxanne_special(item_def *book) { int disc = coinflip() ? SPTYP_TRANSMUTATION : SPTYP_EARTH; make_book_theme_randart(*book, disc, 0, 5, 19, SPELL_STATUE_FORM, "Roxanne"); } void make_book_Kiku_gift(item_def &book, bool first) { book.sub_type = BOOK_RANDART_THEME; _make_book_randart(book); spell_type chosen_spells[SPELLBOOK_SIZE]; for (int i = 0; i < SPELLBOOK_SIZE; i++) chosen_spells[i] = SPELL_NO_SPELL; if (first) { bool can_bleed = you.species != SP_GARGOYLE && you.species != SP_GHOUL && you.species != SP_MUMMY; bool can_regen = you.species != SP_DEEP_DWARF && you.species != SP_MUMMY; bool pain = coinflip(); chosen_spells[0] = pain ? SPELL_PAIN : SPELL_ANIMATE_SKELETON; chosen_spells[1] = SPELL_CORPSE_ROT; chosen_spells[2] = (can_bleed ? SPELL_SUBLIMATION_OF_BLOOD : pain ? SPELL_ANIMATE_SKELETON : SPELL_PAIN); chosen_spells[3] = (!can_regen || coinflip()) ? SPELL_VAMPIRIC_DRAINING : SPELL_REGENERATION; chosen_spells[4] = SPELL_CONTROL_UNDEAD; } else { chosen_spells[0] = coinflip() ? SPELL_ANIMATE_DEAD : SPELL_TWISTED_RESURRECTION; chosen_spells[1] = (you.species == SP_FELID || coinflip()) ? SPELL_AGONY : SPELL_EXCRUCIATING_WOUNDS; chosen_spells[2] = random_choose(SPELL_BOLT_OF_DRAINING, SPELL_SIMULACRUM, SPELL_DEATH_CHANNEL, -1); spell_type extra_spell; do { extra_spell = random_choose(SPELL_ANIMATE_DEAD, SPELL_TWISTED_RESURRECTION, SPELL_AGONY, SPELL_EXCRUCIATING_WOUNDS, SPELL_BOLT_OF_DRAINING, SPELL_SIMULACRUM, SPELL_DEATH_CHANNEL, -1); if (you.species == SP_FELID && extra_spell == SPELL_EXCRUCIATING_WOUNDS) extra_spell = SPELL_NO_SPELL; for (int i = 0; i < 3; i++) if (extra_spell == chosen_spells[i]) extra_spell = SPELL_NO_SPELL; } while (extra_spell == SPELL_NO_SPELL); chosen_spells[3] = extra_spell; chosen_spells[4] = SPELL_DISPEL_UNDEAD; } sort(chosen_spells, chosen_spells + SPELLBOOK_SIZE, _compare_spells); CrawlHashTable &props = book.props; props.erase(SPELL_LIST_KEY); props[SPELL_LIST_KEY].new_vector(SV_INT).resize(SPELLBOOK_SIZE); CrawlVector &spell_vec = props[SPELL_LIST_KEY].get_vector(); spell_vec.set_max_size(SPELLBOOK_SIZE); for (int i = 0; i < SPELLBOOK_SIZE; i++) spell_vec[i].get_int() = chosen_spells[i]; string name = "Kikubaaqudgha's "; book.props["is_named"].get_bool() = true; name += getRandNameString("book_name") + " "; string type_name = getRandNameString("Necromancy"); if (type_name.empty()) name += "Necromancy"; else name += type_name; set_artefact_name(book, name); } bool book_has_title(const item_def &book) { ASSERT(book.base_type == OBJ_BOOKS); if (!is_artefact(book)) return false; return book.props.exists("is_named") && book.props["is_named"].get_bool() == true; } void destroy_spellbook(const item_def &book) { int j, maxlevel = 0; for (j = 0; j < SPELLBOOK_SIZE; j++) { spell_type stype = which_spell_in_book(book, j); if (stype == SPELL_NO_SPELL) continue; maxlevel = max(maxlevel, spell_difficulty(stype)); } did_god_conduct(DID_DESTROY_SPELLBOOK, maxlevel + 5); }