/** * @file * @brief data handlers for player-available spell list **/ #include "AppHdr.h" #include "spl-util.h" #include #include #include #include #include #include #include "externs.h" #include "areas.h" #include "beam.h" #include "coord.h" #include "coordit.h" #include "directn.h" #include "godabil.h" #include "env.h" #include "items.h" #include "libutil.h" #include "message.h" #include "mon-behv.h" #include "mon-util.h" #include "notes.h" #include "options.h" #include "output.h" #include "player.h" #include "prompt.h" #include "religion.h" #include "spl-cast.h" #include "spl-book.h" #include "spl-damage.h" #include "spl-zap.h" #include "strings.h" #include "target.h" #include "terrain.h" #include "transform.h" struct spell_desc { int id; const char *title; unsigned int disciplines; // bitfield unsigned int flags; // bitfield unsigned int level; // Usually in the range 0..200 (0 means uncapped). // Note that some spells are also capped through zap_type. // See spell_power_cap below. int power_cap; // At power 0, you get min_range. At power power_cap, you get max_range. int min_range; int max_range; // Modify spell level for spell noise purposes. int noise_mod; const char *target_prompt; // If a monster is casting this, does it need a tracer? bool ms_needs_tracer; // The spell can be used no matter what the monster's foe is. bool ms_utility; }; #include "spl-data.h" static int spell_list[NUM_SPELLS]; #define SPELLDATASIZE (sizeof(spelldata)/sizeof(struct spell_desc)) static const struct spell_desc *_seekspell(spell_type spellid); // // BEGIN PUBLIC FUNCTIONS // // All this does is merely refresh the internal spell list {dlb}: void init_spell_descs() { for (int i = 0; i < NUM_SPELLS; i++) spell_list[i] = -1; for (unsigned int i = 0; i < SPELLDATASIZE; i++) { const spell_desc &data = spelldata[i]; ASSERTM(data.id >= SPELL_NO_SPELL && data.id < NUM_SPELLS, "spell #%d has invalid id %d", i, data.id); ASSERTM(data.title != NULL && *data.title, "spell #%d, id %d has no name", i, data.id); ASSERTM(data.level >= 1 && data.level <= 9, "spell '%s' has invalid level %d", data.title, data.level); ASSERTM(data.min_range <= data.max_range, "spell '%s' has min_range larger than max_range", data.title); ASSERTM(!(data.flags & SPFLAG_TARGETING_MASK) || (data.min_range >= 0 && data.max_range > 0), "targeted/directed spell '%s' has invalid range", data.title); spell_list[data.id] = i; } } typedef map spell_name_map; static spell_name_map spell_name_cache; void init_spell_name_cache() { for (int i = 0; i < NUM_SPELLS; i++) { spell_type type = static_cast(i); if (!is_valid_spell(type)) continue; const char *sptitle = spell_title(type); ASSERT(sptitle); const string spell_name = lowercase_string(sptitle); spell_name_cache[spell_name] = type; } } spell_type spell_by_name(string name, bool partial_match) { if (name.empty()) return SPELL_NO_SPELL; lowercase(name); if (!partial_match) { spell_name_map::iterator i = spell_name_cache.find(name); if (i != spell_name_cache.end()) return i->second; return SPELL_NO_SPELL; } const spell_type sp = find_earliest_match(name, SPELL_NO_SPELL, NUM_SPELLS, is_valid_spell, spell_title); return sp == NUM_SPELLS ? SPELL_NO_SPELL : sp; } spschool_flag_type school_by_name(string name) { spschool_flag_type short_match, long_match; int short_matches, long_matches; short_match = long_match = SPTYP_NONE; short_matches = long_matches = 0; lowercase(name); for (int i = 0; i <= SPTYP_RANDOM; i++) { spschool_flag_type type = (spschool_flag_type) (1 << i); string short_name = spelltype_short_name(type); string long_name = spelltype_long_name(type); lowercase(short_name); lowercase(long_name); if (name == short_name) return type; if (name == long_name) return type; if (short_name.find(name) != string::npos) { short_match = type; short_matches++; } if (long_name.find(name) != string::npos) { long_match = type; long_matches++; } } if (short_matches != 1 && long_matches != 1) return SPTYP_NONE; if (short_matches == 1 && long_matches != 1) return short_match; if (short_matches != 1 && long_matches == 1) return long_match; if (short_match == long_match) return short_match; return SPTYP_NONE; } int get_spell_slot_by_letter(char letter) { ASSERT(isaalpha(letter)); const int index = letter_to_index(letter); if (you.spell_letter_table[ index ] == -1) return -1; return you.spell_letter_table[index]; } static int _get_spell_slot(spell_type spell) { for (int i = 0; i < MAX_KNOWN_SPELLS; i++) if (you.spells[i] == spell) return i; return -1; } int get_spell_letter(spell_type spell) { return index_to_letter(_get_spell_slot(spell)); } spell_type get_spell_by_letter(char letter) { ASSERT(isaalpha(letter)); const int slot = get_spell_slot_by_letter(letter); return (slot == -1) ? SPELL_NO_SPELL : you.spells[slot]; } bool add_spell_to_memory(spell_type spell) { int i; int j = -1; string sname = spell_title(spell); lowercase(sname); // first we find a slot in our head: for (i = 0; i < MAX_KNOWN_SPELLS; i++) { if (you.spells[i] == SPELL_NO_SPELL) break; } you.spells[i] = spell; // now we find an available label: // first check to see whether we've chosen an automatic label: for (unsigned k = 0; k < Options.auto_spell_letters.size(); ++k) { if (!Options.auto_spell_letters[k].first.matches(sname)) continue; for (unsigned l = 0; l < Options.auto_spell_letters[k].second.length(); ++l) if (isaalpha(Options.auto_spell_letters[k].second[l]) && you.spell_letter_table[letter_to_index(Options.auto_spell_letters[k].second[l])] == -1) { j = letter_to_index(Options.auto_spell_letters[k].second[l]); break; } if (j != -1) break; } // If we didn't find a label above, choose the first available one. if (j == -1) for (j = 0; j < 52; j++) { if (you.spell_letter_table[j] == -1) break; } if (you.num_turns) mprf("Spell assigned to '%c'.", index_to_letter(j)); you.spell_letter_table[j] = i; you.spell_no++; take_note(Note(NOTE_LEARN_SPELL, spell)); spell_skills(spell, you.start_train); #ifdef USE_TILE_LOCAL tiles.layout_statcol(); redraw_screen(); #endif return true; } bool del_spell_from_memory_by_slot(int slot) { ASSERT_RANGE(slot, 0, MAX_KNOWN_SPELLS); int j; if (you.last_cast_spell == you.spells[slot]) you.last_cast_spell = SPELL_NO_SPELL; spell_skills(you.spells[slot], you.stop_train); mprf("Your memory of %s unravels.", spell_title(you.spells[slot])); you.spells[ slot ] = SPELL_NO_SPELL; for (j = 0; j < 52; j++) { if (you.spell_letter_table[j] == slot) you.spell_letter_table[j] = -1; } you.spell_no--; #ifdef USE_TILE_LOCAL tiles.layout_statcol(); redraw_screen(); #endif return true; } bool del_spell_from_memory(spell_type spell) { int i = _get_spell_slot(spell); if (i == -1) return false; else return del_spell_from_memory_by_slot(i); } int spell_hunger(spell_type which_spell, bool rod) { if (player_energy()) return 0; const int level = spell_difficulty(which_spell); const int basehunger[] = { 50, 95, 160, 250, 350, 550, 700, 850, 1000 }; int hunger; if (level < 10 && level > 0) hunger = basehunger[level-1]; else hunger = (basehunger[0] * level * level) / 4; if (rod) { hunger -= you.skill(SK_EVOCATIONS, 10); hunger = max(hunger, level * 5); } else hunger -= you.skill(SK_SPELLCASTING, you.intel()); if (hunger < 0) hunger = 0; return hunger; } // Used to determine whether or not a monster should always fire this spell // if selected. If not, we should use a tracer. // Note - this function assumes that the monster is "nearby" its target! bool spell_needs_tracer(spell_type spell) { return _seekspell(spell)->ms_needs_tracer; } // Checks if the spell is an explosion that can be placed anywhere even without // an unobstructed beam path, such as fire storm. bool spell_is_direct_explosion(spell_type spell) { return spell == SPELL_FIRE_STORM || spell == SPELL_HELLFIRE_BURST; } bool spell_needs_foe(spell_type spell) { return !_seekspell(spell)->ms_utility; } bool spell_harms_target(spell_type spell) { const unsigned int flags = _seekspell(spell)->flags; if (flags & (SPFLAG_HELPFUL | SPFLAG_NEUTRAL)) return false; if (flags & SPFLAG_TARGETING_MASK) return true; return false; } bool spell_harms_area(spell_type spell) { const unsigned int flags = _seekspell(spell)->flags; if (flags & (SPFLAG_HELPFUL | SPFLAG_NEUTRAL)) return false; if (flags & SPFLAG_AREA) return true; return false; } // applied to spell misfires (more power = worse) and triggers // for Xom acting (more power = more likely to grab his attention) {dlb} int spell_mana(spell_type which_spell) { return _seekspell(which_spell)->level; } // applied in naughties (more difficult = higher level knowledge = worse) // and triggers for Sif acting (same reasoning as above, just good) {dlb} int spell_difficulty(spell_type which_spell) { return _seekspell(which_spell)->level; } int spell_levels_required(spell_type which_spell) { int levels = spell_difficulty(which_spell); if (which_spell == SPELL_DELAYED_FIREBALL && you.has_spell(SPELL_FIREBALL)) { levels -= spell_difficulty(SPELL_FIREBALL); } else if (which_spell == SPELL_FIREBALL && you.has_spell(SPELL_DELAYED_FIREBALL)) { levels = 0; } return levels; } unsigned int get_spell_flags(spell_type which_spell) { return _seekspell(which_spell)->flags; } const char *get_spell_target_prompt(spell_type which_spell) { return _seekspell(which_spell)->target_prompt; } bool spell_typematch(spell_type which_spell, unsigned int which_discipline) { return get_spell_disciplines(which_spell) & which_discipline; } //jmf: next two for simple bit handling unsigned int get_spell_disciplines(spell_type spell) { return _seekspell(spell)->disciplines; } int count_bits(unsigned int bits) { unsigned int n; int c = 0; for (n = 1; n < INT_MAX; n <<= 1) if (n & bits) c++; return c; } // NOTE: Assumes that any single spell won't belong to conflicting // disciplines. bool disciplines_conflict(unsigned int disc1, unsigned int disc2) { const unsigned int combined = disc1 | disc2; return (combined & SPTYP_EARTH) && (combined & SPTYP_AIR) || (combined & SPTYP_FIRE) && (combined & SPTYP_ICE); } const char *spell_title(spell_type spell) { return _seekspell(spell)->title; } // FUNCTION APPLICATORS: Idea from Juho Snellman // on the Roguelike News pages, Development section. // // Here are some function applicators: sort of like brain-dead, // home-grown iterators for the container "dungeon". // Apply a function-pointer to all visible squares // Returns summation of return values from passed in function. int apply_area_visible(cell_func cf, int power, actor *agent) { int rv = 0; for (radius_iterator ri(agent->pos(), LOS_NO_TRANS); ri; ++ri) rv += cf(*ri, power, 0, agent); return rv; } // Applies the effect to all nine squares around/including the target. // Returns summation of return values from passed in function. static int _apply_area_square(cell_func cf, const coord_def& where, int power, actor *agent) { int rv = 0; for (adjacent_iterator ai(where, false); ai; ++ai) rv += cf(*ai, power, 0, agent); return rv; } // Applies the effect to the eight squares beside the target. // Returns summation of return values from passed in function. static int _apply_area_around_square(cell_func cf, const coord_def& where, int power, actor *agent) { int rv = 0; for (adjacent_iterator ai(where, true); ai; ++ai) rv += cf(*ai, power, 0, agent); return rv; } // Like apply_area_around_square, but for monsters in those squares, // and takes care not to affect monsters twice that change position. int apply_monsters_around_square(monster_func mf, const coord_def& where, int power) { int rv = 0; set affected; for (adjacent_iterator ai(where, true); ai; ++ai) { monster* mon = monster_at(*ai); if (mon && !affected.count(mon)) { rv += mf(mon, power); affected.insert(mon); } } return rv; } // Affect up to max_targs monsters around a point, chosen randomly. // Return varies with the function called; return values will be added up. int apply_random_around_square(cell_func cf, const coord_def& where, bool exclude_center, int power, int max_targs, actor *agent) { int rv = 0; if (max_targs <= 0) return 0; if (max_targs >= 9 && !exclude_center) return _apply_area_square(cf, where, power, agent); if (max_targs >= 8 && exclude_center) return _apply_area_around_square(cf, where, power, agent); coord_def targs[8]; int count = 0; for (adjacent_iterator ai(where, exclude_center); ai; ++ai) { if (monster_at(*ai) == NULL && *ai != you.pos()) continue; // Found target count++; // Slight difference here over the basic algorithm... // // For cases where the number of choices <= max_targs it's // obvious (all available choices will be selected). // // For choices > max_targs, here's a brief proof: // // Let m = max_targs, k = choices - max_targs, k > 0. // // Proof, by induction (over k): // // 1) Show n = m + 1 (k = 1) gives uniform distribution, // P(new one not chosen) = 1 / (m + 1). // m 1 1 // P(specific previous one replaced) = --- * --- = --- // m+1 m m+1 // // So the probablity is uniform (ie. any element has // a 1/(m+1) chance of being in the unchosen slot). // // 2) Assume the distribution is uniform at n = m+k. // (ie. the probablity that any of the found elements // was chosen = m / (m+k) (the slots are symmetric, // so it's the sum of the probabilities of being in // any of them)). // // 3) Show n = m + k + 1 gives a uniform distribution. // P(new one chosen) = m / (m + k + 1) // P(any specific previous choice remaining chosen) // = [1 - P(swaped into m+k+1 position)] * P(prev. chosen) // m 1 m // = [ 1 - ----- * --- ] * --- // m+k+1 m m+k // // m+k m m // = ----- * --- = ----- // m+k+1 m+k m+k+1 // // Therefore, it's uniform for n = m + k + 1. QED // // The important thing to note in calculating the last // probability is that the chosen elements have already // passed tests which verify that they *don't* belong // in slots m+1...m+k, so the only positions an already // chosen element can end up in are its original // position (in one of the chosen slots), or in the // new slot. // // The new item can, of course, be placed in any slot, // swapping the value there into the new slot... we // just don't care about the non-chosen slots enough // to store them, so it might look like the item // automatically takes the new slot when not chosen // (although, by symmetry all the non-chosen slots are // the same... and similarly, by symmetry, all chosen // slots are the same). // // Yes, that's a long comment for a short piece of // code, but I want people to have an understanding // of why this works (or at least make them wary about // changing it without proof and breaking this code). -- bwr // Accept the first max_targs choices, then when // new choices come up, replace one of the choices // at random, max_targs/count of the time (the rest // of the time it replaces an element in an unchosen // slot -- but we don't care about them). if (count <= max_targs) targs[count - 1] = *ai; else if (x_chance_in_y(max_targs, count)) { const int pick = random2(max_targs); targs[ pick ] = *ai; } } const int targs_found = min(count, max_targs); if (targs_found) { // Used to divide the power up among the targets here, but // it's probably better to allow the full power through and // balance the called function. -- bwr for (int i = 0; i < targs_found; i++) { ASSERT(!targs[i].origin()); rv += cf(targs[i], power, 0, agent); } } return rv; } void apply_area_cloud(cloud_func func, const coord_def& where, int pow, int number, cloud_type ctype, const actor *agent, int spread_rate, int colour, string name, string tile, int excl_rad) { if (number <= 0) return; targetter_cloud place(agent, GDM, number, number); if (!place.set_aim(where)) return; unsigned int dist = 0; while (number > 0) { while (place.queue[dist].empty()) if (++dist >= place.queue.size()) return; vector &q = place.queue[dist]; int el = random2(q.size()); coord_def c = q[el]; q[el] = q[q.size() - 1]; q.pop_back(); if (place.seen[c] <= 0 || cell_is_solid(c)) continue; func(c, pow, spread_rate, ctype, agent, colour, name, tile, excl_rad); number--; } } // Select a spell direction and fill dist and pbolt appropriately. // Return false if the user cancelled, true otherwise. // FIXME: this should accept a direction_chooser_args directly rather // than move the arguments into one. bool spell_direction(dist &spelld, bolt &pbolt, targeting_type restrict, targ_mode_type mode, int range, bool needs_path, bool may_target_monster, bool may_target_self, const char *target_prefix, const char* top_prompt, bool cancel_at_self, targetter *hitfunc, desc_filter get_desc_func) { if (range < 1) range = (pbolt.range < 1) ? you.current_vision : pbolt.range; direction_chooser_args args; args.restricts = restrict; args.mode = mode; args.range = range; args.just_looking = false; args.needs_path = needs_path; args.may_target_monster = may_target_monster; args.may_target_self = may_target_self; args.target_prefix = target_prefix; if (top_prompt) args.top_prompt = top_prompt; args.behaviour = NULL; args.cancel_at_self = cancel_at_self; args.hitfunc = hitfunc; args.get_desc_func = get_desc_func; direction(spelld, args); if (!spelld.isValid) { // Check for user cancel. canned_msg(MSG_OK); return false; } pbolt.set_target(spelld); pbolt.source = you.pos(); return true; } const char* spelltype_short_name(int which_spelltype) { switch (which_spelltype) { case SPTYP_CONJURATION: return "Conj"; case SPTYP_HEXES: return "Hex"; case SPTYP_CHARMS: return "Chrm"; case SPTYP_FIRE: return "Fire"; case SPTYP_ICE: return "Ice"; case SPTYP_TRANSMUTATION: return "Trmt"; case SPTYP_NECROMANCY: return "Necr"; case SPTYP_SUMMONING: return "Summ"; case SPTYP_DIVINATION: return "Divn"; case SPTYP_TRANSLOCATION: return "Tloc"; case SPTYP_POISON: return "Pois"; case SPTYP_EARTH: return "Erth"; case SPTYP_AIR: return "Air"; case SPTYP_RANDOM: return "Rndm"; default: return "Bug"; } } const char* spelltype_long_name(int which_spelltype) { switch (which_spelltype) { case SPTYP_CONJURATION: return "Conjuration"; case SPTYP_HEXES: return "Hexes"; case SPTYP_CHARMS: return "Charms"; case SPTYP_FIRE: return "Fire"; case SPTYP_ICE: return "Ice"; case SPTYP_TRANSMUTATION: return "Transmutation"; case SPTYP_NECROMANCY: return "Necromancy"; case SPTYP_SUMMONING: return "Summoning"; case SPTYP_DIVINATION: return "Divination"; case SPTYP_TRANSLOCATION: return "Translocation"; case SPTYP_POISON: return "Poison"; case SPTYP_EARTH: return "Earth"; case SPTYP_AIR: return "Air"; case SPTYP_RANDOM: return "Random"; default: return "Bug"; } } skill_type spell_type2skill(unsigned int spelltype) { switch (spelltype) { case SPTYP_CONJURATION: return SK_CONJURATIONS; case SPTYP_HEXES: return SK_HEXES; case SPTYP_CHARMS: return SK_CHARMS; case SPTYP_FIRE: return SK_FIRE_MAGIC; case SPTYP_ICE: return SK_ICE_MAGIC; case SPTYP_TRANSMUTATION: return SK_TRANSMUTATIONS; case SPTYP_NECROMANCY: return SK_NECROMANCY; case SPTYP_SUMMONING: return SK_SUMMONINGS; case SPTYP_TRANSLOCATION: return SK_TRANSLOCATIONS; case SPTYP_POISON: return SK_POISON_MAGIC; case SPTYP_EARTH: return SK_EARTH_MAGIC; case SPTYP_AIR: return SK_AIR_MAGIC; default: case SPTYP_DIVINATION: dprf("spell_type2skill: called with spelltype %u", spelltype); return SK_NONE; } } /* ************************************************** * * * END PUBLIC FUNCTIONS * * * ************************************************** */ //jmf: Simplified; moved init code to top function, init_spell_descs(). static const spell_desc *_seekspell(spell_type spell) { ASSERT_RANGE(spell, 0, NUM_SPELLS); const int index = spell_list[spell]; ASSERT(index != -1); return &spelldata[index]; } bool is_valid_spell(spell_type spell) { return spell > SPELL_NO_SPELL && spell < NUM_SPELLS && spell_list[spell] != -1; } static bool _spell_range_varies(spell_type spell) { int minrange = _seekspell(spell)->min_range; int maxrange = _seekspell(spell)->max_range; return minrange < maxrange; } int spell_power_cap(spell_type spell) { const int scap = _seekspell(spell)->power_cap; const int zcap = spell_zap_power_cap(spell); if (scap == 0) return zcap; else if (zcap == 0) return scap; else { // Two separate power caps; pre-zapping spell power // goes into range. if (scap <= zcap || _spell_range_varies(spell)) return scap; else return zcap; } } int spell_range(spell_type spell, int pow, bool player_spell) { int minrange = _seekspell(spell)->min_range; int maxrange = _seekspell(spell)->max_range; ASSERT(maxrange >= minrange); // spells with no range have maxrange == minrange == -1 if (maxrange < 0) return maxrange; // Sandblast is a special case. if (spell == SPELL_SANDBLAST && wielding_rocks()) { minrange++; maxrange++; } if (player_spell && vehumet_supports_spell(spell) && you_worship(GOD_VEHUMET) && spell != SPELL_STICKY_FLAME && spell != SPELL_FREEZE && spell != SPELL_DISCHARGE && spell != SPELL_GLACIATE && !player_under_penance() && you.piety >= piety_breakpoint(3)) { maxrange++; minrange++; } if (minrange == maxrange) return min(minrange, (int)you.current_vision); const int powercap = spell_power_cap(spell); if (powercap <= pow) return min(maxrange, (int)you.current_vision); // Round appropriately. return min((int)you.current_vision, (pow * (maxrange - minrange) + powercap / 2) / powercap + minrange); } /** * Spell casting noise. * * Returns the noise generated by the casting of a spell. The noise depends on * the spell schools and level. A modifier (noise_mod) can be applied to the * spell level. * @see spl-data.h * * Formula (use first match): * - Conjuration (noisy) = \f$ level \f$ * - Air and poison (quiet) = \f$ \frac{level}{2} \f$ * - Other (normal) = \f$ \frac{3 \times level}{4} \f$ * * \param spell The spell being casted. * \return The amount of noise generated. **/ int spell_noise(spell_type spell) { const spell_desc *desc = _seekspell(spell); unsigned int disciplines = desc->disciplines; int level = desc->level + desc->noise_mod; if (disciplines & SPTYP_CONJURATION) return level; else if (disciplines && !(disciplines & (SPTYP_POISON | SPTYP_AIR))) return div_round_up(level * 3, 4); else return div_round_up(level, 2); } spell_type zap_type_to_spell(zap_type zap) { switch (zap) { case ZAP_THROW_FLAME: return SPELL_THROW_FLAME; case ZAP_THROW_FROST: return SPELL_THROW_FROST; case ZAP_SLOW: return SPELL_SLOW; case ZAP_HASTE: return SPELL_HASTE; case ZAP_MAGIC_DART: return SPELL_MAGIC_DART; case ZAP_MAJOR_HEALING: return SPELL_MAJOR_HEALING; case ZAP_PARALYSE: return SPELL_PARALYSE; case ZAP_BOLT_OF_FIRE: return SPELL_BOLT_OF_FIRE; case ZAP_BOLT_OF_COLD: return SPELL_BOLT_OF_COLD; case ZAP_PRIMAL_WAVE: return SPELL_PRIMAL_WAVE; case ZAP_CONFUSE: return SPELL_CONFUSE; case ZAP_INVISIBILITY: return SPELL_INVISIBILITY; case ZAP_DIG: return SPELL_DIG; case ZAP_FIREBALL: return SPELL_FIREBALL; case ZAP_TELEPORT_OTHER: return SPELL_TELEPORT_OTHER; case ZAP_LIGHTNING_BOLT: return SPELL_LIGHTNING_BOLT; case ZAP_POLYMORPH: return SPELL_POLYMORPH; case ZAP_BOLT_OF_DRAINING: return SPELL_BOLT_OF_DRAINING; case ZAP_ENSLAVEMENT: return SPELL_ENSLAVEMENT; case ZAP_DISINTEGRATE: return SPELL_DISINTEGRATE; default: die("zap_type_to_spell() only handles wand zaps for now"); } return SPELL_NO_SPELL; } static bool _spell_is_empowered(spell_type spell) { switch (spell) { case SPELL_STONESKIN: if (you.duration[DUR_TRANSFORMATION] > 0 && you.form == TRAN_STATUE && !player_stoneskin()) { return true; } break; case SPELL_OZOCUBUS_ARMOUR: if (you.duration[DUR_TRANSFORMATION] > 0 && you.form == TRAN_ICE_BEAST && you.duration[DUR_ICY_ARMOUR] < 1) { return true; } break; default: break; } return false; } // This function attempts to determine if 'spell' is useless to // the player. if 'transient' is true, then it will include checks // for volatile or temporary states (such as status effects, mana, etc.) // // its notably used by 'spell_highlight_by_utility' bool spell_is_useless(spell_type spell, bool transient) { if (you_cannot_memorise(spell)) return true; if (transient) { if (you.duration[DUR_CONF] > 0 || !enough_mp(spell_mana(spell), true, false) || spell_no_hostile_in_range(spell)) { return true; } #if TAG_MAJOR_VERSION == 34 if (you.species == SP_LAVA_ORC && !temperature_effect(LORC_STONESKIN)) { switch (spell) { case SPELL_STATUE_FORM: // Stony self is too melty // Too hot for these ice spells: case SPELL_ICE_FORM: case SPELL_OZOCUBUS_ARMOUR: case SPELL_CONDENSATION_SHIELD: return true; default: break; } } #endif } switch (spell) { case SPELL_BLINK: case SPELL_CONTROLLED_BLINK: case SPELL_TELEPORT_SELF: if (you.no_tele(false, false, spell != SPELL_TELEPORT_SELF)) return true; break; case SPELL_SWIFTNESS: if (transient && you.is_stationary()) return true; // looking at player_movement_speed, this should be correct ~DMB if (player_movement_speed() <= 6) return true; break; case SPELL_FLY: if (transient && you.form == TRAN_TREE) return true; if (you.racial_permanent_flight()) return true; if (transient && you.permanent_flight()) return true; break; case SPELL_INVISIBILITY: if (transient && you.backlit()) return true; break; case SPELL_CONTROL_TELEPORT: // Can be cast in advance in most places with -cTele, // but useless once the orb is picked up. if (player_has_orb()) return true; break; case SPELL_DARKNESS: // mere corona is not enough, but divine light blocks it completely if (transient && you.haloed()) return true; if (you_worship(GOD_SHINING_ONE) && !player_under_penance()) return true; break; #if TAG_MAJOR_VERSION == 34 case SPELL_INSULATION: if (player_res_electricity(false, transient, transient)) return true; break; #endif case SPELL_REPEL_MISSILES: if (player_mutation_level(MUT_DISTORTION_FIELD) == 3 || you.scan_artefacts(ARTP_RMSL, true)) { return true; } break; #if TAG_MAJOR_VERSION == 34 case SPELL_STONESKIN: if (you.species == SP_LAVA_ORC) return true; break; #endif case SPELL_LEDAS_LIQUEFACTION: if (!you.stand_on_solid_ground() || you.duration[DUR_LIQUEFYING] || liquefied(you.pos())) { return true; } break; case SPELL_DELAYED_FIREBALL: return transient && you.attribute[ATTR_DELAYED_FIREBALL]; default: break; // quash unhandled constants warnings } return false; } /** * Determines what color a spell should be highlighted with. * * @param spell The type of spell to be colored. * @param default_color Color to be used if the spell is unremarkable. * @param transient If true, check if spell is temporarily useless. * @param rod_spell If the spell being evoked from a rod. * @return The color to highlight the spell. */ int spell_highlight_by_utility(spell_type spell, int default_color, bool transient, bool rod_spell) { // If your god hates the spell, that // overrides all other concerns if (god_hates_spell(spell, you.religion, rod_spell) || is_good_god(you.religion) && you.spellcasting_unholy()) { return COL_FORBIDDEN; } if (_spell_is_empowered(spell) && !rod_spell) default_color = COL_EMPOWERED; if (spell_is_useless(spell, transient)) default_color = COL_USELESS; return default_color; } bool spell_no_hostile_in_range(spell_type spell, bool rod) { int minRange = get_dist_to_nearest_monster(); if (minRange < 0) return false; const int range = calc_spell_range(spell, 0, rod); if (range < 0) return false; switch (spell) { // These don't target monsters. case SPELL_APPORTATION: case SPELL_CONJURE_FLAME: case SPELL_PASSWALL: case SPELL_GOLUBRIAS_PASSAGE: case SPELL_LRD: case SPELL_FULMINANT_PRISM: case SPELL_SUMMON_LIGHTNING_SPIRE: // Shock and Lightning Bolt are no longer here, as the code below can // account for possible bounces. case SPELL_FIRE_STORM: return false; // Special handling for cloud spells. case SPELL_FREEZING_CLOUD: case SPELL_POISONOUS_CLOUD: case SPELL_HOLY_BREATH: { targetter_cloud tgt(&you, range); // Accept monsters that are in clouds for the hostiles-in-range check // (not for actual targetting). tgt.avoid_clouds = false; for (radius_iterator ri(you.pos(), range, C_ROUND, LOS_NO_TRANS); ri; ++ri) { if (!tgt.valid_aim(*ri)) continue; tgt.set_aim(*ri); for (map::iterator it = tgt.seen.begin(); it != tgt.seen.end(); it++) { if (it->second == AFF_NO || it->second == AFF_TRACER) continue; // Checks here are from get_dist_to_nearest_monster(). const monster* mons = monster_at(it->first); if (mons && !mons->wont_attack() && (!mons_class_flag(mons->type, M_NO_EXP_GAIN) || (mons->type == MONS_BALLISTOMYCETE && mons->number != 0))) { return false; } } } return true; } default: break; } // The healing spells. if (testbits(get_spell_flags(spell), SPFLAG_HELPFUL)) return false; const bool neutral = testbits(get_spell_flags(spell), SPFLAG_NEUTRAL); bolt beam; beam.flavour = BEAM_VISUAL; beam.origin_spell = spell; zap_type zap = spell_to_zap(spell); if (spell == SPELL_FIREBALL) zap = ZAP_FIREBALL; else if (spell == SPELL_RANDOM_BOLT) // don't let it think that there are no susceptible monsters in range zap = ZAP_DEBUGGING_RAY; if (zap != NUM_ZAPS) { beam.thrower = KILL_YOU_MISSILE; zappy(zap, calc_spell_power(spell, true, false, true, rod), beam); } else if (spell == SPELL_MEPHITIC_CLOUD) { beam.flavour = BEAM_MEPHITIC; beam.ex_size = 1; beam.damage = dice_def(1, 1); // so that foe_info is populated beam.hit = 20; beam.thrower = KILL_YOU; beam.ench_power = calc_spell_power(spell, true, false, true, rod); beam.is_beam = false; beam.is_explosion = true; } if (beam.flavour != BEAM_VISUAL) { bolt tempbeam; bool found = false; beam.beam_source = MHITYOU; beam.range = range; beam.is_tracer = true; beam.is_targeting = true; beam.source = you.pos(); beam.dont_stop_player = true; beam.friend_info.dont_stop = true; beam.foe_info.dont_stop = true; beam.attitude = ATT_FRIENDLY; beam.can_see_invis = you.can_see_invisible(); #ifdef DEBUG_DIAGNOSTICS beam.quiet_debug = true; #endif for (radius_iterator ri(you.pos(), range, C_ROUND, LOS_DEFAULT); ri; ++ri) { tempbeam = beam; tempbeam.target = *ri; tempbeam.fire(); if (tempbeam.foe_info.count > 0 || neutral && tempbeam.friend_info.count > 0) { found = true; break; } } return !found; } const int rsq = range * range + 1; if (rsq < minRange) return true; return false; }