#include "AppHdr.h" #include "player-stats.h" #include "artefact.h" #include "clua.h" #include "delay.h" #include "godpassive.h" #include "files.h" #include "item_use.h" #include "libutil.h" #include "macro.h" #include "misc.h" #include "mon-util.h" #include "monster.h" #include "notes.h" #include "ouch.h" #include "player.h" #include "religion.h" #include "state.h" #include "strings.h" #include "transform.h" #include "hints.h" #ifdef TOUCH_UI #include "menu.h" #include "tiledef-gui.h" #include "tilepick.h" #endif // Don't make this larger than 255 without changing the type of you.stat_zero // in player.h as well as the associated marshalling code in tags.cc const int STATZERO_TURN_CAP = 200; int player::stat(stat_type s, bool nonneg) const { const int val = max_stat(s) - stat_loss[s]; return nonneg ? max(val, 0) : val; } int player::strength(bool nonneg) const { return stat(STAT_STR, nonneg); } int player::intel(bool nonneg) const { return stat(STAT_INT, nonneg); } int player::dex(bool nonneg) const { return stat(STAT_DEX, nonneg); } static int _stat_modifier(stat_type stat); int player::max_stat(stat_type s) const { return min(base_stats[s] + _stat_modifier(s), 72); } int player::max_strength() const { return max_stat(STAT_STR); } int player::max_intel() const { return max_stat(STAT_INT); } int player::max_dex() const { return max_stat(STAT_DEX); } static void _handle_stat_change(stat_type stat, const char *aux = NULL, bool see_source = true); static void _handle_stat_change(const char *aux = NULL, bool see_source = true); bool attribute_increase() { crawl_state.stat_gain_prompt = true; #ifdef TOUCH_UI learned_something_new(HINT_CHOOSE_STAT); Popup *pop = new Popup("Increase Attributes"); MenuEntry *status = new MenuEntry("", MEL_SUBTITLE); pop->push_entry(new MenuEntry("Your experience leads to an increase in " "your attributes! Increase:", MEL_TITLE)); pop->push_entry(status); MenuEntry *me = new MenuEntry("Strength", MEL_ITEM, 0, 'S', false); me->add_tile(tile_def(TILEG_FIGHTING_ON, TEX_GUI)); pop->push_entry(me); me = new MenuEntry("Intelligence", MEL_ITEM, 0, 'I', false); me->add_tile(tile_def(TILEG_SPELLCASTING_ON, TEX_GUI)); pop->push_entry(me); me = new MenuEntry("Dexterity", MEL_ITEM, 0, 'D', false); me->add_tile(tile_def(TILEG_DODGING_ON, TEX_GUI)); pop->push_entry(me); #else mprf(MSGCH_INTRINSIC_GAIN, "Your experience leads to an increase in your attributes!"); learned_something_new(HINT_CHOOSE_STAT); mprf(MSGCH_PROMPT, "Increase (S)trength, (I)ntelligence, or (D)exterity? "); #endif mouse_control mc(MOUSE_MODE_PROMPT); bool tried_lua = false; int keyin; while (true) { // Calling a user-defined lua function here to let players reply to // the prompt automatically. Either returning a string or using // crawl.sendkeys will work. if (!tried_lua && clua.callfn("choose_stat_gain", 0, 1)) { string result; clua.fnreturns(">s", &result); keyin = result[0]; } else { #ifdef TOUCH_UI keyin = pop->pop(); #else keyin = getchm(); #endif } tried_lua = true; switch (keyin) { CASE_ESCAPE // It is unsafe to save the game here; continue with the turn // normally, when the player reloads, the game will re-prompt // for their level-up stat gain. if (crawl_state.seen_hups) return false; break; case 's': case 'S': modify_stat(STAT_STR, 1, false, "level gain"); return true; case 'i': case 'I': modify_stat(STAT_INT, 1, false, "level gain"); return true; case 'd': case 'D': modify_stat(STAT_DEX, 1, false, "level gain"); return true; #ifdef TOUCH_UI default: status->text = "Please choose an option below"; // too naggy? #endif } } } /* * Have Jiyva increase a player stat by one and decrease a different stat by * one. * * This considers armour evp and skills to determine which stats to change. A * target stat vector is created based on these factors, which is then fuzzed, * and then a shuffle of the player's stat points that doesn't increase the l^2 * distance to the target vector is chosen. */ void jiyva_stat_action() { int cur_stat[3]; int stat_total = 0; int target_stat[3]; for (int x = 0; x < 3; ++x) { cur_stat[x] = you.stat(static_cast(x), false); stat_total += cur_stat[x]; } int evp = you.unadjusted_body_armour_penalty(); target_stat[0] = max(9, evp); target_stat[1] = 9; target_stat[2] = 9; int remaining = stat_total - 18 - target_stat[0]; // Divide up the remaining stat points between Int and either Str or Dex, // based on skills. if (remaining > 0) { int magic_weights = 0; int other_weights = 0; for (int i = SK_FIRST_SKILL; i < NUM_SKILLS; i++) { skill_type sk = static_cast(i); int weight = you.skills[sk]; if (sk >= SK_SPELLCASTING && sk < SK_INVOCATIONS) magic_weights += weight; else other_weights += weight; } // Heavy armour weights towards strength, Dodging skill towards // dexterity. EVP 15 (chain) is enough to weight towards pure Str in // the absence of dodging skill, but 15 dodging will will push that // back to pure Dex. int str_weight = (10 * evp - you.skill(SK_DODGING, 10)) / 15; // Clip weight between 0 (pure dex) and 10 (pure strength). str_weight = min(10, max(0, str_weight)); // If you are in really heavy armour, then you already are getting a // lot of Str and more won't help much, so weight magic more. other_weights = max(other_weights - (evp >= 15 ? 4 : 1) * magic_weights / 2, 0); magic_weights = div_rand_round(remaining * magic_weights, magic_weights + other_weights); other_weights = remaining - magic_weights; target_stat[1] += magic_weights; const int str_adj = div_rand_round(other_weights * str_weight, 10); target_stat[0] += str_adj; target_stat[2] += (other_weights - str_adj); } // Add a little fuzz to the target. for (int x = 0; x < 3; ++x) target_stat[x] += random2(5) - 2; int choices = 0; int stat_up_choice = 0; int stat_down_choice = 0; // Choose a random stat shuffle that doesn't increase the l^2 distance to // the (fuzzed) target. for (int x = 0; x < 3; ++x) for (int y = 0; y < 3; ++y) { if (x != y && cur_stat[y] > 1 && target_stat[x] - cur_stat[x] > target_stat[y] - cur_stat[y] && cur_stat[x] < 72) { choices++; if (one_chance_in(choices)) { stat_up_choice = x; stat_down_choice = y; } } } if (choices) { simple_god_message("'s power touches on your attributes."); const string cause = "the 'helpfulness' of " + god_name(you.religion); modify_stat(static_cast(stat_up_choice), 1, true, cause.c_str()); modify_stat(static_cast(stat_down_choice), -1, true, cause.c_str()); } } static kill_method_type _statloss_killtype(stat_type stat) { switch (stat) { case STAT_STR: return KILLED_BY_WEAKNESS; case STAT_INT: return KILLED_BY_STUPIDITY; case STAT_DEX: return KILLED_BY_CLUMSINESS; default: die("unknown stat"); } } static const char* descs[NUM_STATS][NUM_STAT_DESCS] = { { "strength", "weakened", "weaker", "stronger" }, { "intelligence", "dopey", "stupid", "clever" }, { "dexterity", "clumsy", "clumsy", "agile" } }; const char* stat_desc(stat_type stat, stat_desc_type desc) { return descs[stat][desc]; } void modify_stat(stat_type which_stat, int amount, bool suppress_msg, const char *cause, bool see_source) { ASSERT(!crawl_state.game_is_arena()); // sanity - is non-zero amount? if (amount == 0) return; // Stop delays if a stat drops. if (amount < 0) interrupt_activity(AI_STAT_CHANGE); if (which_stat == STAT_RANDOM) which_stat = static_cast(random2(NUM_STATS)); if (!suppress_msg) { mprf((amount > 0) ? MSGCH_INTRINSIC_GAIN : MSGCH_WARN, "You feel %s.", stat_desc(which_stat, (amount > 0) ? SD_INCREASE : SD_DECREASE)); } you.base_stats[which_stat] += amount; _handle_stat_change(which_stat, cause, see_source); } void notify_stat_change(stat_type which_stat, int amount, bool suppress_msg, const char *cause, bool see_source) { ASSERT(!crawl_state.game_is_arena()); // sanity - is non-zero amount? if (amount == 0) return; // Stop delays if a stat drops. if (amount < 0) interrupt_activity(AI_STAT_CHANGE); if (which_stat == STAT_RANDOM) which_stat = static_cast(random2(NUM_STATS)); if (!suppress_msg) { mprf((amount > 0) ? MSGCH_INTRINSIC_GAIN : MSGCH_WARN, "You feel %s.", stat_desc(which_stat, (amount > 0) ? SD_INCREASE : SD_DECREASE)); } _handle_stat_change(which_stat, cause, see_source); } void notify_stat_change(stat_type which_stat, int amount, bool suppress_msg, const item_def &cause, bool removed) { string name = cause.name(DESC_THE, false, true, false, false, ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES); string verb; switch (cause.base_type) { case OBJ_ARMOUR: case OBJ_JEWELLERY: if (removed) verb = "removing"; else verb = "wearing"; break; case OBJ_WEAPONS: case OBJ_STAVES: case OBJ_RODS: if (removed) verb = "unwielding"; else verb = "wielding"; break; case OBJ_WANDS: verb = "zapping"; break; case OBJ_FOOD: verb = "eating"; break; case OBJ_SCROLLS: verb = "reading"; break; case OBJ_POTIONS: verb = "drinking"; break; default: verb = "using"; } notify_stat_change(which_stat, amount, suppress_msg, (verb + " " + name).c_str(), true); } void notify_stat_change(const char* cause) { _handle_stat_change(cause); } static int _strength_modifier() { int result = 0; if (you.duration[DUR_MIGHT] || you.duration[DUR_BERSERK]) result += 5; if (you.duration[DUR_FORTITUDE]) result += 10; if (you.duration[DUR_DIVINE_STAMINA]) result += you.attribute[ATTR_DIVINE_STAMINA]; result += chei_stat_boost(); // ego items of strength result += 3 * count_worn_ego(SPARM_STRENGTH); // rings of strength result += you.wearing(EQ_RINGS_PLUS, RING_STRENGTH); // randarts of strength result += you.scan_artefacts(ARTP_STRENGTH); // mutations result += 2 * (player_mutation_level(MUT_STRONG) - player_mutation_level(MUT_WEAK)); #if TAG_MAJOR_VERSION == 34 result += player_mutation_level(MUT_STRONG_STIFF) - player_mutation_level(MUT_FLEXIBLE_WEAK); #endif // transformations switch (you.form) { case TRAN_STATUE: result += 2; break; case TRAN_DRAGON: result += 10; break; case TRAN_BAT: result -= 5; break; default: break; } return result; } static int _int_modifier() { int result = 0; if (you.duration[DUR_BRILLIANCE]) result += 5; if (you.duration[DUR_DIVINE_STAMINA]) result += you.attribute[ATTR_DIVINE_STAMINA]; result += chei_stat_boost(); // ego items of intelligence result += 3 * count_worn_ego(SPARM_INTELLIGENCE); // rings of intelligence result += you.wearing(EQ_RINGS_PLUS, RING_INTELLIGENCE); // randarts of intelligence result += you.scan_artefacts(ARTP_INTELLIGENCE); // mutations result += 2 * (player_mutation_level(MUT_CLEVER) - player_mutation_level(MUT_DOPEY)); return result; } static int _dex_modifier() { int result = 0; if (you.duration[DUR_AGILITY]) result += 5; if (you.duration[DUR_DIVINE_STAMINA]) result += you.attribute[ATTR_DIVINE_STAMINA]; result += chei_stat_boost(); // ego items of dexterity result += 3 * count_worn_ego(SPARM_DEXTERITY); // rings of dexterity result += you.wearing(EQ_RINGS_PLUS, RING_DEXTERITY); // randarts of dexterity result += you.scan_artefacts(ARTP_DEXTERITY); // mutations result += 2 * (player_mutation_level(MUT_AGILE) - player_mutation_level(MUT_CLUMSY)); #if TAG_MAJOR_VERSION == 34 result += player_mutation_level(MUT_FLEXIBLE_WEAK) - player_mutation_level(MUT_STRONG_STIFF); #endif result += 2 * player_mutation_level(MUT_THIN_SKELETAL_STRUCTURE); result -= player_mutation_level(MUT_ROUGH_BLACK_SCALES); // transformations switch (you.form) { case TRAN_SPIDER: result += 5; break; case TRAN_STATUE: result -= 2; break; case TRAN_BAT: result += 5; break; default: break; } return result; } static int _stat_modifier(stat_type stat) { switch (stat) { case STAT_STR: return _strength_modifier(); case STAT_INT: return _int_modifier(); case STAT_DEX: return _dex_modifier(); default: mprf(MSGCH_ERROR, "Bad stat: %d", stat); return 0; } } static string _stat_name(stat_type stat) { switch (stat) { case STAT_STR: return "strength"; case STAT_INT: return "intelligence"; case STAT_DEX: return "dexterity"; default: die("invalid stat"); } } bool lose_stat(stat_type which_stat, int stat_loss, bool force, const char *cause, bool see_source) { if (which_stat == STAT_RANDOM) which_stat = static_cast(random2(NUM_STATS)); // scale modifier by player_sust_abil() - right-shift // permissible because stat_loss is unsigned: {dlb} if (!force) { if (you.duration[DUR_DIVINE_STAMINA] > 0) { mprf("Your divine stamina protects you from %s loss.", _stat_name(which_stat).c_str()); return false; } int sust = player_sust_abil(); stat_loss >>= sust; } mprf(stat_loss > 0 ? MSGCH_WARN : MSGCH_PLAIN, "You feel %s%s%s.", stat_loss > 0 && player_sust_abil(false) ? "somewhat " : "", stat_desc(which_stat, SD_LOSS), stat_loss > 0 ? "" : " for a moment"); if (stat_loss > 0) { you.stat_loss[which_stat] = min(100, you.stat_loss[which_stat] + stat_loss); if (you.stat_zero[which_stat]) { mprf(MSGCH_DANGER, "You convulse from lack of %s!", stat_desc(which_stat, SD_NAME)); ouch(5 + random2(you.hp_max / 10), NON_MONSTER, _statloss_killtype(which_stat), cause); } _handle_stat_change(which_stat, cause, see_source); return true; } else return false; } bool lose_stat(stat_type which_stat, int stat_loss, bool force, const string cause, bool see_source) { return lose_stat(which_stat, stat_loss, force, cause.c_str(), see_source); } bool lose_stat(stat_type which_stat, int stat_loss, const monster* cause, bool force) { if (cause == NULL || invalid_monster(cause)) return lose_stat(which_stat, stat_loss, force, NULL, true); bool vis = you.can_see(cause); string name = cause->name(DESC_A, true); if (cause->has_ench(ENCH_SHAPESHIFTER)) name += " (shapeshifter)"; else if (cause->has_ench(ENCH_GLOWING_SHAPESHIFTER)) name += " (glowing shapeshifter)"; return lose_stat(which_stat, stat_loss, force, name, vis); } static stat_type _random_lost_stat() { stat_type choice = NUM_STATS; int found = 0; for (int i = 0; i < NUM_STATS; ++i) if (you.stat_loss[i] > 0) { found++; if (one_chance_in(found)) choice = static_cast(i); } return choice; } // Restore the stat in which_stat by the amount in stat_gain, displaying // a message if suppress_msg is false, and doing so in the recovery // channel if recovery is true. If stat_gain is 0, restore the stat // completely. bool restore_stat(stat_type which_stat, int stat_gain, bool suppress_msg, bool recovery) { // A bit hackish, but cut me some slack, man! -- // Besides, a little recursion never hurt anyone {dlb}: if (which_stat == STAT_ALL) { bool stat_restored = false; for (int i = 0; i < NUM_STATS; ++i) if (restore_stat((stat_type) i, stat_gain, suppress_msg)) stat_restored = true; return stat_restored; } if (which_stat == STAT_RANDOM) which_stat = _random_lost_stat(); if (which_stat >= NUM_STATS || you.stat_loss[which_stat] == 0) return false; if (!suppress_msg) { mprf(recovery ? MSGCH_RECOVERY : MSGCH_PLAIN, "You feel your %s returning.", _stat_name(which_stat).c_str()); } if (stat_gain == 0 || stat_gain > you.stat_loss[which_stat]) stat_gain = you.stat_loss[which_stat]; you.stat_loss[which_stat] -= stat_gain; _handle_stat_change(which_stat); return true; } static void _normalize_stat(stat_type stat) { ASSERT(you.stat_loss[stat] >= 0); // XXX: this doesn't prevent effective stats over 72. you.base_stats[stat] = min(you.base_stats[stat], 72); } static void _handle_stat_change(stat_type stat, const char* cause, bool see_source) { ASSERT_RANGE(stat, 0, NUM_STATS); if (you.stat(stat) <= 0 && you.stat_zero[stat] == 0) { // Turns required for recovery once the stat is restored, randomised slightly. you.stat_zero[stat] = 10 + random2(10); mprf(MSGCH_WARN, "You have lost your %s.", stat_desc(stat, SD_NAME)); take_note(Note(NOTE_MESSAGE, 0, 0, make_stringf("Lost %s.", stat_desc(stat, SD_NAME)).c_str()), true); // 2 to 5 turns of paralysis (XXX: decremented right away?) you.increase_duration(DUR_PARALYSIS, 2 + random2(3)); } you.redraw_stats[stat] = true; _normalize_stat(stat); switch (stat) { case STAT_STR: you.redraw_armour_class = true; // includes shields you.redraw_evasion = true; // Might reduce EV penalty break; case STAT_INT: break; case STAT_DEX: you.redraw_evasion = true; you.redraw_armour_class = true; // includes shields break; default: break; } } static void _handle_stat_change(const char* aux, bool see_source) { for (int i = 0; i < NUM_STATS; ++i) _handle_stat_change(static_cast(i), aux, see_source); } // Called once per turn. void update_stat_zero() { for (int i = 0; i < NUM_STATS; ++i) { stat_type s = static_cast(i); if (you.stat(s) <= 0) { if (you.stat_zero[s] < STATZERO_TURN_CAP) you.stat_zero[s]++; } else if (you.stat_zero[s]) { you.stat_zero[s]--; if (you.stat_zero[s] == 0) { mprf("Your %s has recovered.", stat_desc(s, SD_NAME)); you.redraw_stats[s] = true; } } else // no stat penalty at all continue; } }