Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
8176 lines (6824 sloc) 225 KB
/**
* @file
* @brief Player related functions.
**/
#include "AppHdr.h"
#include "player.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include "ability.h"
#include "abyss.h"
#include "act-iter.h"
#include "areas.h"
#include "art-enum.h"
#include "attack.h"
#include "bloodspatter.h"
#include "branch.h"
#include "chardump.h"
#include "cloud.h"
#include "coordit.h"
#include "delay.h"
#include "dgn-overview.h"
#include "dgn-event.h"
#include "directn.h"
#include "english.h"
#include "env.h"
#include "errors.h"
#include "exercise.h"
#include "files.h"
#include "food.h"
#include "god-abil.h"
#include "god-conduct.h"
#include "god-passive.h"
#include "god-wrath.h"
#include "hints.h"
#include "hiscores.h"
#include "invent.h"
#include "item-prop.h"
#include "items.h"
#include "item-use.h"
#include "kills.h"
#include "libutil.h"
#include "macro.h"
#include "melee-attack.h"
#include "message.h"
#include "mon-place.h"
#include "mutation.h"
#include "nearby-danger.h"
#include "notes.h"
#include "output.h"
#include "player-equip.h"
#include "player-save-info.h"
#include "player-stats.h"
#include "potion.h"
#include "prompt.h"
#include "religion.h"
#include "shout.h"
#include "skills.h"
#include "species.h" // random_starting_species
#include "spl-damage.h"
#include "spl-selfench.h"
#include "spl-transloc.h"
#include "spl-util.h"
#include "sprint.h"
#include "stairs.h"
#include "stash.h"
#include "state.h"
#include "status.h"
#include "stepdown.h"
#include "stringutil.h"
#include "terrain.h"
#ifdef USE_TILE
#include "tilepick.h"
#include "tileview.h"
#endif
#include "transform.h"
#include "traps.h"
#include "travel.h"
#include "view.h"
#include "wizard-option-type.h"
#include "xom.h"
static void _moveto_maybe_repel_stairs()
{
const dungeon_feature_type new_grid = env.grid(you.pos());
const command_type stair_dir = feat_stair_direction(new_grid);
if (stair_dir == CMD_NO_CMD
|| new_grid == DNGN_ENTER_SHOP
|| !you.duration[DUR_REPEL_STAIRS_MOVE])
{
return;
}
int pct = you.duration[DUR_REPEL_STAIRS_CLIMB] ? 29 : 50;
// When the effect is still strong, the chance to actually catch
// a stair is smaller. (Assuming the duration starts out at 1000.)
const int dur = max(0, you.duration[DUR_REPEL_STAIRS_MOVE] - 700);
pct += dur/10;
if (x_chance_in_y(pct, 100))
{
const string stair_str = feature_description_at(you.pos(), false,
DESC_THE, false);
const string prep = feat_preposition(new_grid, true, &you);
if (slide_feature_over(you.pos()))
{
mprf("%s slides away as you move %s it!", stair_str.c_str(),
prep.c_str());
if (player_in_a_dangerous_place() && one_chance_in(5))
xom_is_stimulated(25);
}
}
}
bool check_moveto_cloud(const coord_def& p, const string &move_verb,
bool *prompted)
{
if (you.confused())
return true;
const cloud_type ctype = env.map_knowledge(p).cloud();
// Don't prompt if already in a cloud of the same type.
if (is_damaging_cloud(ctype, true, cloud_is_yours_at(p))
&& ctype != cloud_type_at(you.pos())
&& !crawl_state.disables[DIS_CONFIRMATIONS])
{
// Don't prompt for steam unless we're at uncomfortably low hp.
if (ctype == CLOUD_STEAM)
{
int threshold = 20;
if (player_res_steam() < 0)
threshold = threshold * 3 / 2;
threshold = threshold * you.time_taken / BASELINE_DELAY;
// Do prompt if we'd lose icemail, though.
if (you.hp > threshold && !you.has_mutation(MUT_ICEMAIL))
return true;
}
// Don't prompt for meph if we have clarity, unless at very low HP.
if (ctype == CLOUD_MEPHITIC && you.clarity(false)
&& you.hp > 2 * you.time_taken / BASELINE_DELAY)
{
return true;
}
if (prompted)
*prompted = true;
string prompt = make_stringf("Really %s into that cloud of %s?",
move_verb.c_str(),
cloud_type_name(ctype).c_str());
learned_something_new(HINT_CLOUD_WARNING);
if (!yesno(prompt.c_str(), false, 'n'))
{
canned_msg(MSG_OK);
return false;
}
}
return true;
}
bool check_moveto_trap(const coord_def& p, const string &move_verb,
bool *prompted)
{
// Boldly go into the unknown (for shadow step and other ranged move
// prompts)
if (env.map_knowledge(p).trap() == TRAP_UNASSIGNED)
return true;
// If there's no trap, let's go.
trap_def* trap = trap_at(p);
if (!trap)
return true;
if (trap->type == TRAP_ZOT && !trap->is_safe() && !crawl_state.disables[DIS_CONFIRMATIONS])
{
string msg = "Do you really want to %s into the Zot trap?";
string prompt = make_stringf(msg.c_str(), move_verb.c_str());
if (prompted)
*prompted = true;
if (!yes_or_no("%s", prompt.c_str()))
{
canned_msg(MSG_OK);
return false;
}
}
else if (!trap->is_safe() && !crawl_state.disables[DIS_CONFIRMATIONS])
{
string prompt;
if (prompted)
*prompted = true;
prompt = make_stringf("Really %s %s that %s?", move_verb.c_str(),
(trap->type == TRAP_ALARM
|| trap->type == TRAP_PLATE) ? "onto"
: "into",
feature_description_at(p, false, DESC_BASENAME,
false).c_str());
if (!yesno(prompt.c_str(), true, 'n'))
{
canned_msg(MSG_OK);
return false;
}
}
return true;
}
static bool _check_moveto_dangerous(const coord_def& p, const string& msg)
{
if (you.can_swim() && feat_is_water(env.grid(p))
|| you.airborne() || !is_feat_dangerous(env.grid(p)))
{
return true;
}
if (!msg.empty())
mpr(msg);
else if (species_likes_water(you.species) && feat_is_water(env.grid(p)))
mpr("You cannot enter water in your current form.");
else
canned_msg(MSG_UNTHINKING_ACT);
return false;
}
bool check_moveto_terrain(const coord_def& p, const string &move_verb,
const string &msg, bool *prompted)
{
// Boldly go into the unknown (for shadow step and other ranged move
// prompts)
if (!env.map_knowledge(p).known())
return true;
if (!_check_moveto_dangerous(p, msg))
return false;
if (!need_expiration_warning() && need_expiration_warning(p)
&& !crawl_state.disables[DIS_CONFIRMATIONS])
{
string prompt;
if (prompted)
*prompted = true;
if (!msg.empty())
prompt = msg + " ";
prompt += "Are you sure you want to " + move_verb;
if (you.ground_level())
prompt += " into ";
else
prompt += " over ";
prompt += env.grid(p) == DNGN_DEEP_WATER ? "deep water" : "lava";
prompt += need_expiration_warning(DUR_FLIGHT, p)
? " while you are losing your buoyancy?"
: " while your transformation is expiring?";
if (!yesno(prompt.c_str(), false, 'n'))
{
canned_msg(MSG_OK);
return false;
}
}
return true;
}
bool check_moveto_exclusions(const vector<coord_def> &areas,
const string &move_verb,
bool *prompted)
{
if (is_excluded(you.pos()) || crawl_state.disables[DIS_CONFIRMATIONS])
return true;
int count = 0;
for (auto p : areas)
{
if (is_excluded(p) && !is_stair_exclusion(p))
count++;
}
if (count == 0)
return true;
const string prompt = make_stringf((count == (int) areas.size() ?
"Really %s into a travel-excluded area?" :
"You might %s into a travel-excluded area, are you sure?"),
move_verb.c_str());
if (prompted)
*prompted = true;
if (!yesno(prompt.c_str(), false, 'n'))
{
canned_msg(MSG_OK);
return false;
}
return true;
}
bool check_moveto_exclusion(const coord_def& p, const string &move_verb,
bool *prompted)
{
return check_moveto_exclusions({p}, move_verb, prompted);
}
bool check_moveto(const coord_def& p, const string &move_verb, const string &msg)
{
return check_moveto_terrain(p, move_verb, msg)
&& check_moveto_cloud(p, move_verb)
&& check_moveto_trap(p, move_verb)
&& check_moveto_exclusion(p, move_verb);
}
// Returns true if this is a valid swap for this monster. If true, then
// the valid location is set in loc. (Otherwise loc becomes garbage.)
bool swap_check(monster* mons, coord_def &loc, bool quiet)
{
loc = you.pos();
if (you.is_stationary())
return false;
// Don't move onto dangerous terrain.
if (is_feat_dangerous(grd(mons->pos())))
{
canned_msg(MSG_UNTHINKING_ACT);
return false;
}
if (mons_is_projectile(*mons))
{
if (!quiet)
mpr("It's unwise to walk into this.");
return false;
}
if (mons->caught())
{
if (!quiet)
{
simple_monster_message(*mons,
make_stringf(" is %s!", held_status(mons)).c_str());
}
return false;
}
if (mons->is_constricted())
{
if (!quiet)
simple_monster_message(*mons, " is being constricted!");
return false;
}
if (mons->is_stationary() || mons->asleep() || mons->cannot_move())
{
if (!quiet)
simple_monster_message(*mons, " cannot move out of your way!");
return false;
}
// prompt when swapping into known zot traps
if (!quiet && trap_at(loc) && trap_at(loc)->type == TRAP_ZOT
&& !yes_or_no("Do you really want to swap %s into the Zot trap?",
mons->name(DESC_YOUR).c_str()))
{
return false;
}
// First try: move monster onto your position.
bool swap = !monster_at(loc) && monster_habitable_grid(mons, grd(loc));
if (monster_at(loc)
&& monster_at(loc)->type == MONS_TOADSTOOL
&& mons->type == MONS_WANDERING_MUSHROOM)
{
swap = monster_habitable_grid(mons, grd(loc));
}
// Choose an appropriate habitat square at random around the target.
if (!swap)
{
int num_found = 0;
for (adjacent_iterator ai(mons->pos()); ai; ++ai)
if (!monster_at(*ai) && monster_habitable_grid(mons, grd(*ai))
&& one_chance_in(++num_found))
{
loc = *ai;
}
if (num_found)
swap = true;
}
if (!swap && !quiet)
{
// Might not be ideal, but it's better than insta-killing
// the monster... maybe try for a short blink instead? - bwr
simple_monster_message(*mons, " cannot make way for you.");
// FIXME: activity_interrupt::hit_monster isn't ideal.
interrupt_activity(activity_interrupt::hit_monster, mons);
}
return swap;
}
static void _splash()
{
if (you.can_swim())
noisy(4, you.pos(), "Floosh!");
else if (!you.can_water_walk())
noisy(8, you.pos(), "Splash!");
}
void moveto_location_effects(dungeon_feature_type old_feat,
bool stepped, const coord_def& old_pos)
{
const dungeon_feature_type new_grid = env.grid(you.pos());
// Terrain effects.
if (is_feat_dangerous(new_grid))
fall_into_a_pool(new_grid);
// called after fall_into_a_pool, in case of emergency untransform
if (you.species == SP_MERFOLK)
merfolk_check_swimming(stepped);
if (you.ground_level())
{
if (feat_is_water(new_grid))
{
if (!stepped)
_splash();
if (!you.can_swim() && !you.can_water_walk())
{
if (!feat_is_water(old_feat))
{
mprf("You %s the %s water.",
stepped ? "enter" : "fall into",
new_grid == DNGN_SHALLOW_WATER ? "shallow" : "deep");
}
if (new_grid == DNGN_DEEP_WATER && old_feat != DNGN_DEEP_WATER)
mpr("You sink to the bottom.");
if (!feat_is_water(old_feat))
{
mpr("Moving in this stuff is going to be slow.");
if (you.invisible())
mpr("...and don't expect to remain undetected.");
}
}
if (you.species == SP_OCTOPODE
&& !feat_is_water(old_feat)
&& you.invisible())
{
mpr("Don't expect to remain undetected while in the water.");
}
}
else if (you.props.exists(TEMP_WATERWALK_KEY))
you.props.erase(TEMP_WATERWALK_KEY);
}
id_floor_items();
// Traps go off.
// (But not when losing flight - i.e., moving into the same tile)
trap_def* ptrap = trap_at(you.pos());
if (ptrap && old_pos != you.pos())
ptrap->trigger(you);
if (stepped)
_moveto_maybe_repel_stairs();
}
// Use this function whenever the player enters (or lands and thus re-enters)
// a grid.
//
// stepped - normal walking moves
void move_player_to_grid(const coord_def& p, bool stepped)
{
ASSERT(!crawl_state.game_is_arena());
ASSERT_IN_BOUNDS(p);
if (!stepped)
tornado_move(p);
// assuming that entering the same square means coming from above (flight)
const coord_def old_pos = you.pos();
const bool from_above = (old_pos == p);
const dungeon_feature_type old_grid =
(from_above) ? DNGN_FLOOR : grd(old_pos);
// Really must be clear.
ASSERT(you.can_pass_through_feat(grd(p)));
// Better not be an unsubmerged monster either.
ASSERT(!monster_at(p) || monster_at(p)->submerged()
|| fedhas_passthrough(monster_at(p))
|| mons_is_player_shadow(*monster_at(p)));
// Move the player to new location.
you.moveto(p, true);
viewwindow();
moveto_location_effects(old_grid, stepped, old_pos);
}
/**
* Check if the given terrain feature is safe for the player to move into.
* (Or, at least, not instantly lethal.)
*
* @param grid The type of terrain feature under consideration.
* @param permanently Whether to disregard temporary effects (non-permanent
* flight, forms, etc)
* @param ignore_flight Whether to ignore all forms of flight (including
* permanent flight)
* @return Whether the terrain is safe.
*/
bool is_feat_dangerous(dungeon_feature_type grid, bool permanently,
bool ignore_flight)
{
if (!ignore_flight
&& (you.permanent_flight() || you.airborne() && !permanently))
{
return false;
}
else if (grid == DNGN_DEEP_WATER && !player_likes_water(permanently)
|| grid == DNGN_LAVA)
{
return true;
}
else
return false;
}
bool is_map_persistent()
{
return !testbits(your_branch().branch_flags, brflag::no_map)
|| env.properties.exists(FORCE_MAPPABLE_KEY);
}
bool player_in_hell(bool vestibule)
{
return vestibule ? is_hell_branch(you.where_are_you) :
is_hell_subbranch(you.where_are_you);
}
/**
* Is the player in the slightly-special version of the abyss that AKs start
* in?
*/
bool player_in_starting_abyss()
{
return you.chapter == CHAPTER_POCKET_ABYSS
&& player_in_branch(BRANCH_ABYSS) && you.depth <= 1;
}
bool player_in_connected_branch()
{
return is_connected_branch(you.where_are_you);
}
bool player_likes_water(bool permanently)
{
return !permanently && you.can_water_walk()
|| (species_likes_water(you.species) || !permanently)
&& form_likes_water();
}
/**
* Is the player considered to be closely related, if not the same species, to
* the given monster? (See mon-data.h for species/genus info.)
*
* @param mon The type of monster to be compared.
* @return Whether the player's species is related to the one given.
*/
bool is_player_same_genus(const monster_type mon)
{
return mons_genus(mon) == mons_genus(player_mons(false));
}
void update_player_symbol()
{
you.symbol = Options.show_player_species ? player_mons() : transform_mons();
}
monster_type player_mons(bool transform)
{
monster_type mons;
if (transform)
{
mons = transform_mons();
if (mons != MONS_PLAYER)
return mons;
}
mons = player_species_to_mons_species(you.species);
if (mons == MONS_ORC)
{
if (you_worship(GOD_BEOGH))
{
mons = (you.piety >= piety_breakpoint(4)) ? MONS_ORC_HIGH_PRIEST
: MONS_ORC_PRIEST;
}
}
else if (mons == MONS_OGRE)
{
const skill_type sk = best_skill(SK_FIRST_SKILL, SK_LAST_SKILL);
if (sk >= SK_SPELLCASTING && sk <= SK_LAST_MAGIC)
mons = MONS_OGRE_MAGE;
}
return mons;
}
void update_vision_range()
{
you.normal_vision = LOS_DEFAULT_RANGE;
int nom = 1;
int denom = 1;
// Barachi have +1 base LOS.
if (you.species == SP_BARACHI)
you.normal_vision += 1;
// Nightstalker gives -1/-2/-3.
if (you.get_mutation_level(MUT_NIGHTSTALKER))
{
nom *= you.normal_vision - you.get_mutation_level(MUT_NIGHTSTALKER);
denom *= you.normal_vision;
}
// the Darkness spell.
if (you.duration[DUR_DARKNESS])
nom *= 3, denom *= 4;
// robe of Night.
if (player_equip_unrand(UNRAND_NIGHT))
nom *= 3, denom *= 4;
you.current_vision = (you.normal_vision * nom + denom / 2) / denom;
ASSERT(you.current_vision > 0);
set_los_radius(you.current_vision);
}
/**
* Ignoring form & most equipment, but not the UNRAND_FINGER_AMULET, can the
* player use (usually wear) a given equipment slot?
*
* @param eq The slot in question.
* @param temp Whether to consider forms.
* @return MB_FALSE if the player can never use the slot;
* MB_MAYBE if the player can only use some items for the slot;
* MB_TRUE if the player can use any (fsvo any) item for the slot.
*/
maybe_bool you_can_wear(equipment_type eq, bool temp)
{
if (temp && !get_form()->slot_available(eq))
return MB_FALSE;
switch (eq)
{
case EQ_LEFT_RING:
if (you.get_mutation_level(MUT_MISSING_HAND))
return MB_FALSE;
// intentional fallthrough
case EQ_RIGHT_RING:
return you.species != SP_OCTOPODE ? MB_TRUE : MB_FALSE;
case EQ_RING_EIGHT:
if (you.get_mutation_level(MUT_MISSING_HAND))
return MB_FALSE;
// intentional fallthrough
case EQ_RING_ONE:
case EQ_RING_TWO:
case EQ_RING_THREE:
case EQ_RING_FOUR:
case EQ_RING_FIVE:
case EQ_RING_SIX:
case EQ_RING_SEVEN:
return you.species == SP_OCTOPODE ? MB_TRUE : MB_FALSE;
case EQ_WEAPON:
case EQ_STAFF:
return you.species == SP_FELID ? MB_FALSE :
you.body_size(PSIZE_TORSO, !temp) < SIZE_MEDIUM ? MB_MAYBE :
MB_TRUE;
// You can always wear at least one ring (forms were already handled).
case EQ_RINGS:
case EQ_ALL_ARMOUR:
case EQ_AMULET:
return MB_TRUE;
case EQ_RING_AMULET:
return player_equip_unrand(UNRAND_FINGER_AMULET) ? MB_TRUE : MB_FALSE;
default:
break;
}
item_def dummy, alternate;
dummy.base_type = alternate.base_type = OBJ_ARMOUR;
dummy.sub_type = alternate.sub_type = NUM_ARMOURS;
// Make sure can_wear_armour doesn't think it's Lear's.
dummy.unrand_idx = alternate.unrand_idx = 0;
switch (eq)
{
case EQ_CLOAK:
dummy.sub_type = ARM_CLOAK;
alternate.sub_type = ARM_SCARF;
break;
case EQ_GLOVES:
dummy.sub_type = ARM_GLOVES;
break;
case EQ_BOOTS: // And bardings
dummy.sub_type = ARM_BOOTS;
if (you.species == SP_NAGA)
alternate.sub_type = ARM_NAGA_BARDING;
if (you.species == SP_CENTAUR)
alternate.sub_type = ARM_CENTAUR_BARDING;
break;
case EQ_BODY_ARMOUR:
// Assume that anything that can wear any armour at all can wear a robe
// and that anything that can wear CPA can wear all armour.
dummy.sub_type = ARM_CRYSTAL_PLATE_ARMOUR;
alternate.sub_type = ARM_ROBE;
break;
case EQ_SHIELD:
// No races right now that can wear ARM_LARGE_SHIELD but not ARM_SHIELD
dummy.sub_type = ARM_LARGE_SHIELD;
if (you.body_size(PSIZE_TORSO, !temp) < SIZE_MEDIUM)
alternate.sub_type = ARM_BUCKLER;
break;
case EQ_HELMET:
dummy.sub_type = ARM_HELMET;
alternate.sub_type = ARM_HAT;
break;
default:
die("unhandled equipment type %d", eq);
break;
}
ASSERT(dummy.sub_type != NUM_ARMOURS);
if (can_wear_armour(dummy, false, !temp))
return MB_TRUE;
else if (alternate.sub_type != NUM_ARMOURS
&& can_wear_armour(alternate, false, !temp))
{
return MB_MAYBE;
}
else
return MB_FALSE;
}
bool player_has_feet(bool temp, bool include_mutations)
{
if (you.species == SP_NAGA
|| you.species == SP_FELID
|| you.species == SP_OCTOPODE
|| you.fishtail && temp)
{
return false;
}
if (include_mutations &&
(you.get_mutation_level(MUT_HOOVES, temp) == 3
|| you.get_mutation_level(MUT_TALONS, temp) == 3))
{
return false;
}
return true;
}
// Returns false if the player is wielding a weapon inappropriate for Berserk.
bool berserk_check_wielded_weapon()
{
const item_def * const wpn = you.weapon();
bool penance = false;
if (wpn && wpn->defined()
&& (!is_melee_weapon(*wpn)
|| needs_handle_warning(*wpn, OPER_ATTACK, penance)))
{
string prompt = "Do you really want to go berserk while wielding "
+ wpn->name(DESC_YOUR) + "?";
if (penance)
prompt += " This could place you under penance!";
if (!yesno(prompt.c_str(), true, 'n'))
{
canned_msg(MSG_OK);
return false;
}
}
return true;
}
// Looks in equipment "slot" to see if there is an equipped "sub_type".
// Returns number of matches (in the case of rings, both are checked)
int player::wearing(equipment_type slot, int sub_type, bool calc_unid) const
{
int ret = 0;
const item_def* item;
switch (slot)
{
case EQ_WEAPON:
// Hands can have more than just weapons.
if (weapon() && weapon()->is_type(OBJ_WEAPONS, sub_type))
ret++;
break;
case EQ_STAFF:
// Like above, but must be magical staff.
if (weapon()
&& weapon()->is_type(OBJ_STAVES, sub_type)
&& (calc_unid || item_type_known(*weapon())))
{
ret++;
}
break;
case EQ_AMULET:
case EQ_AMULET_PLUS:
if ((item = slot_item(static_cast<equipment_type>(EQ_AMULET)))
&& item->sub_type == sub_type
&& (calc_unid
|| item_type_known(*item)))
{
ret += (slot == EQ_AMULET_PLUS ? item->plus : 1);
}
break;
case EQ_RINGS:
case EQ_RINGS_PLUS:
for (int slots = EQ_FIRST_JEWELLERY; slots <= EQ_LAST_JEWELLERY; slots++)
{
if (slots == EQ_AMULET)
continue;
if ((item = slot_item(static_cast<equipment_type>(slots)))
&& item->sub_type == sub_type
&& (calc_unid
|| item_type_known(*item)))
{
ret += (slot == EQ_RINGS_PLUS ? item->plus : 1);
}
}
break;
case EQ_ALL_ARMOUR:
// Doesn't make much sense here... be specific. -- bwr
die("EQ_ALL_ARMOUR is not a proper slot");
break;
default:
if (! (slot >= EQ_FIRST_EQUIP && slot < NUM_EQUIP))
die("invalid slot");
if ((item = slot_item(slot))
&& item->sub_type == sub_type
&& (calc_unid || item_type_known(*item)))
{
ret++;
}
break;
}
return ret;
}
// Looks in equipment "slot" to see if equipped item has "special" ego-type
// Returns number of matches (jewellery returns zero -- no ego type).
// [ds] There's no equivalent of calc_unid or req_id because as of now, weapons
// and armour type-id on wield/wear.
int player::wearing_ego(equipment_type slot, int special, bool calc_unid) const
{
int ret = 0;
const item_def* item;
switch (slot)
{
case EQ_WEAPON:
// Hands can have more than just weapons.
if ((item = slot_item(EQ_WEAPON))
&& item->base_type == OBJ_WEAPONS
&& get_weapon_brand(*item) == special)
{
ret++;
}
break;
case EQ_LEFT_RING:
case EQ_RIGHT_RING:
case EQ_AMULET:
case EQ_STAFF:
case EQ_RINGS:
case EQ_RINGS_PLUS:
// no ego types for these slots
break;
case EQ_ALL_ARMOUR:
// Check all armour slots:
for (int i = EQ_MIN_ARMOUR; i <= EQ_MAX_ARMOUR; i++)
{
if ((item = slot_item(static_cast<equipment_type>(i)))
&& get_armour_ego_type(*item) == special
&& (calc_unid || item_type_known(*item)))
{
ret++;
}
}
break;
default:
if (slot < EQ_MIN_ARMOUR || slot > EQ_MAX_ARMOUR)
die("invalid slot: %d", slot);
// Check a specific armour slot for an ego type:
if ((item = slot_item(static_cast<equipment_type>(slot)))
&& get_armour_ego_type(*item) == special
&& (calc_unid || item_type_known(*item)))
{
ret++;
}
break;
}
return ret;
}
// Returns true if the indicated unrandart is equipped
// [ds] There's no equivalent of calc_unid or req_id because as of now, weapons
// and armour type-id on wield/wear.
bool player_equip_unrand(int unrand_index)
{
const unrandart_entry* entry = get_unrand_entry(unrand_index);
equipment_type slot = get_item_slot(entry->base_type,
entry->sub_type);
item_def* item;
switch (slot)
{
case EQ_WEAPON:
// Hands can have more than just weapons.
if ((item = you.slot_item(slot))
&& item->base_type == OBJ_WEAPONS
&& is_unrandom_artefact(*item)
&& item->unrand_idx == unrand_index)
{
return true;
}
break;
case EQ_RINGS:
for (int slots = EQ_FIRST_JEWELLERY; slots <= EQ_LAST_JEWELLERY; ++slots)
{
if (slots == EQ_AMULET)
continue;
if ((item = you.slot_item(static_cast<equipment_type>(slots)))
&& is_unrandom_artefact(*item)
&& item->unrand_idx == unrand_index)
{
return true;
}
}
break;
case EQ_NONE:
case EQ_STAFF:
case EQ_LEFT_RING:
case EQ_RIGHT_RING:
case EQ_RINGS_PLUS:
case EQ_ALL_ARMOUR:
// no unrandarts for these slots.
break;
default:
if (slot <= EQ_NONE || slot >= NUM_EQUIP)
die("invalid slot: %d", slot);
// Check a specific slot.
if ((item = you.slot_item(slot))
&& is_unrandom_artefact(*item)
&& item->unrand_idx == unrand_index)
{
return true;
}
break;
}
return false;
}
bool player_can_hear(const coord_def& p, int hear_distance)
{
return !silenced(p)
&& !silenced(you.pos())
&& you.pos().distance_from(p) <= hear_distance;
}
int player_teleport(bool calc_unid)
{
ASSERT(!crawl_state.game_is_arena());
// Don't allow any form of teleportation in Sprint or Gauntlets.
if (crawl_state.game_is_sprint() || player_in_branch(BRANCH_GAUNTLET))
return 0;
// Short-circuit rings of teleport to prevent spam.
if (you.species == SP_FORMICID)
return 0;
int tp = 0;
// rings (keep in sync with _equip_jewellery_effect)
tp += 8 * you.wearing(EQ_RINGS, RING_TELEPORTATION, calc_unid);
// artefacts
tp += 8 * you.scan_artefacts(ARTP_CAUSE_TELEPORTATION, calc_unid);
// mutations
tp += you.get_mutation_level(MUT_TELEPORT) * 4;
return tp;
}
// Computes bonuses to regeneration from most sources. Does not handle
// slow regeneration, vampireness, or Trog's Hand.
static int _player_bonus_regen()
{
int rr = 0;
// Trog's Hand is handled separately so that it will bypass slow
// regeneration, and it overrides the spell.
if (you.duration[DUR_REGENERATION]
&& !you.duration[DUR_TROGS_HAND])
{
rr += 100;
}
// Jewellery.
if (you.props[REGEN_AMULET_ACTIVE].get_int() == 1)
rr += REGEN_PIP * you.wearing(EQ_AMULET, AMU_REGENERATION);
// Artefacts
rr += REGEN_PIP * you.scan_artefacts(ARTP_REGENERATION);
// Troll leather
if (you.wearing(EQ_BODY_ARMOUR, ARM_TROLL_LEATHER_ARMOUR))
rr += REGEN_PIP;
// Fast heal mutation.
rr += you.get_mutation_level(MUT_REGENERATION) * REGEN_PIP;
// Powered By Death mutation, boosts regen by variable strength
// if the duration of the effect is still active.
if (you.duration[DUR_POWERED_BY_DEATH])
rr += you.props[POWERED_BY_DEATH_KEY].get_int() * 100;
return rr;
}
// Inhibited regeneration: stops regeneration when monsters are visible
bool regeneration_is_inhibited()
{
if (you.get_mutation_level(MUT_INHIBITED_REGENERATION) == 1
|| (you.species == SP_VAMPIRE && !you.vampire_alive))
{
for (monster_near_iterator mi(you.pos(), LOS_NO_TRANS); mi; ++mi)
{
if (mons_is_threatening(**mi)
&& !mi->wont_attack()
&& !mi->neutral()
&& !mi->submerged())
{
return true;
}
}
}
return false;
}
int player_regen()
{
// Note: if some condition can set rr = 0, can't be rested off, and
// would allow travel, please update is_sufficiently_rested.
int rr = you.hp_max / 3;
if (rr > 20)
rr = 20 + ((rr - 20) / 2);
// Add in miscellaneous bonuses
rr += _player_bonus_regen();
// Before applying other effects, make sure that there's something
// to heal.
rr = max(1, rr);
// Bonus regeneration for alive vampires.
if (you.species == SP_VAMPIRE && you.vampire_alive)
rr += 20;
if (you.duration[DUR_COLLAPSE])
rr /= 4;
if (you.disease || regeneration_is_inhibited() || !player_regenerates_hp())
rr = 0;
// Trog's Hand. This circumvents sickness or inhibited regeneration.
if (you.duration[DUR_TROGS_HAND])
rr += 100;
return rr;
}
int player_mp_regen()
{
int regen_amount = 7 + you.max_magic_points / 2;
if (you.get_mutation_level(MUT_MANA_REGENERATION))
regen_amount *= 2;
if (you.props[MANA_REGEN_AMULET_ACTIVE].get_int() == 1)
regen_amount += 25;
return regen_amount;
}
// Some amulets need to be worn while at full health before they begin to
// function.
void update_amulet_attunement_by_health()
{
// amulet of regeneration
// You must be wearing the amulet and able to regenerate to get benefits.
if (you.wearing(EQ_AMULET, AMU_REGENERATION)
&& you.get_mutation_level(MUT_NO_REGENERATION) == 0)
{
// If you hit max HP, turn on the amulet.
if (you.hp == you.hp_max
&& you.props[REGEN_AMULET_ACTIVE].get_int() == 0)
{
you.props[REGEN_AMULET_ACTIVE] = 1;
mpr("Your amulet attunes itself to your body and you begin to "
"regenerate more quickly.");
}
}
else
you.props[REGEN_AMULET_ACTIVE] = 0;
// amulet of the acrobat
if (you.wearing(EQ_AMULET, AMU_ACROBAT))
{
if (you.hp == you.hp_max
&& you.props[ACROBAT_AMULET_ACTIVE].get_int() == 0)
{
you.props[ACROBAT_AMULET_ACTIVE] = 1;
mpr("Your amulet attunes itself to your body. You feel like "
"doing cartwheels.");
}
}
else
you.props[ACROBAT_AMULET_ACTIVE] = 0;
}
// Amulet of magic regeneration needs to be worn while at full magic before it
// begins to function.
void update_mana_regen_amulet_attunement()
{
if (you.wearing(EQ_AMULET, AMU_MANA_REGENERATION)
&& player_regenerates_mp())
{
if (you.magic_points == you.max_magic_points
&& you.props[MANA_REGEN_AMULET_ACTIVE].get_int() == 0)
{
you.props[MANA_REGEN_AMULET_ACTIVE] = 1;
mpr("Your amulet attunes itself to your body and you begin to "
"regenerate magic more quickly.");
}
}
else
you.props[MANA_REGEN_AMULET_ACTIVE] = 0;
}
int player_hunger_rate(bool temp)
{
int hunger = 3;
if (you.species == SP_TROLL)
hunger += 3; // in addition to the +3 for fast metabolism
if (temp
&& (you.duration[DUR_REGENERATION]
|| you.duration[DUR_TROGS_HAND])
&& you.hp < you.hp_max)
{
hunger += 4;
}
hunger += you.get_mutation_level(MUT_FAST_METABOLISM)
- you.get_mutation_level(MUT_SLOW_METABOLISM);
// If Cheibriados has slowed your life processes, you will hunger less.
if (have_passive(passive_t::slow_metabolism))
hunger /= 2;
if (hunger < 1)
hunger = 1;
return hunger;
}
/**
* How many spell levels does the player have total, including those used up
* by memorized spells?
*/
int player_total_spell_levels()
{
return you.experience_level - 1 + you.skill(SK_SPELLCASTING, 2, true);
}
/**
* How many spell levels does the player currently have available for
* memorizing new spells?
*/
int player_spell_levels()
{
int sl = min(player_total_spell_levels(), 99);
#if TAG_MAJOR_VERSION == 34
bool fireball = false;
bool delayed_fireball = false;
#endif
for (const spell_type spell : you.spells)
{
#if TAG_MAJOR_VERSION == 34
if (spell == SPELL_FIREBALL)
fireball = true;
else if (spell == SPELL_DELAYED_FIREBALL)
delayed_fireball = true;
#endif
if (spell != SPELL_NO_SPELL)
sl -= spell_difficulty(spell);
}
#if TAG_MAJOR_VERSION == 34
// Fireball is free for characters with delayed fireball
if (fireball && delayed_fireball)
sl += spell_difficulty(SPELL_FIREBALL);
#endif
// Note: This can happen because of draining. -- bwr
if (sl < 0)
sl = 0;
return sl;
}
bool player_likes_chunks(bool permanently)
{
return you.gourmand(true, !permanently)
|| you.get_mutation_level(MUT_CARNIVOROUS) > 0;
}
// If temp is set to false, temporary sources or resistance won't be counted.
int player_res_fire(bool calc_unid, bool temp, bool items)
{
int rf = 0;
if (items)
{
// rings of fire resistance/fire
rf += you.wearing(EQ_RINGS, RING_PROTECTION_FROM_FIRE, calc_unid);
rf += you.wearing(EQ_RINGS, RING_FIRE, calc_unid);
// rings of ice
rf -= you.wearing(EQ_RINGS, RING_ICE, calc_unid);
// Staves
rf += you.wearing(EQ_STAFF, STAFF_FIRE, calc_unid);
// body armour:
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
rf += armour_type_prop(body_armour->sub_type, ARMF_RES_FIRE);
// ego armours
rf += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_FIRE_RESISTANCE);
rf += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_RESISTANCE);
// randart weapons:
rf += you.scan_artefacts(ARTP_FIRE, calc_unid);
// dragonskin cloak: 0.5 to draconic resistances
if (calc_unid && player_equip_unrand(UNRAND_DRAGONSKIN)
&& coinflip())
{
rf++;
}
}
// species:
if (you.species == SP_MUMMY)
rf--;
// mutations:
rf += you.get_mutation_level(MUT_HEAT_RESISTANCE, temp);
rf -= you.get_mutation_level(MUT_HEAT_VULNERABILITY, temp);
rf -= you.get_mutation_level(MUT_TEMPERATURE_SENSITIVITY, temp);
rf += you.get_mutation_level(MUT_MOLTEN_SCALES, temp) == 3 ? 1 : 0;
// spells:
if (temp)
{
if (you.duration[DUR_RESISTANCE])
rf++;
if (you.duration[DUR_FIRE_SHIELD])
rf += 2;
if (you.duration[DUR_QAZLAL_FIRE_RES])
rf++;
rf += get_form()->res_fire();
}
if (rf > 3)
rf = 3;
if (temp && you.duration[DUR_FIRE_VULN])
rf--;
if (rf < -3)
rf = -3;
return rf;
}
int player_res_steam(bool calc_unid, bool temp, bool items)
{
int res = 0;
const int rf = player_res_fire(calc_unid, temp, items);
if (you.species == SP_PALE_DRACONIAN)
res += 2;
if (items)
{
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
res += armour_type_prop(body_armour->sub_type, ARMF_RES_STEAM) * 2;
}
res += rf * 2;
if (res > 2)
res = 2;
return res;
}
int player_res_cold(bool calc_unid, bool temp, bool items)
{
int rc = 0;
if (temp)
{
if (you.duration[DUR_RESISTANCE])
rc++;
if (you.duration[DUR_FIRE_SHIELD])
rc -= 2;
if (you.duration[DUR_QAZLAL_COLD_RES])
rc++;
rc += get_form()->res_cold();
if (you.species == SP_VAMPIRE && !you.vampire_alive)
rc += 2;
}
if (items)
{
// rings of cold resistance/ice
rc += you.wearing(EQ_RINGS, RING_PROTECTION_FROM_COLD, calc_unid);
rc += you.wearing(EQ_RINGS, RING_ICE, calc_unid);
// rings of fire
rc -= you.wearing(EQ_RINGS, RING_FIRE, calc_unid);
// Staves
rc += you.wearing(EQ_STAFF, STAFF_COLD, calc_unid);
// body armour:
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
rc += armour_type_prop(body_armour->sub_type, ARMF_RES_COLD);
// ego armours
rc += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_COLD_RESISTANCE);
rc += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_RESISTANCE);
// randart weapons:
rc += you.scan_artefacts(ARTP_COLD, calc_unid);
// dragonskin cloak: 0.5 to draconic resistances
if (calc_unid && player_equip_unrand(UNRAND_DRAGONSKIN) && coinflip())
rc++;
}
// mutations:
rc += you.get_mutation_level(MUT_COLD_RESISTANCE, temp);
rc -= you.get_mutation_level(MUT_COLD_VULNERABILITY, temp);
rc -= you.get_mutation_level(MUT_TEMPERATURE_SENSITIVITY, temp);
rc += you.get_mutation_level(MUT_ICY_BLUE_SCALES, temp) == 3 ? 1 : 0;
rc += you.get_mutation_level(MUT_SHAGGY_FUR, temp) == 3 ? 1 : 0;
if (rc < -3)
rc = -3;
else if (rc > 3)
rc = 3;
return rc;
}
bool player::res_corr(bool calc_unid, bool items) const
{
if (have_passive(passive_t::resist_corrosion))
return true;
if (get_mutation_level(MUT_ACID_RESISTANCE))
return true;
if (get_form()->res_acid())
return true;
if (you.duration[DUR_RESISTANCE])
return true;
// TODO: why doesn't this use the usual form suppression mechanism?
if (form_keeps_mutations()
&& get_mutation_level(MUT_YELLOW_SCALES) >= 3)
{
return true;
}
return actor::res_corr(calc_unid, items);
}
int player_res_acid(bool calc_unid, bool items)
{
return you.res_corr(calc_unid, items) ? 1 : 0;
}
int player_res_electricity(bool calc_unid, bool temp, bool items)
{
int re = 0;
if (items)
{
// staff
re += you.wearing(EQ_STAFF, STAFF_AIR, calc_unid);
// body armour:
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
re += armour_type_prop(body_armour->sub_type, ARMF_RES_ELEC);
// randart weapons:
re += you.scan_artefacts(ARTP_ELECTRICITY, calc_unid);
// dragonskin cloak: 0.5 to draconic resistances
if (calc_unid && player_equip_unrand(UNRAND_DRAGONSKIN) && coinflip())
re++;
}
// mutations:
re += you.get_mutation_level(MUT_THIN_METALLIC_SCALES, temp) == 3 ? 1 : 0;
re += you.get_mutation_level(MUT_SHOCK_RESISTANCE, temp);
re -= you.get_mutation_level(MUT_SHOCK_VULNERABILITY, temp);
if (temp)
{
if (you.duration[DUR_RESISTANCE])
re++;
if (you.duration[DUR_QAZLAL_ELEC_RES])
re++;
// transformations:
if (get_form()->res_elec())
re++;
}
if (re > 1)
re = 1;
return re;
}
/**
* Is the player character immune to torment?
*
* @param random Whether to include unreliable effects (stochastic resist)
* @return Whether the player resists a given instance of torment; if
* random is passed, the result may vary from call to call.
*/
bool player_res_torment(bool random)
{
if (you.get_mutation_level(MUT_TORMENT_RESISTANCE))
return true;
if (random
&& you.get_mutation_level(MUT_STOCHASTIC_TORMENT_RESISTANCE)
&& coinflip())
{
return true;
}
return get_form()->res_neg() == 3
|| you.species == SP_VAMPIRE && !you.vampire_alive
|| you.petrified()
#if TAG_MAJOR_VERSION == 34
|| player_equip_unrand(UNRAND_ETERNAL_TORMENT)
#endif
;
}
// Kiku protects you from torment to a degree.
bool player_kiku_res_torment()
{
// no protection during pain branding weapon
return have_passive(passive_t::resist_torment)
&& !(you_worship(GOD_KIKUBAAQUDGHA) && you.gift_timeout);
}
// If temp is set to false, temporary sources or resistance won't be counted.
int player_res_poison(bool calc_unid, bool temp, bool items)
{
switch (you.undead_state(temp))
{
case US_ALIVE:
break;
case US_HUNGRY_DEAD: //ghouls
case US_UNDEAD: // mummies & lichform
return 3;
case US_SEMI_UNDEAD: // vampire
if (!you.vampire_alive) // XXX: && temp?
return 3;
break;
}
if (you.is_nonliving(temp)
|| temp && get_form()->res_pois() == 3
|| items && player_equip_unrand(UNRAND_OLGREB)
|| temp && you.duration[DUR_DIVINE_STAMINA])
{
return 3;
}
int rp = 0;
if (items)
{
// rings of poison resistance
rp += you.wearing(EQ_RINGS, RING_POISON_RESISTANCE, calc_unid);
// Staves
rp += you.wearing(EQ_STAFF, STAFF_POISON, calc_unid);
// ego armour:
rp += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_POISON_RESISTANCE);
// body armour:
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
rp += armour_type_prop(body_armour->sub_type, ARMF_RES_POISON);
// rPois+ artefacts
rp += you.scan_artefacts(ARTP_POISON, calc_unid);
// dragonskin cloak: 0.5 to draconic resistances
if (calc_unid && player_equip_unrand(UNRAND_DRAGONSKIN) && coinflip())
rp++;
}
// mutations:
rp += you.get_mutation_level(MUT_POISON_RESISTANCE, temp);
rp += you.get_mutation_level(MUT_SLIMY_GREEN_SCALES, temp) == 3 ? 1 : 0;
if (temp)
{
// potions/cards:
if (you.duration[DUR_RESISTANCE])
rp++;
if (get_form()->res_pois() > 0)
rp++;
}
// Cap rPois at + before vulnerability effects are applied
// (so carrying multiple rPois effects is never useful)
rp = min(1, rp);
if (temp)
{
if (get_form()->res_pois() < 0)
rp--;
if (you.duration[DUR_POISON_VULN])
rp--;
}
// don't allow rPois--, etc.
rp = max(-1, rp);
return rp;
}
int player_res_sticky_flame(bool calc_unid, bool temp, bool items)
{
int rsf = 0;
// dragonskin cloak: 0.5 to draconic resistances
if (items && calc_unid
&& player_equip_unrand(UNRAND_DRAGONSKIN) && coinflip())
{
rsf++;
}
if (get_form()->res_sticky_flame())
rsf++;
if (rsf > 1)
rsf = 1;
return rsf;
}
int player_spec_death()
{
int sd = 0;
// Staves
sd += you.wearing(EQ_STAFF, STAFF_DEATH);
// species:
sd += you.get_mutation_level(MUT_NECRO_ENHANCER);
// transformations:
if (you.form == transformation::lich)
sd++;
return sd;
}
int player_spec_fire()
{
int sf = 0;
// staves:
sf += you.wearing(EQ_STAFF, STAFF_FIRE);
// rings of fire:
sf += you.wearing(EQ_RINGS, RING_FIRE);
if (you.duration[DUR_FIRE_SHIELD])
sf++;
return sf;
}
int player_spec_cold()
{
int sc = 0;
// staves:
sc += you.wearing(EQ_STAFF, STAFF_COLD);
// rings of ice:
sc += you.wearing(EQ_RINGS, RING_ICE);
return sc;
}
int player_spec_earth()
{
int se = 0;
// Staves
se += you.wearing(EQ_STAFF, STAFF_EARTH);
return se;
}
int player_spec_air()
{
int sa = 0;
// Staves
sa += you.wearing(EQ_STAFF, STAFF_AIR);
return sa;
}
int player_spec_conj()
{
int sc = 0;
// Staves
sc += you.wearing(EQ_STAFF, STAFF_CONJURATION);
if (player_equip_unrand(UNRAND_BATTLE))
sc++;
return sc;
}
int player_spec_hex()
{
return 0;
}
int player_spec_charm()
{
// Nothing, for the moment.
return 0;
}
int player_spec_summ()
{
int ss = 0;
// Staves
ss += you.wearing(EQ_STAFF, STAFF_SUMMONING);
return ss;
}
int player_spec_poison()
{
int sp = 0;
// Staves
sp += you.wearing(EQ_STAFF, STAFF_POISON);
if (player_equip_unrand(UNRAND_OLGREB))
sp++;
return sp;
}
int player_energy()
{
int pe = 0;
// Staves
pe += you.wearing(EQ_STAFF, STAFF_ENERGY);
return pe;
}
// If temp is set to false, temporary sources of resistance won't be
// counted.
int player_prot_life(bool calc_unid, bool temp, bool items)
{
int pl = 0;
// Hunger is temporary, true, but that's something you can control,
// especially as life protection only increases the hungrier you
// get.
if (you.species == SP_VAMPIRE && !you.vampire_alive)
pl = 3;
// Same here. Your piety status, and, hence, TSO's protection, is
// something you can more or less control.
if (you_worship(GOD_SHINING_ONE))
{
if (you.piety >= piety_breakpoint(1))
pl++;
if (you.piety >= piety_breakpoint(3))
pl++;
if (you.piety >= piety_breakpoint(5))
pl++;
}
if (temp)
{
pl += get_form()->res_neg();
// completely stoned, unlike statue which has some life force
if (you.petrified())
pl += 3;
}
if (items)
{
// rings
pl += you.wearing(EQ_RINGS, RING_LIFE_PROTECTION, calc_unid);
// armour (checks body armour only)
pl += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_POSITIVE_ENERGY);
// pearl dragon counts
const item_def *body_armour = you.slot_item(EQ_BODY_ARMOUR);
if (body_armour)
pl += armour_type_prop(body_armour->sub_type, ARMF_RES_NEG);
// randart wpns
pl += you.scan_artefacts(ARTP_NEGATIVE_ENERGY, calc_unid);
// dragonskin cloak: 0.5 to draconic resistances
if (calc_unid && player_equip_unrand(UNRAND_DRAGONSKIN) && coinflip())
pl++;
pl += you.wearing(EQ_STAFF, STAFF_DEATH, calc_unid);
}
// undead/demonic power
pl += you.get_mutation_level(MUT_NEGATIVE_ENERGY_RESISTANCE, temp);
pl = min(3, pl);
return pl;
}
// New player movement speed system... allows for a bit more than
// "player runs fast" and "player walks slow" in that the speed is
// actually calculated (allowing for centaurs to get a bonus from
// swiftness and other such things). Levels of the mutation now
// also have meaning (before they all just meant fast). Most of
// this isn't as fast as it used to be (6 for having anything), but
// even a slight speed advantage is very good... and we certainly don't
// want to go past 6 (see below). -- bwr
int player_movement_speed()
{
int mv = 10;
// transformations
if (you.form == transformation::bat)
mv = 5; // but allowed minimum is six
else if (you.form == transformation::pig)
mv = 7;
else if (you.form == transformation::wisp)
mv = 8;
else if (you.fishtail || you.form == transformation::hydra && you.in_water())
mv = 6;
// Wading through water is very slow.
if (you.in_water() && !you.can_swim())
mv += 6;
// moving on liquefied ground takes longer
if (you.liquefied_ground())
mv += 3;
// armour
if (you.run())
mv -= 1;
mv += you.wearing_ego(EQ_ALL_ARMOUR, SPARM_PONDEROUSNESS);
// Cheibriados
if (have_passive(passive_t::slowed))
mv += 2 + min(div_rand_round(you.piety, 20), 8);
else if (player_under_penance(GOD_CHEIBRIADOS))
mv += 2 + min(div_rand_round(you.piety_max[GOD_CHEIBRIADOS], 20), 8);
// Tengu can move slightly faster when flying.
if (you.tengu_flight())
mv--;
if (you.duration[DUR_FROZEN])
mv += 3;
// Mutations: -2, -3, -4, unless innate and shapechanged.
if (int fast = you.get_mutation_level(MUT_FAST))
mv -= fast + 1;
if (int slow = you.get_mutation_level(MUT_SLOW))
{
mv *= 10 + slow * 2;
mv /= 10;
}
if (you.duration[DUR_SWIFTNESS] > 0 && !you.in_liquid())
{
if (you.attribute[ATTR_SWIFTNESS] > 0)
mv = div_rand_round(3*mv, 4);
else if (mv >= 8)
mv = div_rand_round(3*mv, 2);
else if (mv == 7)
mv = div_rand_round(7*6, 5); // balance for the cap at 6
}
// We'll use the old value of six as a minimum, with haste this could
// end up as a speed of three, which is about as fast as we want
// the player to be able to go (since 3 is 3.33x as fast and 2 is 5x,
// which is a bit of a jump, and a bit too fast) -- bwr
// Currently Haste takes 6 to 4, which is 2.5x as fast as delay 10
// and still seems plenty fast. -- elliptic
if (mv < FASTEST_PLAYER_MOVE_SPEED)
mv = FASTEST_PLAYER_MOVE_SPEED;
return mv;
}
/**
* Multiply the power of some evocation per the player's current evocations
* enhancers.
*
* @param power The base power of the evocation.
* @param enhancers Bonus enhancers to evocations (pak device surge).
* @return A modified power value.
*/
const int player_adjust_evoc_power(const int power, int enhancers)
{
const int total_enhancers = you.spec_evoke() + enhancers;
return stepdown_spellpower(100 *apply_enhancement(power, total_enhancers));
}
// This function differs from the above in that it's used to set the
// initial time_taken value for the turn. Everything else (movement,
// spellcasting, combat) applies a ratio to this value.
int player_speed()
{
int ps = 10;
// When paralysed, speed is irrelevant.
if (you.cannot_act())
return ps;
if (you.duration[DUR_SLOW] || have_stat_zero())
ps = haste_mul(ps);
if (you.duration[DUR_BERSERK] && !have_passive(passive_t::no_haste))
ps = berserk_div(ps);
else if (you.duration[DUR_HASTE])
ps = haste_div(ps);
if (you.form == transformation::statue || you.duration[DUR_PETRIFYING])
{
ps *= 15;
ps /= 10;
}
return ps;
}
bool is_effectively_light_armour(const item_def *item)
{
return !item
|| (abs(property(*item, PARM_EVASION)) / 10 < 5);
}
bool player_effectively_in_light_armour()
{
const item_def *armour = you.slot_item(EQ_BODY_ARMOUR, false);
return is_effectively_light_armour(armour);
}
// This function returns true if the player has a radically different
// shape... minor changes like blade hands don't count, also note
// that lich transformation doesn't change the character's shape
// (so we end up with Naga-liches, Spriggan-liches, Minotaur-liches)
// it just makes the character undead (with the benefits that implies). - bwr
bool player_is_shapechanged()
{
if (you.form == transformation::none
|| you.form == transformation::blade_hands
|| you.form == transformation::lich
|| you.form == transformation::shadow
|| you.form == transformation::appendage)
{
return false;
}
return true;
}
void update_acrobat_status()
{
if (you.props[ACROBAT_AMULET_ACTIVE].get_int() != 1)
return;
// Acrobat duration goes slightly into the next turn, giving the
// player visual feedback of the EV bonus recieved.
// This is assignment and not increment as acrobat duration depends
// on player action.
you.duration[DUR_ACROBAT] = you.time_taken+1;
you.redraw_evasion = true;
}
// An evasion factor based on the player's body size, smaller == higher
// evasion size factor.
static int _player_evasion_size_factor(bool base = false)
{
// XXX: you.body_size() implementations are incomplete, fix.
const size_type size = you.body_size(PSIZE_BODY, base);
return 2 * (SIZE_MEDIUM - size);
}
// Determines racial shield penalties (formicids get a bonus compared to
// other medium-sized races)
int player_shield_racial_factor()
{
return max(1, 5 + (you.species == SP_FORMICID ? -2 // Same as trolls/centaurs/etc.
: _player_evasion_size_factor(true)));
}
// The total EV penalty to the player for all their worn armour items
// with a base EV penalty (i.e. EV penalty as a base armour property,
// not as a randart property).
static int _player_adjusted_evasion_penalty(const int scale)
{
int piece_armour_evasion_penalty = 0;
// Some lesser armours have small penalties now (barding).
for (int i = EQ_MIN_ARMOUR; i < EQ_MAX_ARMOUR; i++)
{
if (i == EQ_SHIELD || !you.slot_item(static_cast<equipment_type>(i)))
continue;
// [ds] Evasion modifiers for armour are negatives, change
// those to positive for penalty calc.
const int penalty = (-property(you.inv[you.equip[i]], PARM_EVASION))/3;
if (penalty > 0)
piece_armour_evasion_penalty += penalty;
}
return piece_armour_evasion_penalty * scale / 10 +
you.adjusted_body_armour_penalty(scale);
}
// Player EV bonuses for various effects and transformations. This
// does not include tengu/merfolk EV bonuses for flight/swimming.
static int _player_evasion_bonuses()
{
int evbonus = 0;
if (you.duration[DUR_AGILITY])
evbonus += AGILITY_BONUS;
evbonus += you.wearing(EQ_RINGS_PLUS, RING_EVASION);
evbonus += you.scan_artefacts(ARTP_EVASION);
// mutations
evbonus += you.get_mutation_level(MUT_GELATINOUS_BODY);
if (you.get_mutation_level(MUT_DISTORTION_FIELD))
evbonus += you.get_mutation_level(MUT_DISTORTION_FIELD) + 1;
// transformation penalties/bonuses not covered by size alone:
if (you.get_mutation_level(MUT_SLOW_REFLEXES))
evbonus -= you.get_mutation_level(MUT_SLOW_REFLEXES) * 5;
// If you have an active amulet of the acrobat and just moved or waited, get massive
// EV bonus.
if (acrobat_boost_active())
evbonus += 15;
return evbonus;
}
// Player EV scaling for being flying tengu or swimming merfolk.
static int _player_scale_evasion(int prescaled_ev, const int scale)
{
if (you.duration[DUR_PETRIFYING] || you.caught())
prescaled_ev /= 2;
// Merfolk get a 25% evasion bonus in water.
if (you.fishtail)
{
const int ev_bonus = max(2 * scale, prescaled_ev / 4);
return prescaled_ev + ev_bonus;
}
// Flying Tengu get a 20% evasion bonus.
if (you.tengu_flight())
{
const int ev_bonus = max(1 * scale, prescaled_ev / 5);
return prescaled_ev + ev_bonus;
}
return prescaled_ev;
}
/**
* What is the player's bonus to EV from dodging when not paralyzed, after
* accounting for size & body armour penalties?
*
* First, calculate base dodge bonus (linear with dodging * stepdowned dex),
* and armour dodge penalty (base armour evp, increased for small races &
* decreased for large, then with a magic "3" subtracted from it to make the
* penalties not too harsh).
*
* If the player's strength is greater than the armour dodge penalty, return
* base dodge * (1 - dodge_pen / (str*2)).
* E.g., if str is twice dodge penalty, return 3/4 of base dodge. If
* str = dodge_pen * 4, return 7/8...
*
* If str is less than dodge penalty, return
* base_dodge * str / (dodge_pen * 2).
* E.g., if str = dodge_pen / 2, return 1/4 of base dodge. if
* str = dodge_pen / 4, return 1/8...
*
* For either equation, if str = dodge_pen, the result is base_dodge/2.
*
* @param scale A scale to multiply the result by, to avoid precision loss.
* @return A bonus to EV, multiplied by the scale.
*/
static int _player_armour_adjusted_dodge_bonus(int scale)
{
const int ev_dex = stepdown(you.dex(), 18, ROUND_CLOSE, MAX_STAT_VALUE);
const int dodge_bonus =
(70 + you.skill(SK_DODGING, 10) * ev_dex) * scale
/ (20 - _player_evasion_size_factor()) / 10;
const int armour_dodge_penalty = you.unadjusted_body_armour_penalty() - 3;
if (armour_dodge_penalty <= 0)
return dodge_bonus;
const int str = max(1, you.strength());
if (armour_dodge_penalty >= str)
return dodge_bonus * str / (armour_dodge_penalty * 2);
return dodge_bonus - dodge_bonus * armour_dodge_penalty / (str * 2);
}
// Total EV for player using the revised 0.6 evasion model.
static int _player_evasion(ev_ignore_type evit)
{
const int size_factor = _player_evasion_size_factor();
// Size is all that matters when paralysed or at 0 dex.
if ((you.cannot_move() || you.duration[DUR_CLUMSY]
|| you.form == transformation::tree)
&& !(evit & ev_ignore::helpless))
{
return max(1, 2 + size_factor / 2);
}
const int scale = 100;
const int size_base_ev = (10 + size_factor) * scale;
const int vertigo_penalty = you.duration[DUR_VERTIGO] ? 5 * scale : 0;
const int prestepdown_evasion =
size_base_ev
+ _player_armour_adjusted_dodge_bonus(scale)
- _player_adjusted_evasion_penalty(scale)
- you.adjusted_shield_penalty(scale)
- vertigo_penalty;
const int poststepdown_evasion =
stepdown_value(prestepdown_evasion, 20*scale, 30*scale, 60*scale, -1);
const int evasion_bonuses = _player_evasion_bonuses() * scale;
const int final_evasion =
_player_scale_evasion(poststepdown_evasion, scale) + evasion_bonuses;
return unscale_round_up(final_evasion, scale);
}
// Returns the spellcasting penalty (increase in spell failure) for the
// player's worn body armour and shield.
int player_armour_shield_spell_penalty()
{
const int scale = 100;
const int body_armour_penalty =
max(19 * you.adjusted_body_armour_penalty(scale), 0);
const int total_penalty = body_armour_penalty
+ 19 * you.adjusted_shield_penalty(scale);
return max(total_penalty, 0) / scale;
}
/**
* How many spell-success-chance-boosting ('wizardry') effects can the player
* apply to the given spell?
*
* @param spell The type of spell being cast.
* @return The number of relevant wizardry effects.
*/
int player_wizardry(spell_type spell)
{
return you.wearing(EQ_RINGS, RING_WIZARDRY)
+ you.wearing(EQ_STAFF, STAFF_WIZARDRY);
}
/**
* Calculate the SH value used internally.
*
* Exactly twice the value displayed to players, for legacy reasons.
* @return The player's current SH value.
*/
int player_shield_class()
{
int shield = 0;
if (you.incapacitated())
return 0;
if (you.shield())
{
const item_def& item = you.inv[you.equip[EQ_SHIELD]];
int size_factor = (you.body_size(PSIZE_TORSO) - SIZE_MEDIUM)
* (item.sub_type - ARM_LARGE_SHIELD);
int base_shield = property(item, PARM_AC) * 2 + size_factor;
// bonus applied only to base, see above for effect:
shield += base_shield * 50;
shield += base_shield * you.skill(SK_SHIELDS, 5) / 2;
shield += item.plus * 200;
shield += you.skill(SK_SHIELDS, 38)
+ min(you.skill(SK_SHIELDS, 38), 3 * 38);
int stat = 0;
if (item.sub_type == ARM_BUCKLER)
stat = you.dex() * 38;
else if (item.sub_type == ARM_LARGE_SHIELD)
stat = you.dex() * 12 + you.strength() * 26;
else
stat = you.dex() * 19 + you.strength() * 19;
stat = stat * (base_shield + 13) / 26;
shield += stat;
}
// mutations
// +4, +6, +8 (displayed values)
shield += (you.get_mutation_level(MUT_LARGE_BONE_PLATES) > 0
? you.get_mutation_level(MUT_LARGE_BONE_PLATES) * 400 + 400
: 0);
shield += qazlal_sh_boost() * 100;
shield += tso_sh_boost() * 100;
shield += you.wearing(EQ_AMULET_PLUS, AMU_REFLECTION) * 200;
shield += you.scan_artefacts(ARTP_SHIELDING) * 200;
return (shield + 50) / 100;
}
/**
* Calculate the SH value that should be displayed to players.
*
* Exactly half the internal value, for legacy reasons.
* @return The SH value to be displayed.
*/
int player_displayed_shield_class()
{
return player_shield_class() / 2;
}
/**
* Does the player have 'omnireflection' (the ability to reflect piercing
* effects and enchantments)?
*
* @return Whether the player has the Warlock's Mirror equipped.
*/
bool player_omnireflects()
{
return player_equip_unrand(UNRAND_WARLOCK_MIRROR);
}
void forget_map(bool rot)
{
ASSERT(!crawl_state.game_is_arena());
// If forgetting was intentional, clear the travel trail.
if (!rot)
clear_travel_trail();
const bool rot_resist = player_in_branch(BRANCH_ABYSS)
&& have_passive(passive_t::map_rot_res_abyss);
const double geometric_chance = 0.99;
const int radius = (rot_resist ? 200 : 100);
const int scalar = 0xFF;
for (rectangle_iterator ri(0); ri; ++ri)
{
const coord_def &p = *ri;
if (!env.map_knowledge(p).known() || you.see_cell(p))
continue;
if (rot)
{
const int dist = grid_distance(you.pos(), p);
int chance = pow(geometric_chance,
max(1, (dist * dist - radius) / 40)) * scalar;
if (x_chance_in_y(chance, scalar))
continue;
}
if (you.see_cell(p))
continue;
env.map_knowledge(p).clear();
if (env.map_forgotten)
(*env.map_forgotten)(p).clear();
StashTrack.update_stash(p);
#ifdef USE_TILE
tile_forget_map(p);
#endif
}
ash_detect_portals(is_map_persistent());
#ifdef USE_TILE
tiles.update_minimap_bounds();
#endif
}
static void _recover_stat()
{
FixedVector<int, NUM_STATS> recovered_stats(0);
while (you.attribute[ATTR_STAT_LOSS_XP] <= 0)
{
stat_type stat = random_lost_stat();
ASSERT(stat != NUM_STATS);
recovered_stats[stat]++;
// Very heavily drained stats recover faster.
if (you.stat(stat, false) < 0)
recovered_stats[stat] += random2(-you.stat(stat, false) / 2);
bool still_drained = false;
for (int i = 0; i < NUM_STATS; ++i)
if (you.stat_loss[i] - recovered_stats[i] > 0)
still_drained = true;
if (still_drained)
you.attribute[ATTR_STAT_LOSS_XP] += stat_loss_roll();
else
break;
}
for (int i = 0; i < NUM_STATS; ++i)
if (recovered_stats[i] > 0)
restore_stat((stat_type) i, recovered_stats[i], false, true);
}
int get_exp_progress()
{
if (you.experience_level >= you.get_max_xl())
return 0;
const int current = exp_needed(you.experience_level);
const int next = exp_needed(you.experience_level + 1);
if (next == current)
return 0;
return (you.experience - current) * 100 / (next - current);
}
static void _recharge_xp_evokers(int exp)
{
FixedVector<item_def*, NUM_MISCELLANY> evokers(nullptr);
list_charging_evokers(evokers);
int xp_factor = max(min((int)exp_needed(you.experience_level+1, 0) * 2 / 7,
you.experience_level * 425),
you.experience_level*4 + 30)
/ (3 + you.skill_rdiv(SK_EVOCATIONS, 2, 13));
for (int i = 0; i < NUM_MISCELLANY; ++i)
{
item_def* evoker = evokers[i];
if (!evoker)
continue;
int &debt = evoker_debt(evoker->sub_type);
if (debt == 0)
continue;
const int old_charges = evoker_charges(i);
debt = max(0, debt - div_rand_round(exp, xp_factor));
const int gained = evoker_charges(i) - old_charges;
if (!gained)
continue;
if (evoker_max_charges(i) == 1)
mprf("%s has recharged.", evoker->name(DESC_YOUR).c_str());
else
{
mprf("%s has regained %s charge%s.",
evoker->name(DESC_YOUR).c_str(),
number_in_words(gained).c_str(), gained > 1 ? "s" : "");
}
}
}
/// Make progress toward the abyss spawning an exit/stairs.
static void _reduce_abyss_xp_timer(int exp)
{
if (!player_in_branch(BRANCH_ABYSS))
return;
const int xp_factor =
max(min((int)exp_needed(you.experience_level+1, 0) / 7,
you.experience_level * 425),
you.experience_level*2 + 15) / 5;
if (!you.props.exists(ABYSS_STAIR_XP_KEY))
you.props[ABYSS_STAIR_XP_KEY] = EXIT_XP_COST;
const int reqd_xp = you.props[ABYSS_STAIR_XP_KEY].get_int();
const int new_req = reqd_xp - div_rand_round(exp, xp_factor);
dprf("reducing xp timer from %d to %d (factor = %d)",
reqd_xp, new_req, xp_factor);
you.props[ABYSS_STAIR_XP_KEY].get_int() = new_req;
}
/// update penance for XP based gods
static void _handle_xp_penance(int exp)
{
vector<god_type> xp_gods;
for (god_iterator it; it; ++it)
{
if (xp_penance(*it))
xp_gods.push_back(*it);
}
if (!xp_gods.empty())
{
god_type god = xp_gods[random2(xp_gods.size())];
reduce_xp_penance(god, exp);
}
}
/// update transfer knowledge
static void _transfer_knowledge(int exp)
{
if (!(you.transfer_skill_points > 0))
return;
// Can happen if the game got interrupted during target skill choice.
if (is_invalid_skill(you.transfer_to_skill))
{
you.transfer_from_skill = SK_NONE;
you.transfer_skill_points = 0;
you.transfer_total_skill_points = 0;
}
else
{
int amount = exp * 20
/ calc_skill_cost(you.skill_cost_level);
if (amount >= 20 || one_chance_in(20 - amount))
{
amount = max(20, amount);
transfer_skill_points(you.transfer_from_skill,
you.transfer_to_skill, amount, false);
}
}
}
/// update temporary mutations
static void _handle_temp_mutation(int exp)
{
if (!(you.attribute[ATTR_TEMP_MUTATIONS] > 0))
return;
you.attribute[ATTR_TEMP_MUT_XP] -= exp;
if (you.attribute[ATTR_TEMP_MUT_XP] <= 0)
temp_mutation_wanes();
}
/// update stat loss
static void _handle_stat_loss(int exp)
{
if (!(you.attribute[ATTR_STAT_LOSS_XP] > 0))
return;
int loss = div_rand_round(exp * 3 / 2,
max(1, calc_skill_cost(you.skill_cost_level) - 3));
you.attribute[ATTR_STAT_LOSS_XP] -= loss;
dprf("Stat loss points: %d", you.attribute[ATTR_STAT_LOSS_XP]);
if (you.attribute[ATTR_STAT_LOSS_XP] <= 0)
_recover_stat();
}
/// update xp drain
static void _handle_xp_drain(int exp)
{
if (!you.attribute[ATTR_XP_DRAIN])
return;
int loss = div_rand_round(exp * 3 / 2,
calc_skill_cost(you.skill_cost_level));
// Make it easier to recover from very heavy levels of draining
// (they're nasty enough as it is)
loss = loss * (1 + (you.attribute[ATTR_XP_DRAIN] / 250.0f));
dprf("Lost %d of %d draining points", loss, you.attribute[ATTR_XP_DRAIN]);
you.attribute[ATTR_XP_DRAIN] -= loss;
// Regaining skills may affect AC/EV.
you.redraw_armour_class = true;
you.redraw_evasion = true;
if (you.attribute[ATTR_XP_DRAIN] <= 0)
{
you.attribute[ATTR_XP_DRAIN] = 0;
mprf(MSGCH_RECOVERY, "Your life force feels restored.");
}
}
static void _handle_god_wrath(int exp)
{
for (god_iterator it; it; ++it)
{
if (active_penance(*it))
{
you.attribute[ATTR_GOD_WRATH_XP] -= exp;
while (you.attribute[ATTR_GOD_WRATH_XP] < 0)
{
you.attribute[ATTR_GOD_WRATH_COUNT]++;
set_penance_xp_timeout();
}
break;
}
}
}
void gain_exp(unsigned int exp_gained, unsigned int* actual_gain)
{
if (crawl_state.game_is_arena())
return;
// xp-gated effects that don't use sprint inflation
_handle_xp_penance(exp_gained);
_handle_god_wrath(exp_gained);
_transfer_knowledge(exp_gained);
// evolution mutation timer
you.attribute[ATTR_EVOL_XP] += exp_gained;
// modified experience due to sprint inflation
unsigned int skill_xp = exp_gained;
if (crawl_state.game_is_sprint())
skill_xp = sprint_modify_exp(skill_xp);
// xp-gated effects that use sprint inflation
_handle_stat_loss(skill_xp);
_handle_temp_mutation(skill_xp);
_recharge_xp_evokers(skill_xp);
_reduce_abyss_xp_timer(skill_xp);
_handle_xp_drain(skill_xp);
if (player_under_penance(GOD_HEPLIAKLQANA))
return; // no xp for you!
// handle actual experience gains,
// i.e. XL and skills
const unsigned int old_exp = you.experience;
dprf("gain_exp: %d", exp_gained);
if (you.experience + exp_gained > (unsigned int)MAX_EXP_TOTAL)
you.experience = MAX_EXP_TOTAL;
else
you.experience += exp_gained;
you.exp_available += 10 * skill_xp;
train_skills();
while (check_selected_skills()
&& you.exp_available >= calc_skill_cost(you.skill_cost_level))
{
train_skills();
}
level_change();
if (actual_gain != nullptr)
*actual_gain = you.experience - old_exp;
}
bool will_gain_life(int lev)
{
if (lev < you.attribute[ATTR_LIFE_GAINED] - 2)
return false;
return you.lives + you.deaths < (lev - 1) / 3;
}
static void _felid_extra_life()
{
if (will_gain_life(you.max_level)
&& you.lives < 2)
{
you.lives++;
mprf(MSGCH_INTRINSIC_GAIN, "Extra life!");
you.attribute[ATTR_LIFE_GAINED] = you.max_level;
// Should play the 1UP sound from SMB...
}
}
static void _gain_and_note_hp_mp()
{
const int old_mp = you.magic_points;
const int old_maxmp = you.max_magic_points;
// recalculate for game
calc_hp(true, false);
calc_mp();
set_mp(old_maxmp > 0 ? old_mp * you.max_magic_points / old_maxmp
: you.max_magic_points);
// Get "real" values for note-taking, i.e. ignore Berserk,
// transformations or equipped items.
const int note_maxhp = get_real_hp(false, true);
const int note_maxmp = get_real_mp(false);
char buf[200];
sprintf(buf, "HP: %d/%d MP: %d/%d",
min(you.hp, note_maxhp), note_maxhp,
min(you.magic_points, note_maxmp), note_maxmp);
take_note(Note(NOTE_XP_LEVEL_CHANGE, you.experience_level, 0, buf));
}
/**
* Calculate max HP changes and scale current HP accordingly.
*/
void calc_hp(bool scale, bool set)
{
// Rounding must be down or Deep Dwarves would abuse certain values.
// We can reduce errors by a factor of 100 by using partial hp we have.
int oldhp = you.hp;
int old_max = you.hp_max;
you.hp_max = get_real_hp(true, true);
if (scale)
{
int hp = you.hp * 100 + you.hit_points_regeneration;
int new_max = you.hp_max;
hp = hp * new_max / old_max;
if (hp < 100)
hp = 100;
set_hp(min(hp / 100, you.hp_max));
you.hit_points_regeneration = hp % 100;
}
if (set)
you.hp = you.hp_max;
you.hp = min(you.hp, you.hp_max);
if (oldhp != you.hp || old_max != you.hp_max)
{
dprf("HP changed: %d/%d -> %d/%d", oldhp, old_max, you.hp, you.hp_max);
you.redraw_hit_points = true;
}
}
int xp_to_level_diff(int xp, int scale)
{
ASSERT(xp >= 0);
const int adjusted_xp = you.experience + xp;
int projected_level = you.experience_level;
while (you.experience >= exp_needed(projected_level + 1))
projected_level++; // handle xl 27 chars
int adjusted_level = projected_level;
// closest whole number level, rounding down
while (adjusted_xp >= (int) exp_needed(adjusted_level + 1))
adjusted_level++;
if (scale > 1)
{
// TODO: what is up with all the casts here?
// decimal scaled version of current level including whatever fractional
// part scale can handle
const int cur_level_scaled = projected_level * scale
+ (you.experience - (int) exp_needed(projected_level)) * scale /
((int) exp_needed(projected_level + 1)
- (int) exp_needed(projected_level));
// decimal scaled version of what adjusted_xp would get you
const int adjusted_level_scaled = adjusted_level * scale
+ (adjusted_xp - (int) exp_needed(adjusted_level)) * scale /
((int) exp_needed(adjusted_level + 1)
- (int) exp_needed(adjusted_level));
// TODO: this would be more usable with better rounding behavior
return adjusted_level_scaled - cur_level_scaled;
} else
return adjusted_level - projected_level;
}
/**
* Handle the effects from a player's change in XL.
* @param aux A string describing the cause of the level
* change.
* @param skip_attribute_increase If true and XL has increased, don't process
* stat gains. Currently only used by wizmode
* commands.
*/
void level_change(bool skip_attribute_increase)
{
// necessary for the time being, as level_change() is called
// directly sometimes {dlb}
you.redraw_experience = true;
while (you.experience < exp_needed(you.experience_level))
lose_level();
while (you.experience_level < you.get_max_xl()
&& you.experience >= exp_needed(you.experience_level + 1))
{
if (!skip_attribute_increase)
{
crawl_state.cancel_cmd_all();
if (is_processing_macro())
flush_input_buffer(FLUSH_ABORT_MACRO);
}
// [ds] Make sure we increment you.experience_level and apply
// any stat/hp increases only after we've cleared all prompts
// for this experience level. If we do part of the work before
// the prompt, and a player on telnet gets disconnected, the
// SIGHUP will save Crawl in the in-between state and rob the
// player of their level-up perks.
const int new_exp = you.experience_level + 1;
// some species need to do this at a specific time; most just do it at the end
bool updated_maxhp = false;
if (new_exp <= you.max_level)
{
mprf(MSGCH_INTRINSIC_GAIN,
"Welcome back to level %d!", new_exp);
// No more prompts for this XL past this point.
you.experience_level = new_exp;
}
else // Character has gained a new level
{
// Don't want to see the dead creature at the prompt.
redraw_screen();
if (new_exp == 27)
mprf(MSGCH_INTRINSIC_GAIN, "You have reached level 27, the final one!");
else if (new_exp == you.get_max_xl())
mprf(MSGCH_INTRINSIC_GAIN, "You have reached level %d, the highest you will ever reach!",
you.get_max_xl());
else
{
mprf(MSGCH_INTRINSIC_GAIN, "You have reached level %d!",
new_exp);
}
const bool manual_stat_level = new_exp % 3 == 0; // 3,6,9,12...
// Must do this before actually changing experience_level,
// so we will re-prompt on load if a hup is received.
if (manual_stat_level && !skip_attribute_increase)
if (!attribute_increase())
return; // abort level gain, the xp is still there
// Set this after printing, since a more() might clear it.
you.redraw_experience = true;
crawl_state.stat_gain_prompt = false;
you.experience_level = new_exp;
you.max_level = you.experience_level;
#ifdef USE_TILE_LOCAL
// In case of intrinsic ability changes.
tiles.layout_statcol();
redraw_screen();
#endif
if (!skip_attribute_increase)
species_stat_gain(you.species);
switch (you.species)
{
case SP_VAMPIRE:
if (you.experience_level == 3)
{
if (you.vampire_alive)
{
mprf(MSGCH_INTRINSIC_GAIN, "If you were bloodless "
"you could now transform into a vampire bat.");
}
else
{
mprf(MSGCH_INTRINSIC_GAIN,
"You can now transform into a vampire bat.");
}
}
break;
case SP_NAGA:
if (!(you.experience_level % 3))
{
mprf(MSGCH_INTRINSIC_GAIN, "Your skin feels tougher.");
you.redraw_armour_class = true;
}
break;
case SP_BASE_DRACONIAN:
if (you.experience_level >= 7)
{
you.species = random_draconian_colour();
// We just changed our aptitudes, so some skills may now
// be at the wrong level (with negative progress); if we
// print anything in this condition, we might trigger a
// --More--, a redraw, and a crash (#6376 on Mantis).
//
// Hence we first fix up our skill levels silently (passing
// do_level_up = false) but save the old values; then when
// we want the messages later, we restore the old skill
// levels and call check_skill_level_change() again, this
// time passing do_level_up = true.
uint8_t saved_skills[NUM_SKILLS];
for (skill_type sk = SK_FIRST_SKILL; sk < NUM_SKILLS; ++sk)
{
saved_skills[sk] = you.skills[sk];
check_skill_level_change(sk, false);
}
// The player symbol depends on species.
update_player_symbol();
#ifdef USE_TILE
init_player_doll();
#endif
mprf(MSGCH_INTRINSIC_GAIN,
"Your scales start taking on %s colour.",
article_a(scale_type(you.species)).c_str());
// Produce messages about skill increases/decreases. We
// restore one skill level at a time so that at most the
// skill being checked is at the wrong level.
for (skill_type sk = SK_FIRST_SKILL; sk < NUM_SKILLS; ++sk)
{
const int oldapt = species_apt(sk, SP_BASE_DRACONIAN);
const int newapt = species_apt(sk, you.species);
if (oldapt != newapt)
{
mprf(MSGCH_INTRINSIC_GAIN, "You learn %s %s%s.",
skill_name(sk),
abs(oldapt - newapt) > 1 ? "much " : "",
oldapt > newapt ? "slower" : "quicker");
}
you.skills[sk] = saved_skills[sk];
check_skill_level_change(sk);
}
// It's possible we passed a training target due to
// skills being rescaled to new aptitudes. Thus, we must
// check the training targets.
check_training_targets();
// Tell the player about their new species
for (auto &mut : fake_mutations(you.species, false))
mprf(MSGCH_INTRINSIC_GAIN, "%s", mut.c_str());
// needs to be done early here, so HP doesn't look rotted
// when we redraw the screen
_gain_and_note_hp_mp();
updated_maxhp = true;
redraw_screen();
}
break;
case SP_DEMONSPAWN:
{
bool gave_message = false;
int level = 0;
mutation_type first_body_facet = NUM_MUTATIONS;
for (const player::demon_trait trait : you.demonic_traits)
{
if (is_body_facet(trait.mutation))
{
if (first_body_facet < NUM_MUTATIONS
&& trait.mutation != first_body_facet)
{
if (you.experience_level == level)
{
mprf(MSGCH_MUTATION, "You feel monstrous as "
"your demonic heritage exerts itself.");
mark_milestone("monstrous", "discovered their "
"monstrous ancestry!");
}
break;
}
if (first_body_facet == NUM_MUTATIONS)
{
first_body_facet = trait.mutation;
level = trait.level_gained;
}
}
}
for (const player::demon_trait trait : you.demonic_traits)
{
if (trait.level_gained == you.experience_level)
{
if (!gave_message)
{
mprf(MSGCH_INTRINSIC_GAIN,
"Your demonic ancestry asserts itself...");
gave_message = true;
}
perma_mutate(trait.mutation, 1, "demonic ancestry");
}
}
break;
}
case SP_FELID:
_felid_extra_life();
break;
default:
break;
}
give_level_mutations(you.species, you.experience_level);
}
if (species_is_draconian(you.species) && !(you.experience_level % 3))
{
mprf(MSGCH_INTRINSIC_GAIN, "Your scales feel tougher.");
you.redraw_armour_class = true;
}
if (!updated_maxhp)
_gain_and_note_hp_mp();
xom_is_stimulated(12);
if (in_good_standing(GOD_HEPLIAKLQANA))
upgrade_hepliaklqana_ancestor();
learned_something_new(HINT_NEW_LEVEL);
}
while (you.experience >= exp_needed(you.max_level + 1))
{
ASSERT(you.experience_level == you.get_max_xl());
ASSERT(you.max_level < 127); // marshalled as an 1-byte value
you.max_level++;
if (you.species == SP_FELID)
_felid_extra_life();
}
you.redraw_title = true;
#ifdef DGL_WHEREIS
whereis_record();
#endif
// Hints mode arbitrarily ends at xp 7.
if (crawl_state.game_is_hints() && you.experience_level >= 7)
hints_finished();
}
void adjust_level(int diff, bool just_xp)
{
ASSERT((uint64_t)you.experience <= (uint64_t)MAX_EXP_TOTAL);
const int max_exp_level = you.get_max_xl();
if (you.experience_level + diff < 1)
you.experience = 0;
else if (you.experience_level + diff >= max_exp_level)
{
const unsigned needed = exp_needed(max_exp_level);
// Level gain when already at max should never reduce player XP;
// but level loss (diff < 0) should.
if (diff < 0 || you.experience < needed)
you.experience = needed;
}
else
{
while (diff < 0 && you.experience >=
exp_needed(max_exp_level))
{
// Having XP for level 53 and going back to 26 due to a single
// card would mean your felid is not going to get any extra lives
// in foreseable future.
you.experience -= exp_needed(max_exp_level)
- exp_needed(max_exp_level - 1);
diff++;
}
int old_min = exp_needed(you.experience_level);
int old_max = exp_needed(you.experience_level + 1);
int new_min = exp_needed(you.experience_level + diff);
int new_max = exp_needed(you.experience_level + 1 + diff);
dprf("XP before: %d\n", you.experience);
dprf("%4.2f of %d..%d to %d..%d",
(you.experience - old_min) * 1.0 / (old_max - old_min),
old_min, old_max, new_min, new_max);
you.experience = ((int64_t)(new_max - new_min))
* (you.experience - old_min)
/ (old_max - old_min)
+ new_min;
dprf("XP after: %d\n", you.experience);
}
ASSERT((uint64_t)you.experience <= (uint64_t)MAX_EXP_TOTAL);
if (!just_xp)
level_change();
}
/**
* Get the player's current stealth value.
*
* (Keep in mind, while tweaking this function: the order in which stealth
* modifiers are applied is significant!)
*
* @return The player's current stealth value.
*/
int player_stealth()
{
ASSERT(!crawl_state.game_is_arena());
// Extreme stealthiness can be enforced by wizmode stealth setting.
if (crawl_state.disables[DIS_MON_SIGHT])
return 1000;
// berserking, "clumsy" (0-dex), sacrifice stealth.
if (you.berserk()
|| you.duration[DUR_CLUMSY]
|| you.get_mutation_level(MUT_NO_STEALTH))
{
return 0;
}
int stealth = you.dex() * 3;
stealth += you.skill(SK_STEALTH, 15);
if (you.confused())
stealth /= 3;
const item_def *arm = you.slot_item(EQ_BODY_ARMOUR, false);
const item_def *boots = you.slot_item(EQ_BOOTS, false);
if (arm)
{
// [ds] New stealth penalty formula from rob: SP = 6 * (EP^2)
// Now 2 * EP^2 / 3 after EP rescaling.
const int evp = you.unadjusted_body_armour_penalty();
const int penalty = evp * evp * 2 / 3;
stealth -= penalty;
const int pips = armour_type_prop(arm->sub_type, ARMF_STEALTH);
stealth += pips * STEALTH_PIP;
}
stealth += STEALTH_PIP * you.scan_artefacts(ARTP_STEALTH);
stealth += STEALTH_PIP * you.wearing(EQ_RINGS, RING_STEALTH);
stealth -= STEALTH_PIP * you.wearing(EQ_RINGS, RING_ATTENTION);
if (you.duration[DUR_STEALTH])
stealth += STEALTH_PIP * 2;
if (you.duration[DUR_AGILITY])
stealth += STEALTH_PIP;
if (you.form == transformation::blade_hands && you.species == SP_FELID
&& !you.airborne())
{
stealth -= STEALTH_PIP; // klack klack klack go the blade paws
// this is an absurd special case but also it's really funny so w/e
}
// Mutations.
stealth += STEALTH_PIP * you.get_mutation_level(MUT_NIGHTSTALKER);
stealth += (STEALTH_PIP / 2)
* you.get_mutation_level(MUT_THIN_SKELETAL_STRUCTURE);
stealth += STEALTH_PIP * you.get_mutation_level(MUT_CAMOUFLAGE);
const int how_transparent = you.get_mutation_level(MUT_TRANSLUCENT_SKIN);
if (how_transparent)
stealth += 15 * (how_transparent);
// Radiating silence is the negative complement of shouting all the
// time... a sudden change from background noise to no noise is going
// to clue anything in to the fact that something is very wrong...
// a personal silence spell would naturally be different, but this
// silence radiates for a distance and prevents monster spellcasting,
// which pretty much gives away the stealth game.
if (you.duration[DUR_SILENCE])
stealth -= STEALTH_PIP;
// Bloodless vampires are stealthier.
if (you.species == SP_VAMPIRE && !you.vampire_alive)
stealth += STEALTH_PIP * 2;
if (!you.airborne())
{
if (you.in_water())
{
// Merfolk can sneak up on monsters underwater -- bwr
if (you.fishtail || you.species == SP_OCTOPODE)
stealth += STEALTH_PIP;
else if (!you.can_swim() && !you.extra_balanced())
stealth /= 2; // splashy-splashy
}
else if (boots && get_armour_ego_type(*boots) == SPARM_STEALTH)
stealth += STEALTH_PIP;
else if (you.has_usable_hooves())
stealth -= 5 + 5 * you.get_mutation_level(MUT_HOOVES);
else if (you.species == SP_FELID
&& (you.form == transformation::none
|| you.form == transformation::appendage))
{
stealth += 20; // paws
}
}
// If you've been tagged with Corona or are Glowing, the glow
// makes you extremely unstealthy.
if (you.backlit())
stealth = stealth * 2 / 5;
// On the other hand, shrouding has the reverse effect, if you know
// how to make use of it:
if (you.umbra())
{
int umbra_mul = 1, umbra_div = 1;
if (you.nightvision())
{
umbra_mul = you.piety + MAX_PIETY;
umbra_div = MAX_PIETY;
}
if (player_equip_unrand(UNRAND_SHADOWS)
&& 2 * umbra_mul < 3 * umbra_div)
{
umbra_mul = 3;
umbra_div = 2;
}
stealth *= umbra_mul;
stealth /= umbra_div;
}
if (you.form == transformation::shadow)
stealth *= 2;
// If you're surrounded by a storm, you're inherently pretty conspicuous.
if (have_passive(passive_t::storm_shield))
{
stealth = stealth
* (MAX_PIETY - min((int)you.piety, piety_breakpoint(5)))
/ (MAX_PIETY - piety_breakpoint(0));
}
// The shifting glow from the Orb, while too unstable to negate invis
// or affect to-hit, affects stealth even more than regular glow.
if (player_has_orb())
stealth /= 3;
stealth = max(0, stealth);
return stealth;
}
// Is a given duration about to expire?
bool dur_expiring(duration_type dur)
{
const int value = you.duration[dur];
if (value <= 0)
return false;
return value <= duration_expire_point(dur);
}
static void _display_char_status(int value, const char *fmt, ...)
{
va_list argp;
va_start(argp, fmt);
string msg = vmake_stringf(fmt, argp);
if (you.wizard)
mprf("%s (%d).", msg.c_str(), value);
else
mprf("%s.", msg.c_str());
va_end(argp);
}
static void _display_vampire_status()
{
string msg = "At your current blood state you ";
vector<const char *> attrib;
if (!you.vampire_alive)
{
attrib.push_back("are immune to poison");
attrib.push_back("significantly resist cold");
attrib.push_back("are immune to negative energy");
attrib.push_back("resist torment");
attrib.push_back("do not heal.");
}
else
attrib.push_back("heal quickly.");
if (!attrib.empty())
{
msg += comma_separated_line(attrib.begin(), attrib.end());
mpr(msg);
}
}
static void _display_movement_speed()
{
const int move_cost = (player_speed() * player_movement_speed()) / 10;
const bool water = you.in_liquid();
const bool swim = you.swimming();
const bool fly = you.airborne();
const bool swift = (you.duration[DUR_SWIFTNESS] > 0
&& you.attribute[ATTR_SWIFTNESS] >= 0);
const bool antiswift = (you.duration[DUR_SWIFTNESS] > 0
&& you.attribute[ATTR_SWIFTNESS] < 0);
_display_char_status(move_cost, "Your %s speed is %s%s%s",
// order is important for these:
(swim) ? "swimming" :
(water) ? "wading" :
(fly) ? "flying"
: "movement",
(!water && swift) ? "aided by the wind" :
(!water && antiswift) ? "hindered by the wind" : "",
(!water && swift) ? ((move_cost >= 10) ? ", but still "
: " and ") :
(!water && antiswift) ? ((move_cost <= 10) ? ", but still "
: " and ")
: "",
(move_cost < 8) ? "very quick" :
(move_cost < 10) ? "quick" :
(move_cost == 10) ? "average" :
(move_cost < 13) ? "slow"
: "very slow");
}
static void _display_tohit()
{
#ifdef DEBUG_DIAGNOSTICS
melee_attack attk(&you, nullptr);
const int to_hit = attk.calc_to_hit(false);
dprf("To-hit: %d", to_hit);
#endif
}
static const char* _attack_delay_desc(int attack_delay)
{
return (attack_delay >= 200) ? "extremely slow" :
(attack_delay >= 155) ? "very slow" :
(attack_delay >= 125) ? "quite slow" :
(attack_delay >= 105) ? "below average" :
(attack_delay >= 95) ? "average" :
(attack_delay >= 75) ? "above average" :
(attack_delay >= 55) ? "quite fast" :
(attack_delay >= 45) ? "very fast" :
(attack_delay >= 35) ? "extremely fast" :
"blindingly fast";
}
/**
* Print a message indicating the player's attack delay with their current
* weapon & its ammo (if applicable).
*
* Assumes the attack speed of a ranged weapon does not depend on what
* ammunition is being used (as long as it is valid).
*/
static void _display_attack_delay()
{
const item_def* weapon = you.weapon();
int delay;
if (weapon && is_range_weapon(*weapon))
{
item_def ammo;
ammo.base_type = OBJ_MISSILES;
ammo.sub_type = fires_ammo_type(*weapon);
delay = you.attack_delay(&ammo, false).expected();
}
else
delay = you.attack_delay(nullptr, false).expected();
const bool at_min_delay = weapon
&& you.skill(item_attack_skill(*weapon))
>= weapon_min_delay_skill(*weapon);
// Scale to fit the displayed weapon base delay, i.e.,
// normal speed is 100 (as in 100%).
int avg = 10 * delay;
_display_char_status(avg, "Your attack speed is %s%s%s",
_attack_delay_desc(avg),
at_min_delay ?
" (and cannot be improved with additional weapon skill)" : "",
you.adjusted_shield_penalty() ?
" (and is slowed by your insufficient shield skill)" : "");
}
// forward declaration
static string _constriction_description();
void display_char_status()
{
const int halo_size = you.halo_radius();
if (halo_size >= 0)
{
if (halo_size > 37)
mpr("You are illuminated by a large divine halo.");
else if (halo_size > 10)
mpr("You are illuminated by a divine halo.");
else
mpr("You are illuminated by a small divine halo.");
}
else if (you.haloed())
mpr("An external divine halo illuminates you.");
if (you.species == SP_VAMPIRE)
_display_vampire_status();
status_info inf;
for (unsigned i = 0; i <= STATUS_LAST_STATUS; ++i)
{
if (fill_status_info(i, inf) && !inf.long_text.empty())
mpr(inf.long_text);
}
string cinfo = _constriction_description();
if (!cinfo.empty())
mpr(cinfo);
_display_movement_speed();
_display_tohit();
_display_attack_delay();
// Display base attributes, if necessary.
if (innate_stat(STAT_STR) != you.strength()
|| innate_stat(STAT_INT) != you.intel()
|| innate_stat(STAT_DEX) != you.dex())
{
mprf("Your base attributes are Str %d, Int %d, Dex %d.",
innate_stat(STAT_STR),
innate_stat(STAT_INT),
innate_stat(STAT_DEX));
}
}
bool player::clarity(bool calc_unid, bool items) const
{
if (you.get_mutation_level(MUT_CLARITY))
return true;
if (have_passive(passive_t::clarity))
return true;
return actor::clarity(calc_unid, items);
}
bool player::gourmand(bool calc_unid, bool items) const
{
return you.get_mutation_level(MUT_GOURMAND) > 0
|| actor::gourmand(calc_unid, items);
}
bool player::stasis() const
{
return species == SP_FORMICID;
}
bool player::cloud_immune(bool calc_unid, bool items) const
{
return have_passive(passive_t::cloud_immunity)
|| actor::cloud_immune(calc_unid, items);
}
unsigned int exp_needed(int lev, int exp_apt)
{
unsigned int level = 0;
// Note: For historical reasons, all of the following numbers are for a
// species (like human) with XP aptitude 1, not 0 as one might expect.
// Basic plan:
// Section 1: levels 1- 5, second derivative goes 10-10-20-30.
// Section 2: levels 6-13, second derivative is exponential/doubling.
// Section 3: levels 14-27, second derivative is constant at 8470.
// Here's a table:
//
// level xp delta delta2
// ===== ======= ===== ======
// 1 0 0 0
// 2 10 10 10
// 3 30 20 10
// 4 70 40 20
// 5 140 70 30
// 6 270 130 60
// 7 520 250 120
// 8 1010 490 240
// 9 1980 970 480
// 10 3910 1930 960
// 11 7760 3850 1920
// 12 15450 7690 3840
// 13 26895 11445 3755
// 14 45585 18690 7245
// 15 72745 27160 8470
// 16 108375 35630 8470
// 17 152475 44100 8470
// 18 205045 52570 8470
// 19 266085 61040 8470
// 20 335595 69510 8470
// 21 413575 77980 8470
// 22 500025 86450 8470
// 23 594945 94920 8470
// 24 698335 103390 8470
// 25 810195 111860 8470
// 26 930525 120330 8470
// 27 1059325 128800 8470
switch (lev)
{
case 1:
level = 1;
break;
case 2:
level = 10;
break;
case 3:
level = 30;
break;
case 4:
level = 70;
break;
default:
if (lev < 13)
{
lev -= 4;
level = 10 + 10 * lev + (60 << lev);
}
else
{
lev -= 12;
level = 16675 + 5985 * lev + 4235 * lev * lev;
}
break;
}
if (exp_apt == -99)
exp_apt = species_exp_modifier(you.species);
return (unsigned int) ((level - 1) * apt_to_factor(exp_apt - 1));
}
// returns bonuses from rings of slaying, etc.
int slaying_bonus(bool ranged)
{
int ret = 0;
ret += you.wearing(EQ_RINGS_PLUS, RING_SLAYING);
ret += you.scan_artefacts(ARTP_SLAYING);
if (you.wearing_ego(EQ_GLOVES, SPARM_ARCHERY) && ranged)
ret += 4;
ret += 3 * augmentation_amount();
if (you.duration[DUR_SONG_OF_SLAYING])
ret += you.props[SONG_OF_SLAYING_KEY].get_int();
if (you.duration[DUR_HORROR])
ret -= you.props[HORROR_PENALTY_KEY].get_int();
ret += you.attribute[ATTR_HEAVENLY_STORM];
return ret;
}
// Checks each equip slot for a randart, and adds up all of those with
// a given property. Slow if any randarts are worn, so avoid where
// possible. If `matches' is non-nullptr, items with nonzero property are
// pushed onto *matches.
int player::scan_artefacts(artefact_prop_type which_property,
bool calc_unid,
vector<item_def> *matches) const
{
int retval = 0;
for (int i = EQ_FIRST_EQUIP; i < NUM_EQUIP; ++i)
{
if (melded[i] || equip[i] == -1)
continue;
const int eq = equip[i];
// Only weapons give their effects when in our hands.
if (i == EQ_WEAPON && inv[ eq ].base_type != OBJ_WEAPONS)
continue;
if (!is_artefact(inv[ eq ]))
continue;
bool known;
int val = artefact_property(inv[eq], which_property, known);
if (calc_unid || known)
{
retval += val;
if (matches && val)
matches->push_back(inv[eq]);
}
}
return retval;
}
void dec_hp(int hp_loss, bool fatal, const char *aux)
{
ASSERT(!crawl_state.game_is_arena());
if (!fatal && you.hp < 1)
you.hp = 1;
if (!fatal && hp_loss >= you.hp)
hp_loss = you.hp - 1;
if (hp_loss < 1)
return;
// If it's not fatal, use ouch() so that notes can be taken. If it IS
// fatal, somebody else is doing the bookkeeping, and we don't want to mess
// with that.
if (!fatal && aux)
ouch(hp_loss, KILLED_BY_SOMETHING, MID_NOBODY, aux);
else
you.hp -= hp_loss;
you.redraw_hit_points = true;
}
void calc_mp()
{
you.max_magic_points = get_real_mp(true);
you.magic_points = min(you.magic_points, you.max_magic_points);
you.redraw_magic_points = true;
}
void flush_mp()
{
if (Options.magic_point_warning
&& you.magic_points < you.max_magic_points
* Options.magic_point_warning / 100)
{
mprf(MSGCH_DANGER, "* * * LOW MAGIC WARNING * * *");
}
take_note(Note(NOTE_MP_CHANGE, you.magic_points, you.max_magic_points));
you.redraw_magic_points = true;
}
void dec_mp(int mp_loss, bool silent)
{
ASSERT(!crawl_state.game_is_arena());
if (mp_loss < 1)
return;
you.magic_points -= mp_loss;
you.magic_points = max(0, you.magic_points);
if (!silent)
flush_mp();
}
bool enough_hp(int minimum, bool suppress_msg, bool abort_macros)
{
ASSERT(!crawl_state.game_is_arena());
if (you.duration[DUR_DEATHS_DOOR])
{
if (!suppress_msg)
mpr("You cannot pay life while functionally dead.");
if (abort_macros)
{
crawl_state.cancel_cmd_again();
crawl_state.cancel_cmd_repeat();
}
return false;
}
// We want to at least keep 1 HP. -- bwr
if (you.hp < minimum + 1)
{
if (!suppress_msg)
mpr("You don't have enough health at the moment.");
if (abort_macros)
{
crawl_state.cancel_cmd_again();
crawl_state.cancel_cmd_repeat();
}
return false;
}
return true;
}
bool enough_mp(int minimum, bool suppress_msg, bool abort_macros)
{
ASSERT(!crawl_state.game_is_arena());
if (you.magic_points < minimum)
{
if (!suppress_msg)
{
if (get_real_mp(true) < minimum)
mpr("You don't have enough magic capacity.");
else
mpr("You don't have enough magic at the moment.");
}
if (abort_macros)
{
crawl_state.cancel_cmd_again();
crawl_state.cancel_cmd_repeat();
}
return false;
}
return true;
}
static int _rest_trigger_level(int max)
{
return (max * Options.rest_wait_percent) / 100;
}
static bool _should_stop_resting(int cur, int max)
{
return cur == max || cur == _rest_trigger_level(max);
}
void inc_mp(int mp_gain, bool silent)
{
ASSERT(!crawl_state.game_is_arena());
if (mp_gain < 1 || you.magic_points >= you.max_magic_points)
return;
you.magic_points += mp_gain;
if (you.magic_points > you.max_magic_points)
you.magic_points = you.max_magic_points;
if (!silent)
{
if (_should_stop_resting(you.magic_points, you.max_magic_points))
interrupt_activity(activity_interrupt::full_mp);
you.redraw_magic_points = true;
}
}
// Note that "max_too" refers to the base potential, the actual
// resulting max value is subject to penalties, bonuses, and scalings.
// To avoid message spam, don't take notes when HP increases.
void inc_hp(int hp_gain)
{
ASSERT(!crawl_state.game_is_arena());
if (hp_gain < 1 || you.hp >= you.hp_max)
return;
you.hp += hp_gain;
if (you.hp > you.hp_max)
you.hp = you.hp_max;
if (_should_stop_resting(you.hp, you.hp_max))
interrupt_activity(activity_interrupt::full_hp);
you.redraw_hit_points = true;
}
void rot_hp(int hp_loss)
{
if (!player_rotted() && hp_loss > 0)
you.redraw_magic_points = true;
const int initial_rot = you.hp_max_adj_temp;
you.hp_max_adj_temp -= hp_loss;
// don't allow more rot than you have normal mhp
you.hp_max_adj_temp = max(-(get_real_hp(false, false) - 1),
you.hp_max_adj_temp);
if (initial_rot == you.hp_max_adj_temp)
return;
calc_hp();
if (you.species != SP_GHOUL)
xom_is_stimulated(hp_loss * 25);
you.redraw_hit_points = true;
}
int unrot_hp(int hp_recovered)
{
int hp_balance = 0;
if (hp_recovered > -you.hp_max_adj_temp)
{
hp_balance = hp_recovered + you.hp_max_adj_temp;
you.hp_max_adj_temp = 0;
}
else
you.hp_max_adj_temp += hp_recovered;
calc_hp();
you.redraw_hit_points = true;
if (!player_rotted())
you.redraw_magic_points = true;
return hp_balance;
}
int player_rotted()
{
return -you.hp_max_adj_temp;
}
void rot_mp(int mp_loss)
{
you.mp_max_adj -= mp_loss;
calc_mp();
you.redraw_magic_points = true;
}
void inc_max_hp(int hp_gain)
{
you.hp += hp_gain;
you.hp_max_adj_perm += hp_gain;
calc_hp();
take_note(Note(NOTE_MAXHP_CHANGE, you.hp_max));
you.redraw_hit_points = true;
}
void dec_max_hp(int hp_loss)
{
you.hp_max_adj_perm -= hp_loss;
calc_hp();
take_note(Note(NOTE_MAXHP_CHANGE, you.hp_max));
you.redraw_hit_points = true;
}
void set_hp(int new_amount)
{
ASSERT(!crawl_state.game_is_arena());
you.hp = new_amount;
if (you.hp > you.hp_max)
you.hp = you.hp_max;
// Must remain outside conditional, given code usage. {dlb}
you.redraw_hit_points = true;
}
void set_mp(int new_amount)
{
ASSERT(!crawl_state.game_is_arena());
you.magic_points = new_amount;
if (you.magic_points > you.max_magic_points)
you.magic_points = you.max_magic_points;
take_note(Note(NOTE_MP_CHANGE, you.magic_points, you.max_magic_points));
// Must remain outside conditional, given code usage. {dlb}
you.redraw_magic_points = true;
}
/**
* Get the player's max HP
* @param trans Whether to include transformations, berserk,
* items etc.
* @param rotted Whether to include the effects of rotting.
* @return The player's calculated max HP.
*/
int get_real_hp(bool trans, bool rotted)
{
int hitp;
hitp = you.experience_level * 11 / 2 + 8;
hitp += you.hp_max_adj_perm;
// Important: we shouldn't add Heroism boosts here.
hitp += you.experience_level * you.skill(SK_FIGHTING, 5, true) / 70
+ (you.skill(SK_FIGHTING, 3, true) + 1) / 2;
// Racial modifier.
hitp *= 10 + species_hp_modifier(you.species);
hitp /= 10;
const bool hep_frail = have_passive(passive_t::frail)
|| player_under_penance(GOD_HEPLIAKLQANA);
// Mutations that increase HP by a percentage
hitp *= 100 + (you.get_mutation_level(MUT_ROBUST) * 10)
+ (you.attribute[ATTR_DIVINE_VIGOUR] * 5)
+ (you.get_mutation_level(MUT_RUGGED_BROWN_SCALES) ?
you.get_mutation_level(MUT_RUGGED_BROWN_SCALES) * 2 + 1 : 0)
- (you.get_mutation_level(MUT_FRAIL) * 10)
- (hep_frail ? 10 : 0)
- (!you.vampire_alive ? 20 : 0);
hitp /= 100;
if (rotted)
hitp += you.hp_max_adj_temp;
if (trans)
hitp += you.scan_artefacts(ARTP_HP);
// Being berserk makes you resistant to damage. I don't know why.
if (trans && you.berserk())
hitp = hitp * 3 / 2;
// Some transformations give you extra hp.
if (trans)
hitp = hitp * form_hp_mod() / 10;
#if TAG_MAJOR_VERSION == 34
if (trans && player_equip_unrand(UNRAND_ETERNAL_TORMENT))
hitp = hitp * 4 / 5;
#endif
return max(1, hitp);
}
int get_real_mp(bool include_items)
{
const int scale = 100;
int spellcasting = you.skill(SK_SPELLCASTING, 1 * scale, true);
int scaled_xl = you.experience_level * scale;
// the first 4 experience levels give an extra .5 mp up to your spellcasting
// the last 4 give no mp
int enp = min(23 * scale, scaled_xl);
int spell_extra = spellcasting; // 100%
int invoc_extra = you.skill(SK_INVOCATIONS, 1 * scale, true) / 2; // 50%
int evoc_extra = you.skill(SK_EVOCATIONS, 1 * scale, true) / 2; // 50%
int highest_skill = max(spell_extra, max(invoc_extra, evoc_extra));
enp += highest_skill + min(8 * scale, min(highest_skill, scaled_xl)) / 2;
// Analogous to ROBUST/FRAIL
enp *= 100 + (you.get_mutation_level(MUT_HIGH_MAGIC) * 10)
+ (you.attribute[ATTR_DIVINE_VIGOUR] * 5)
- (you.get_mutation_level(MUT_LOW_MAGIC) * 10);
enp /= 100 * scale;
// enp = stepdown_value(enp, 9, 18, 45, 100)
enp += species_mp_modifier(you.species);
// This is our "rotted" base, applied after multipliers
enp += you.mp_max_adj;
// Now applied after scaling so that power items are more useful -- bwr
if (include_items)
{
enp += 9 * you.wearing(EQ_RINGS, RING_MAGICAL_POWER);
enp += you.scan_artefacts(ARTP_MAGICAL_POWER);
if (you.wearing(EQ_STAFF, STAFF_POWER))
enp += 15;
}
if (include_items && you.wearing_ego(EQ_WEAPON, SPWPN_ANTIMAGIC))
enp /= 3;
enp = max(enp, 0);
return enp;
}
bool player_regenerates_hp()
{
if (you.has_mutation(MUT_NO_REGENERATION))
return false;
return true;
}
bool player_regenerates_mp()
{
// Don't let DD use guardian spirit for free HP, since their
// damage shaving is enough. (due, dpeg)
if (you.spirit_shield() && you.species == SP_DEEP_DWARF)
return false;
#if TAG_MAJOR_VERSION == 34
// Pakellas blocks MP regeneration.
if (have_passive(passive_t::no_mp_regen) || player_under_penance(GOD_PAKELLAS))
return false;
#endif
return true;
}
int get_contamination_level()
{
const int glow = you.magic_contamination;
if (glow > 60000)
return glow / 20000 + 4;
if (glow > 40000)
return 6;
if (glow > 25000)
return 5;
if (glow > 15000)
return 4;
if (glow > 5000)
return 3;
if (glow > 3500) // An indicator that using another contamination-causing