/* * File: xom.cc * Summary: All things Xomly * Written by: Zooko */ #include "AppHdr.h" #include #include "artefact.h" #include "beam.h" #include "branch.h" #include "coordit.h" #include "database.h" #ifdef WIZARD #include "dbg-util.h" #endif #include "delay.h" #include "directn.h" #include "effects.h" #include "env.h" #include "map_knowledge.h" #include "feature.h" #include "goditem.h" #include "it_use2.h" #include "itemprop.h" #include "items.h" #include "kills.h" #include "los.h" #include "makeitem.h" #include "message.h" #include "misc.h" #include "mon-behv.h" #include "mon-iter.h" #include "mon-util.h" #include "mon-place.h" #include "mgen_data.h" #include "coord.h" #include "mon-stuff.h" #include "mutation.h" #include "notes.h" #include "options.h" #include "ouch.h" #include "item_use.h" // for safe_to_remove_or_wear() #include "output.h" // for the monster list #include "player.h" #include "religion.h" #include "spells2.h" #include "spells3.h" #include "spl-book.h" #include "spl-cast.h" #include "spl-mis.h" #include "spl-util.h" #include "stash.h" #include "state.h" #include "stuff.h" #include "areas.h" #include "teleport.h" #include "terrain.h" #include "transform.h" #include "traps.h" #include "travel.h" #include "view.h" #include "viewchar.h" #include "xom.h" #ifdef DEBUG_XOM # define DEBUG_RELIGION 1 # define NOTE_DEBUG_XOM 1 #endif #ifdef DEBUG_RELIGION # define DEBUG_DIAGNOSTICS 1 # define DEBUG_GIFTS 1 #endif // Which spells? First I copied all spells from your_spells(), and then // I filtered some out, especially conjurations. Then I sorted them in // roughly ascending order of power. // Define fake magic mapping spell to keep the old behaviour. #define FAKE_SPELL_MAGIC_MAPPING SPELL_NO_SPELL // Spells to be cast at tension 0 (no or only low-level monsters around), // mostly flavour. static const spell_type _xom_nontension_spells[] = { FAKE_SPELL_MAGIC_MAPPING, SPELL_DETECT_ITEMS, SPELL_SUMMON_BUTTERFLIES, SPELL_DETECT_CREATURES, SPELL_FLY, SPELL_SPIDER_FORM, SPELL_STATUE_FORM, SPELL_ICE_FORM, SPELL_DRAGON_FORM, SPELL_NECROMUTATION }; // Spells to be cast at tension > 0, i.e. usually in battle situations. static const spell_type _xom_tension_spells[] = { SPELL_BLINK, SPELL_CONFUSING_TOUCH, SPELL_CAUSE_FEAR, SPELL_ENGLACIATION, SPELL_DISPERSAL, SPELL_STONESKIN, SPELL_RING_OF_FLAMES, SPELL_OLGREBS_TOXIC_RADIANCE, SPELL_TUKIMAS_VORPAL_BLADE, SPELL_MAXWELLS_SILVER_HAMMER, SPELL_FIRE_BRAND, SPELL_FREEZING_AURA, SPELL_POISON_WEAPON, SPELL_STONEMAIL, SPELL_LETHAL_INFUSION, SPELL_EXCRUCIATING_WOUNDS, SPELL_WARP_BRAND, SPELL_TUKIMAS_DANCE, SPELL_RECALL, SPELL_SUMMON_BUTTERFLIES, SPELL_SUMMON_SMALL_MAMMALS, SPELL_SUMMON_SCORPIONS, SPELL_SUMMON_SWARM, SPELL_FLY, SPELL_SPIDER_FORM, SPELL_STATUE_FORM, SPELL_ICE_FORM, SPELL_DRAGON_FORM, SPELL_ANIMATE_DEAD, SPELL_SHADOW_CREATURES, SPELL_SUMMON_HORRIBLE_THINGS, SPELL_CALL_CANINE_FAMILIAR, SPELL_SUMMON_ICE_BEAST, SPELL_SUMMON_UGLY_THING, SPELL_CONJURE_BALL_LIGHTNING, SPELL_SUMMON_DRAGON, SPELL_DEATH_CHANNEL, SPELL_NECROMUTATION }; static const char *_xom_message_arrays[NUM_XOM_MESSAGE_TYPES][6] = { // XM_NORMAL { "Xom is interested.", "Xom is mildly amused.", "Xom is amused.", "Xom is highly amused!", "Xom thinks this is hilarious!", "Xom roars with laughter!" }, // XM_INTRIGUED { "Xom is interested.", "Xom is very interested.", "Xom is extremely interested.", "Xom is intrigued!", "Xom is very intrigued!", "Xom is fascinated!" } }; static const char *describe_xom_mood() { return (you.piety > 180) ? "Xom's teddy bear." : (you.piety > 150) ? "a beloved toy of Xom." : (you.piety > 120) ? "a favourite toy of Xom." : (you.piety > 80) ? "a toy of Xom." : (you.piety > 50) ? "a plaything of Xom." : (you.piety > 20) ? "a special plaything of Xom." : "a very special plaything of Xom."; } const char *describe_xom_favour(bool upper) { std::string favour; if (you.religion != GOD_XOM) favour = "a very buggy toy of Xom."; else if (you.gift_timeout < 1) favour = "a BORING thing."; else favour = describe_xom_mood(); if (upper) favour = uppercase_first(favour); return (favour.c_str()); } static std::string _get_xom_speech(const std::string key) { std::string result = getSpeakString("Xom " + key); if (result.empty()) result = getSpeakString("Xom general effect"); if (result.empty()) return ("Xom makes something happen."); return (result); } static bool _xom_is_bored() { return (you.religion == GOD_XOM && you.gift_timeout == 0); } static bool _xom_feels_nasty() { // Xom will only directly kill you with a bad effect if you're under // penance from him, or if he's bored. return (you.penance[GOD_XOM] || _xom_is_bored()); } bool xom_is_nice(int tension) { if (you.penance[GOD_XOM]) return (false); if (you.religion == GOD_XOM) { // If you.gift_timeout is 0, then Xom is BORED. He HATES that. if (you.gift_timeout == 0) return (false); // At high tension Xom is more likely to be nice, at zero // tension the opposite. const int tension_bonus = (tension == -1 ? 0 : tension == 0 ? -std::min(abs(HALF_MAX_PIETY - you.piety) / 2, you.piety / 10) : std::min((MAX_PIETY - you.piety) / 2, random2(tension))); const int effective_piety = you.piety + tension_bonus; ASSERT(effective_piety >= 0 && effective_piety <= MAX_PIETY); #ifdef DEBUG_XOM mprf(MSGCH_DIAGNOSTICS, "Xom: tension: %d, piety: %d -> tension bonus = %d, eff. piety: %d", tension, you.piety, tension_bonus, effective_piety); #endif // Whether Xom is nice depends largely on his mood (== piety). return (x_chance_in_y(effective_piety, MAX_PIETY)); } else // CARD_XOM return coinflip(); } static void _xom_is_stimulated(int maxinterestingness, const char *message_array[], bool force_message) { if (you.religion != GOD_XOM || maxinterestingness <= 0) return; // Xom is not directly stimulated by his own acts. if (crawl_state.which_god_acting() == GOD_XOM) return; int interestingness = random2(piety_scale(maxinterestingness)); interestingness = std::min(255, interestingness); #if defined(DEBUG_RELIGION) || defined(DEBUG_GIFTS) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "Xom: gift_timeout: %d, maxinterestingness = %d, interestingness = %d", you.gift_timeout, maxinterestingness, interestingness); #endif bool was_stimulated = false; if (interestingness > you.gift_timeout && interestingness >= 12) { you.gift_timeout = interestingness; was_stimulated = true; } if (was_stimulated || force_message) { god_speaks(GOD_XOM, ((interestingness > 200) ? message_array[5] : (interestingness > 100) ? message_array[4] : (interestingness > 75) ? message_array[3] : (interestingness > 50) ? message_array[2] : (interestingness > 25) ? message_array[1] : message_array[0])); } } void xom_is_stimulated(int maxinterestingness, xom_message_type message_type, bool force_message) { _xom_is_stimulated(maxinterestingness, _xom_message_arrays[message_type], force_message); } void xom_is_stimulated(int maxinterestingness, const std::string& message, bool force_message) { if (you.religion != GOD_XOM) return; const char *message_array[6]; for (int i = 0; i < 6; ++i) message_array[i] = message.c_str(); _xom_is_stimulated(maxinterestingness, message_array, force_message); } void xom_tick() { // Xom semi-randomly drifts your piety. const std::string old_xom_favour = describe_xom_favour(); const bool good = (you.piety == HALF_MAX_PIETY? coinflip() : you.piety > HALF_MAX_PIETY); int size = abs(you.piety - HALF_MAX_PIETY); // Piety slowly drifts towards the extremes. const int delta = piety_scale(x_chance_in_y(511, 1000) ? 1 : -1); size += delta; if (size > HALF_MAX_PIETY) size = HALF_MAX_PIETY; you.piety = HALF_MAX_PIETY + (good ? size : -size); std::string new_xom_favour = describe_xom_favour(); if (old_xom_favour != new_xom_favour) { // If we entered another favour state, take a big step into // the new territory, to avoid oscillating favour announcements // every few turns. size += delta * 8; if (size > HALF_MAX_PIETY) size = HALF_MAX_PIETY; // If size was 0 to begin with, it may become negative, but that // doesn't really matter. you.piety = HALF_MAX_PIETY + (good ? size : -size); } #ifdef DEBUG_DIAGNOSTICS snprintf(info, INFO_SIZE, "xom_tick(), delta: %d, piety: %d", delta, you.piety); take_note(Note(NOTE_MESSAGE, 0, 0, info), true); #endif // ...but he gets bored... if (you.gift_timeout > 0 && coinflip()) you.gift_timeout--; new_xom_favour = describe_xom_favour(); if (old_xom_favour != new_xom_favour) { const std::string msg = "You are now " + new_xom_favour; god_speaks(you.religion, msg.c_str()); } if (you.gift_timeout == 1) simple_god_message(" is getting BORED."); if (wearing_amulet(AMU_FAITH)? coinflip() : one_chance_in(3)) { const int tension = get_tension(GOD_XOM); const int chance = (tension == 0 ? 1 : tension <= 5 ? 2 : tension <= 10 ? 3 : tension <= 20 ? 4 : 5); // If Xom is bored, the chances for Xom acting are reversed. if (you.gift_timeout == 0 && x_chance_in_y(5-chance,5)) { xom_acts(abs(you.piety - HALF_MAX_PIETY), tension); return; } else if (you.gift_timeout <= 1 && chance > 0 && x_chance_in_y(chance-1, 4)) { // During tension, Xom may briefly forget about being bored. const int interest = random2(chance*15); if (interest > 0) { if (interest < 25) simple_god_message(" is interested."); else simple_god_message(" is intrigued."); you.gift_timeout += interest; #if defined(DEBUG_RELIGION) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "tension %d (chance: %d) -> increase interest to %d", tension, chance, you.gift_timeout); #endif } } if (x_chance_in_y(chance, 5)) xom_acts(abs(you.piety - HALF_MAX_PIETY), tension); } } // Picks 100 random grids from the level and checks whether they've been // marked as seen (explored) or known (mapped). If seen_only is true, // grids only "seen" via magic mapping don't count. Returns the // estimated percentage value of exploration. static int _exploration_estimate(bool seen_only = false) { int seen = 0; int total = 0; int tries = 0; do { tries++; coord_def pos = random_in_bounds(); if (!seen_only && is_terrain_known(pos) || is_terrain_seen(pos)) { seen++; total++; continue; } bool open = true; if (feat_is_solid(grd(pos)) && !feat_is_closed_door(grd(pos))) { open = false; for (adjacent_iterator ai(pos); ai; ++ai) { if (map_bounds(*ai) && (!feat_is_opaque(grd(*ai)) || feat_is_closed_door(grd(*ai)))) { open = true; break; } } } if (open) total++; } while (total < 100 && tries < 1000); #ifdef DEBUG_XOM mprf(MSGCH_DIAGNOSTICS, "exploration estimate (%s): %d out of %d grids seen", seen_only ? "explored" : "mapped", seen, total); #endif // If we didn't get any qualifying grids, there are probably so few // of them you've already seen them all. if (total == 0) return (100); if (total < 100) seen *= 100 / total; return (seen); } static bool _spell_weapon_check(const spell_type spell) { switch (spell) { case SPELL_TUKIMAS_DANCE: // Requires a wielded weapon. return (player_weapon_wielded()); case SPELL_TUKIMAS_VORPAL_BLADE: case SPELL_MAXWELLS_SILVER_HAMMER: case SPELL_FIRE_BRAND: case SPELL_FREEZING_AURA: case SPELL_POISON_WEAPON: case SPELL_LETHAL_INFUSION: case SPELL_EXCRUCIATING_WOUNDS: case SPELL_WARP_BRAND: { if (!player_weapon_wielded()) return (false); // The wielded weapon must be a non-branded non-launcher // non-artefact! const item_def& weapon = *you.weapon(); return (!is_artefact(weapon) && !is_range_weapon(weapon) && get_weapon_brand(weapon) == SPWPN_NORMAL); } default: return (true); } } static bool _teleportation_check(const spell_type spell = SPELL_TELEPORT_SELF) { switch (spell) { case SPELL_BLINK: case SPELL_TELEPORT_SELF: return (!item_blocks_teleport(false)); default: return (true); } } static bool _transformation_check(const spell_type spell) { transformation_type tran = TRAN_NONE; switch (spell) { case SPELL_SPIDER_FORM: tran = TRAN_SPIDER; break; case SPELL_STATUE_FORM: tran = TRAN_STATUE; break; case SPELL_ICE_FORM: tran = TRAN_ICE_BEAST; break; case SPELL_DRAGON_FORM: tran = TRAN_DRAGON; break; case SPELL_NECROMUTATION: tran = TRAN_LICH; break; default: break; } if (tran == TRAN_NONE) return (true); // Check whether existing enchantments/transformations, cursed // equipment or potential stat loss interfere with this // transformation. return (transform(0, tran, true, true)); } static int _xom_makes_you_cast_random_spell(int sever, int tension, bool debug = false) { int spellenum = std::max(1, sever); god_acting gdact(GOD_XOM); spell_type spell; if (tension > 0) { const int nxomspells = ARRAYSZ(_xom_tension_spells); spellenum = std::min(nxomspells, spellenum); spell = _xom_tension_spells[random2(spellenum)]; // Don't attempt to cast spells that are guaranteed to fail. // You may still get results such as "The spell fizzles" or // "Nothing appears to happen", but those should be rarer now. if (!_spell_weapon_check(spell)) return (XOM_DID_NOTHING); } else { const int nxomspells = ARRAYSZ(_xom_nontension_spells); // spellenum will be at least 3, so we don't run into infinite loops // for Detect Creatures/Magic Mapping in fully explored levels. spellenum = std::min(nxomspells, std::max(3 + coinflip(), spellenum)); spell = _xom_nontension_spells[random2(spellenum)]; if (spell == FAKE_SPELL_MAGIC_MAPPING || spell == SPELL_DETECT_ITEMS) { // If the level is already mostly explored, there's a chance // we might try something else. const int explored = _exploration_estimate(); if (explored > 80 && x_chance_in_y(explored, 100)) return (XOM_DID_NOTHING); } } // Handle magic mapping specially, now that it's no longer a spell. if (spell == FAKE_SPELL_MAGIC_MAPPING) { if (you.level_type == LEVEL_PANDEMONIUM) return (XOM_DID_NOTHING); if (debug) return (XOM_GOOD_MAPPING); god_speaks(GOD_XOM, _get_xom_speech("spell effect").c_str()); #if defined(DEBUG_DIAGNOSTICS) || defined(DEBUG_RELIGION) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "_xom_makes_you_cast_random_spell(); spell: %d, spellenum: %d", spell, spellenum); #endif take_note(Note(NOTE_XOM_EFFECT, you.piety, tension, "magic mapping"), true); const int power = stepdown_value( sever, 10, 10, 40, 45 ); magic_mapping(5 + power, 50 + random2avg(power * 2, 2), false); return (XOM_GOOD_MAPPING); } // Don't attempt to cast spells the undead cannot memorise. if (you_cannot_memorise(spell)) return (XOM_DID_NOTHING); // Don't attempt to teleport the player if the teleportation will // fail. if (!_teleportation_check(spell)) return (XOM_DID_NOTHING); // Don't attempt to transform the player if the transformation will // fail. if (!_transformation_check(spell)) return (XOM_DID_NOTHING); const int result = (tension > 0 ? XOM_GOOD_SPELL_TENSION : XOM_GOOD_SPELL_CALM); if (debug) return (result); god_speaks(GOD_XOM, _get_xom_speech("spell effect").c_str()); #if defined(DEBUG_DIAGNOSTICS) || defined(DEBUG_RELIGION) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "_xom_makes_you_cast_random_spell(); spell: %d, spellenum: %d", spell, spellenum); #endif static char spell_buf[100]; snprintf(spell_buf, sizeof(spell_buf), "cast spell '%s'", spell_title(spell)); take_note(Note(NOTE_XOM_EFFECT, you.piety, tension, spell_buf), true); your_spells(spell, sever, false); return (result); } static void _try_brand_switch(const int item_index) { if (item_index == NON_ITEM) return; item_def &item(mitm[item_index]); // Only apply it to melee weapons for the player. if (item.base_type != OBJ_WEAPONS || is_range_weapon(item)) return; if (is_unrandom_artefact(item)) return; // Only do it some of the time. if (one_chance_in(5)) return; int brand; if (item.base_type == OBJ_WEAPONS) { // Only switch already branded items. if (get_weapon_brand(item) == SPWPN_NORMAL) return; brand = (int) SPWPN_CHAOS; } else { // Only switch already branded items. if (get_ammo_brand(item) == SPWPN_NORMAL) return; brand = (int) SPMSL_CHAOS; } if (is_random_artefact(item)) artefact_set_property(item, ARTP_BRAND, brand); else item.special = brand; } static void _xom_make_item(object_class_type base, int subtype, int power) { god_acting gdact(GOD_XOM); int thing_created = items(true, base, subtype, true, power, MAKE_ITEM_RANDOM_RACE, 0, 0, GOD_XOM); if (feat_destroys_item(grd(you.pos()), mitm[thing_created], !silenced(you.pos()))) { simple_god_message(" snickers.", GOD_XOM); destroy_item(thing_created, true); thing_created = NON_ITEM; } if (thing_created == NON_ITEM) { god_speaks(GOD_XOM, "\"No, never mind.\""); return; } _try_brand_switch(thing_created); static char gift_buf[100]; snprintf(gift_buf, sizeof(gift_buf), "god gift: %s", mitm[thing_created].name(DESC_PLAIN).c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, gift_buf), true); mitm[thing_created].inscription = "god gift"; canned_msg(MSG_SOMETHING_APPEARS); move_item_to_grid(&thing_created, you.pos()); stop_running(); } static void _xom_acquirement(object_class_type force_class) { god_acting gdact(GOD_XOM); int item_index = NON_ITEM; if (!acquirement(force_class, GOD_XOM, false, &item_index) || item_index == NON_ITEM) { god_speaks(GOD_XOM, "\"No, never mind.\""); return; } _try_brand_switch(item_index); static char gift_buf[100]; snprintf(gift_buf, sizeof(gift_buf), "god gift: %s", mitm[item_index].name(DESC_PLAIN).c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, gift_buf), true); stop_running(); } static object_class_type _get_unrelated_wield_class(object_class_type ref) { object_class_type objtype = OBJ_WEAPONS; if (ref == OBJ_WEAPONS) { if (one_chance_in(10)) objtype = OBJ_MISCELLANY; else objtype = OBJ_STAVES; } else if (ref == OBJ_STAVES) { if (one_chance_in(10)) objtype = OBJ_MISCELLANY; else objtype = OBJ_WEAPONS; } else { const int temp_rand = random2(3); objtype = (temp_rand == 0) ? OBJ_WEAPONS : (temp_rand == 1) ? OBJ_STAVES : OBJ_MISCELLANY; } return (objtype); } // Gift an item the player can't currently use. It can still be really // good or even acquirement level. static bool _xom_annoyance_gift(int power, bool debug = false) { god_acting gdact(GOD_XOM); if (coinflip() && player_in_a_dangerous_place()) { const item_def *weapon = you.weapon(); // Xom has a sense of humour. if (coinflip() && weapon && weapon->cursed()) { if (debug) return (true); // If you are wielding a cursed item then Xom will give you // an item of that same type. Ha ha! god_speaks(GOD_XOM, _get_xom_speech("cursed gift").c_str()); if (coinflip()) // For added humour, give the same sub-type. _xom_make_item(weapon->base_type, weapon->sub_type, power * 3); else _xom_acquirement(weapon->base_type); return (true); } const item_def *gloves = you.slot_item(EQ_GLOVES); if (coinflip() && gloves && gloves->cursed()) { if (debug) return (true); // If you are wearing cursed gloves, then Xom will give you // a ring. Ha ha! god_speaks(GOD_XOM, _get_xom_speech("cursed gift").c_str()); _xom_make_item(OBJ_JEWELLERY, get_random_ring_type(), power * 3); return (true); }; const item_def *amulet = you.slot_item(EQ_AMULET); if (coinflip() && amulet && amulet->cursed()) { if (debug) return (true); // If you are wearing a cursed amulet, then Xom will give // you an amulet. Ha ha! god_speaks(GOD_XOM, _get_xom_speech("cursed gift").c_str()); _xom_make_item(OBJ_JEWELLERY, get_random_amulet_type(), power * 3); return (true); }; const item_def *left_ring = you.slot_item(EQ_LEFT_RING); const item_def *right_ring = you.slot_item(EQ_RIGHT_RING); if (coinflip() && ((left_ring && left_ring->cursed()) || (right_ring && right_ring->cursed()))) { if (debug) return (true); // If you are wearing a cursed ring, then Xom will give you // a ring. Ha ha! god_speaks(GOD_XOM, _get_xom_speech("ring gift").c_str()); _xom_make_item(OBJ_JEWELLERY, get_random_ring_type(), power * 3); return (true); } if (one_chance_in(5) && weapon) { if (debug) return (true); // Xom will give you a wielded item of a type different from // what you are currently wielding. god_speaks(GOD_XOM, _get_xom_speech("weapon gift").c_str()); const object_class_type objtype = _get_unrelated_wield_class(weapon->base_type); if (x_chance_in_y(power, 256)) _xom_acquirement(objtype); else _xom_make_item(objtype, OBJ_RANDOM, power * 3); return (true); } } const item_def *cloak = you.slot_item(EQ_CLOAK); if (coinflip() && cloak && cloak->cursed()) { // If you are wearing a cursed cloak, then Xom will give you a // cloak or body armour. Ha ha! god_speaks(GOD_XOM, _get_xom_speech("armour gift").c_str()); _xom_make_item(OBJ_ARMOUR, one_chance_in(10) ? ARM_CLOAK : get_random_body_armour_type(power * 2), power * 3); return (true); } return (false); } static int _xom_give_item(int power, bool debug = false) { if (_xom_annoyance_gift(power, debug)) return (XOM_GOOD_ANNOYANCE_GIFT); if (!debug) god_speaks(GOD_XOM, _get_xom_speech("general gift").c_str()); // There are two kinds of Xom gifts: acquirement and random object. // The result from acquirement is very good (usually as good or // better than random object), and it is sometimes tuned to the // player's skills and nature. Being tuned to the player's skills // and nature is not very Xomlike... if (x_chance_in_y(power, 256)) { if (debug) return (XOM_GOOD_ACQUIREMENT); const object_class_type types[] = { OBJ_WEAPONS, OBJ_ARMOUR, OBJ_JEWELLERY, OBJ_BOOKS, OBJ_STAVES, OBJ_WANDS, OBJ_MISCELLANY, OBJ_FOOD, OBJ_GOLD }; god_acting gdact(GOD_XOM); _xom_acquirement(RANDOM_ELEMENT(types)); } else { if (debug) return (XOM_GOOD_RANDOM_ITEM); // Random-type random object. _xom_make_item(OBJ_RANDOM, OBJ_RANDOM, power * 3); } more(); return (XOM_GOOD_RANDOM_ITEM); } static bool _choose_mutatable_monster(const monsters* mon) { return (mon->alive() && mon->can_safely_mutate() && !mon->submerged()); } static bool _is_chaos_upgradeable(const item_def &item, const monsters* mon) { // Since Xom is a god, he is capable of changing randarts, but not // other artefacts. if (is_unrandom_artefact(item)) return (false); // Only upgrade permanent items, since the player should get a // chance to use the item if he or she can defeat the monster. if (item.flags & ISFLAG_SUMMONED) return (false); // Blessed weapons are protected, being gifts from good gods. if (is_blessed(item)) return (false); // God gifts from good gods are protected. Also, Beogh hates all // the other gods, so he'll protect his gifts as well. if (item.orig_monnum < 0) { god_type iorig = static_cast(-item.orig_monnum); if (iorig > GOD_NO_GOD && iorig < NUM_GODS && (is_good_god(iorig) || iorig == GOD_BEOGH)) { return (false); } } // Leave branded items alone, since this is supposed to be an // upgrade. if (item.base_type == OBJ_MISSILES) { // Don't make boulders or throwing nets of chaos. if (item.sub_type == MI_LARGE_ROCK || item.sub_type == MI_THROWING_NET) { return (false); } if (get_ammo_brand(item) == SPMSL_NORMAL) return (true); } else { // If the weapon is a launcher, and the monster is either out // of ammo or is carrying javelins, then don't bother upgrading // the launcher. if (is_range_weapon(item) && (mon->inv[MSLOT_MISSILE] == NON_ITEM || !has_launcher(mitm[mon->inv[MSLOT_MISSILE]]))) { return (false); } if (get_weapon_brand(item) == SPWPN_NORMAL) return (true); } return (false); } static bool _choose_chaos_upgrade(const monsters* mon) { // Only choose monsters that will attack. if (!mon->alive() || mons_attitude(mon) != ATT_HOSTILE || mons_is_fleeing(mon) || mons_is_panicking(mon)) { return (false); } if (mons_itemuse(mon) < MONUSE_STARTING_EQUIPMENT) return (false); // Holy beings are presumably protected by another god, unless // they're gifts from a chaotic god. if (mon->is_holy() && !is_chaotic_god(mon->god)) return (false); // God gifts from good gods will be protected by their god from // being given chaos weapons, while other gods won't mind the help // in their servants' killing the player. if (is_good_god(mon->god)) return (false); // Beogh presumably doesn't want Xom messing with his orcs, even if // it would give them a better weapon. if (mons_species(mon->type) == MONS_ORC && (mon->is_priest() || coinflip())) { return (false); } mon_inv_type slots[] = {MSLOT_WEAPON, MSLOT_ALT_WEAPON, MSLOT_MISSILE}; // NOTE: Code assumes that the monster will only be carrying one // missile launcher at a time. bool special_launcher = false; for (int i = 0; i < 3; ++i) { const mon_inv_type slot = slots[i]; const int midx = mon->inv[slot]; if (midx == NON_ITEM) continue; const item_def &item(mitm[midx]); // The monster already has a chaos weapon. Give the upgrade to // a different monster. if (is_chaotic_item(item)) return (false); if (_is_chaos_upgradeable(item, mon)) { if (item.base_type != OBJ_MISSILES) return (true); // If, for some weird reason, a monster is carrying a bow // and javelins, then branding the javelins is okay, since // they won't be fired by the bow. if (!special_launcher || !has_launcher(item)) return (true); } if (is_range_weapon(item)) { // If the launcher alters its ammo, then branding the // monster's ammo won't be an upgrade. int brand = get_weapon_brand(item); if (brand == SPWPN_FLAME || brand == SPWPN_FROST || brand == SPWPN_VENOM) { special_launcher = true; } } } return (false); } static void _do_chaos_upgrade(item_def &item, const monsters* mon) { ASSERT(item.base_type == OBJ_MISSILES || item.base_type == OBJ_WEAPONS); ASSERT(!is_unrandom_artefact(item)); bool seen = false; if (mon && you.can_see(mon) && item.base_type == OBJ_WEAPONS) { seen = true; description_level_type desc = mon->friendly() ? DESC_CAP_YOUR : DESC_CAP_THE; std::string msg = apostrophise(mon->name(desc)); msg += " "; msg += item.name(DESC_PLAIN, false, false, false); msg += " is briefly surrounded by a scintillating aura of " "random colours."; mpr(msg.c_str()); } const int brand = (item.base_type == OBJ_WEAPONS) ? (int) SPWPN_CHAOS : (int) SPMSL_CHAOS; if (is_random_artefact(item)) { artefact_set_property(item, ARTP_BRAND, brand); if (seen) artefact_wpn_learn_prop(item, ARTP_BRAND); } else { item.special = brand; if (seen) set_ident_flags(item, ISFLAG_KNOW_TYPE); // Make sure it's visibly special. if (!(item.flags & ISFLAG_COSMETIC_MASK)) item.flags |= ISFLAG_GLOWING; // Make the pluses more like a randomly generated ego item. item.plus += random2(5); item.plus2 += random2(5); } } static monster_type _xom_random_demon(int sever, bool use_greater_demons = true) { const int roll = random2(1000 - (MAX_PIETY - sever) * 3); #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "_xom_random_demon(); sever = %d, roll: %d", sever, roll); #endif const demon_class_type dct = (roll >= 850) ? DEMON_GREATER : (roll >= 340) ? DEMON_COMMON : DEMON_LESSER; monster_type demon = MONS_PROGRAM_BUG; // Sometimes, send a holy warrior instead. if (dct == DEMON_GREATER && coinflip()) demon = summon_any_holy_being(HOLY_BEING_WARRIOR); else { const demon_class_type dct2 = (!use_greater_demons && dct == DEMON_GREATER) ? DEMON_COMMON : dct; if (dct2 == DEMON_COMMON && one_chance_in(10)) demon = MONS_CHAOS_SPAWN; else demon = summon_any_demon(dct2); } return (demon); } static bool _feat_is_deadly(dungeon_feature_type feat) { if (you.airborne()) return (false); return (feat == DNGN_LAVA || feat == DNGN_DEEP_WATER && !you.can_swim()); } static bool _player_is_dead() { return (you.hp <= 0 || you.strength <= 0 || you.dex <= 0 || you.intel <= 0 || _feat_is_deadly(grd(you.pos())) || you.did_escape_death()); } static int _xom_do_potion(bool debug = false) { if (debug) return (XOM_GOOD_POTION); potion_type pot = POT_HEALING; while (true) { pot = static_cast( random_choose(POT_HEALING, POT_HEAL_WOUNDS, POT_MAGIC, POT_SPEED, POT_MIGHT, POT_AGILITY, POT_BRILLIANCE, POT_INVISIBILITY, POT_BERSERK_RAGE, POT_EXPERIENCE, -1)); if (pot == POT_EXPERIENCE && !one_chance_in(6)) pot = POT_BERSERK_RAGE; bool has_effect = true; // Don't pick something that won't have an effect. // Extending an existing effect is okay, though. switch (pot) { case POT_HEALING: if (you.rotting || you.disease || you.duration[DUR_CONF] || you.duration[DUR_POISONING]) { break; } // else fall through case POT_HEAL_WOUNDS: if (you.hp == you.hp_max && player_rotted() == 0) has_effect = false; break; case POT_MAGIC: if (you.magic_points == you.max_magic_points) has_effect = false; break; case POT_BERSERK_RAGE: if (!you.can_go_berserk(false)) has_effect = false; break; case POT_EXPERIENCE: if (you.experience_level == 27) has_effect = false; break; default: break; } if (has_effect) break; } god_speaks(GOD_XOM, _get_xom_speech("potion effect").c_str()); if (pot == POT_BERSERK_RAGE) you.berserk_penalty = NO_BERSERK_PENALTY; // Take a note. std::string potion_msg = "potion effect "; switch (pot) { case POT_HEALING: potion_msg += "(healing)"; break; case POT_HEAL_WOUNDS: potion_msg += "(heal wounds)"; break; case POT_MAGIC: potion_msg += "(magic)"; break; case POT_SPEED: potion_msg += "(speed)"; break; case POT_MIGHT: potion_msg += "(might)"; break; case POT_AGILITY: potion_msg += "(agility)"; break; case POT_BRILLIANCE: potion_msg += "(brilliance)"; break; case POT_INVISIBILITY: potion_msg += "(invisibility)"; break; case POT_BERSERK_RAGE: potion_msg += "(berserk)"; break; case POT_EXPERIENCE: potion_msg += "(experience)"; break; default: potion_msg += "(other)"; break; } take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, potion_msg.c_str()), true); potion_effect(pot, 150); return (XOM_GOOD_POTION); } static int _xom_confuse_monsters(int sever, bool debug = false) { bool rc = false; for (monster_iterator mi(&you.get_los()); mi; ++mi) { if (mi->wont_attack() || !mons_class_is_confusable(mi->type) || one_chance_in(20)) { continue; } if (debug) return (XOM_GOOD_CONFUSION); if (mi->add_ench(mon_enchant(ENCH_CONFUSION, 0, KC_FRIENDLY, random2(sever)))) { // Only give this message once. if (!rc) god_speaks(GOD_XOM, _get_xom_speech("confusion").c_str()); simple_monster_message(*mi, " looks rather confused."); rc = true; } } if (rc) { take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "confuse monster(s)"), true); return (XOM_GOOD_CONFUSION); } return (XOM_DID_NOTHING); } static int _xom_send_allies(int sever, bool debug = false) { if (debug) return (XOM_GOOD_ALLIES); // The number of allies is dependent on severity, though heavily // randomised. int numdemons = sever; for (int i = 0; i < 3; i++) numdemons = random2(numdemons + 1); numdemons = std::min(numdemons + 2, 16); // Limit number of demons by experience level. const int maxdemons = (you.max_level * 3); if (numdemons > maxdemons) numdemons = maxdemons; int numdifferent = 0; // If we have a mix of demons and non-demons, there's a chance // that one or both of the factions may be hostile. int hostiletype = random_choose_weighted(3, 0, // both friendly 4, 1, // one hostile 4, 2, // other hostile 1, 3, // both hostile 0); std::vector is_demonic(numdemons, false); std::vector summons(numdemons); int num_actually_summoned = 0; for (int i = 0; i < numdemons; ++i) { monster_type mon_type = _xom_random_demon(sever); mgen_data mg(mon_type, BEH_FRIENDLY, &you, 3, MON_SUMM_AID, you.pos(), MHITYOU, MG_FORCE_BEH, GOD_XOM); // Even though the friendlies are charged to you for accounting, // they should still show as Xom's fault if one of them kills you. mg.non_actor_summoner = "Xom"; summons[i] = create_monster(mg); if (summons[i] != -1) { num_actually_summoned++; is_demonic[i] = (mons_class_holiness(mon_type) == MH_DEMONIC); // If it's not a demon, Xom got it someplace else, so use // different messages below. if (!is_demonic[i]) numdifferent++; } } if (num_actually_summoned) { const bool only_holy = (numdifferent == num_actually_summoned); const bool only_demonic = (numdifferent == 0); if (only_holy) { god_speaks(GOD_XOM, _get_xom_speech("multiple holy summons").c_str()); } else if (only_demonic) { god_speaks(GOD_XOM, _get_xom_speech("multiple summons").c_str()); } else { god_speaks(GOD_XOM, _get_xom_speech("multiple mixed summons").c_str()); } // If we have only non-demons, there's a chance that they // may be hostile. if (only_holy && one_chance_in(4)) hostiletype = 2; // If we have only demons, they'll always be friendly. else if (only_demonic) hostiletype = 0; for (int i = 0; i < numdemons; ++i) { if (summons[i] == -1) continue; monsters *mon = &menv[summons[i]]; if (hostiletype != 0) { // Mark factions hostile as appropriate. if (hostiletype == 3 || (is_demonic[i] && hostiletype == 1) || (!is_demonic[i] && hostiletype == 2)) { mon->attitude = ATT_HOSTILE; // XXX need to reset summon quota here? behaviour_event(mon, ME_ALERT, MHITYOU); } } player_angers_monster(mon); } // Take a note. static char summ_buf[80]; snprintf(summ_buf, sizeof(summ_buf), "summons %d %s%s%s", num_actually_summoned, hostiletype == 0 ? "friendly " : hostiletype == 3 ? "hostile " : "", only_demonic ? "demon" : "monster", num_actually_summoned > 1 ? "s" : ""); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, summ_buf), true); return (XOM_GOOD_ALLIES); } return (XOM_DID_NOTHING); } static int _xom_send_one_ally(int sever, bool debug = false) { if (debug) return (XOM_GOOD_SINGLE_ALLY); const monster_type mon_type = _xom_random_demon(sever); const bool is_demonic = (mons_class_holiness(mon_type) == MH_DEMONIC); // If we have a non-demon, Xom got it someplace else, so use // different messages below. bool different = !is_demonic; beh_type beha = BEH_FRIENDLY; // There's a chance that a non-demon may be hostile. if (different && one_chance_in(4)) beha = BEH_HOSTILE; mgen_data mg(mon_type, beha, (beha == BEH_FRIENDLY) ? &you : 0, 6, MON_SUMM_AID, you.pos(), MHITYOU, MG_FORCE_BEH, GOD_XOM); mg.non_actor_summoner = "Xom"; const int summons = create_monster(mg); if (summons != -1) { if (different) god_speaks(GOD_XOM, _get_xom_speech("single holy summon").c_str()); else god_speaks(GOD_XOM, _get_xom_speech("single summon").c_str()); player_angers_monster(&menv[summons]); // Take a note. static char summ_buf[80]; snprintf(summ_buf, sizeof(summ_buf), "summons %s %s", beha == BEH_FRIENDLY ? "friendly" : "hostile", menv[summons].name(DESC_PLAIN).c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, summ_buf), true); return (XOM_GOOD_SINGLE_ALLY); } return (XOM_DID_NOTHING); } static int _xom_polymorph_nearby_monster(bool helpful, bool debug = false) { if (there_are_monsters_nearby(false, false)) { monsters *mon = choose_random_nearby_monster(0, _choose_mutatable_monster); if (mon) { if (debug) return (helpful ? XOM_GOOD_POLYMORPH : XOM_BAD_POLYMORPH); const char* lookup = (helpful ? "good monster polymorph" : "bad monster polymorph"); god_speaks(GOD_XOM, _get_xom_speech(lookup).c_str()); bool see_old = you.can_see(mon); std::string old_name = mon->full_name(DESC_PLAIN); if (one_chance_in(8) && !mons_is_ghost_demon(mon->type) && !mon->is_shapeshifter() && mon->holiness() == MH_NATURAL) { mon->add_ench(one_chance_in(3) ? ENCH_GLOWING_SHAPESHIFTER : ENCH_SHAPESHIFTER); } const bool powerup = !(mon->wont_attack() ^ helpful); monster_polymorph(mon, RANDOM_MONSTER, powerup ? PPT_MORE : PPT_LESS); bool see_new = you.can_see(mon); if (see_old || see_new) { std::string new_name = mon->full_name(DESC_PLAIN); if (!see_old) old_name = "something unseen"; else if (!see_new) new_name = "something unseen"; // Take a note. static char poly_buf[120]; snprintf(poly_buf, sizeof(poly_buf), "polymorph %s -> %s", old_name.c_str(), new_name.c_str()); std::string poly = poly_buf; #ifdef NOTE_DEBUG_XOM poly += " ("; poly += (powerup ? "upgrade" : "downgrade"); poly += ")"; #endif take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, poly.c_str()), true); } return (helpful ? XOM_GOOD_POLYMORPH : XOM_BAD_POLYMORPH); } } return (XOM_DID_NOTHING); } static void _confuse_monster(monsters mons, int sever) { if (!mons_class_is_confusable(mons.type)) return; const bool was_confused = mons.confused(); if (mons.add_ench(mon_enchant(ENCH_CONFUSION, 0, KC_FRIENDLY, random2(sever)))) { if (was_confused) simple_monster_message(&mons, " looks rather more confused."); else simple_monster_message(&mons, " looks rather confused."); } } static bool _swap_monsters(monsters *m1, monsters *m2) { monsters& mon1(*m1); monsters& mon2(*m2); const bool mon1_caught = mon1.caught(); const bool mon2_caught = mon2.caught(); const coord_def mon1_pos = mon1.pos(); const coord_def mon2_pos = mon2.pos(); if (!mon2.is_habitable(mon1_pos) || !mon1.is_habitable(mon2_pos)) return (false); // Make submerged monsters unsubmerge. mon1.del_ench(ENCH_SUBMERGED); mon2.del_ench(ENCH_SUBMERGED); mgrd(mon1_pos) = mon2.mindex(); mon1.moveto(mon2_pos); mgrd(mon2_pos) = mon1.mindex(); mon2.moveto(mon1_pos); if (mon1_caught && !mon2_caught) { check_net_will_hold_monster(&mon2); mon1.del_ench(ENCH_HELD, true); } else if (mon2_caught && !mon1_caught) { check_net_will_hold_monster(&mon1); mon2.del_ench(ENCH_HELD, true); } return (true); } static bool _art_is_safe(item_def item) { int prop_str = artefact_wpn_property(item, ARTP_STRENGTH); int prop_int = artefact_wpn_property(item, ARTP_INTELLIGENCE); int prop_dex = artefact_wpn_property(item, ARTP_DEXTERITY); return (prop_str >= 0 && prop_int >= 0 && prop_dex >= 0); } static int _xom_swap_weapons(bool debug = false) { if (player_stair_delay()) return (XOM_DID_NOTHING); item_def *wpn = you.weapon(); if (!wpn) return (XOM_DID_NOTHING); if (you.berserk() || wpn->base_type != OBJ_WEAPONS || get_weapon_brand(*wpn) == SPWPN_DISTORTION || !safe_to_remove_or_wear(*wpn, true, true)) { return (XOM_DID_NOTHING); } std::vector mons_wpn; for (monster_iterator mi(&you); mi; ++mi) { if (!wpn || mi->wont_attack() || mi->is_summoned() || mons_itemuse(*mi) < MONUSE_STARTING_EQUIPMENT || (mi->flags & MF_HARD_RESET)) { continue; } const int mweap = mi->inv[MSLOT_WEAPON]; if (mweap == NON_ITEM) continue; const item_def weapon = mitm[mweap]; // Let's be nice about this. if (weapon.base_type == OBJ_WEAPONS && !(weapon.flags & ISFLAG_SUMMONED) && you.can_wield(weapon, true) && mi->can_wield(*wpn, true) && !get_weapon_brand(weapon) != SPWPN_DISTORTION && (!is_artefact(weapon) || _art_is_safe(weapon))) { mons_wpn.push_back(*mi); } } if (mons_wpn.empty()) return (XOM_DID_NOTHING); if (debug) return (XOM_BAD_SWAP_WEAPONS); god_speaks(GOD_XOM, _get_xom_speech("swap weapons").c_str()); const int num_mons = mons_wpn.size(); // Pick a random monster... monsters *mon = mons_wpn[random2(num_mons)]; // ...and get its weapon. int monwpn = mon->inv[MSLOT_WEAPON]; int mywpn = you.equip[EQ_WEAPON]; ASSERT (monwpn != NON_ITEM); ASSERT (mywpn != -1); unwield_item(); item_def &myweapon = you.inv[mywpn]; int index = get_item_slot(10); if (index == NON_ITEM) return (XOM_DID_NOTHING); // Move monster's old item to player's inventory as last step. mon->unequip(*(mon->mslot_item(MSLOT_WEAPON)), MSLOT_WEAPON, 0, true); mon->inv[MSLOT_WEAPON] = NON_ITEM; mitm[index] = myweapon; unwind_var save_speedinc(mon->speed_increment); if (!mon->pickup_item(mitm[index], false, true)) { mpr("Monster wouldn't take item.", MSGCH_ERROR); mon->inv[MSLOT_WEAPON] = monwpn; mon->equip(mitm[monwpn], MSLOT_WEAPON, 0); unlink_item(index); destroy_item(myweapon); return (XOM_DID_NOTHING); } // Mark the weapon as thrown, so that we'll autograb it once the // monster is dead. mitm[index].flags |= ISFLAG_THROWN; mprf("%s wields %s!", mon->name(DESC_CAP_THE).c_str(), myweapon.name(DESC_NOCAP_YOUR).c_str()); mon->equip(myweapon, MSLOT_WEAPON, 0); // Item is gone from player's inventory. dec_inv_item_quantity(mywpn, myweapon.quantity); mitm[monwpn].pos.reset(); mitm[monwpn].link = NON_ITEM; int freeslot = find_free_slot(mitm[monwpn]); if (freeslot < 0 || freeslot >= ENDOFPACK || you.inv[freeslot].is_valid()) { // Something is terribly wrong. return (XOM_DID_NOTHING); } item_def &myitem = you.inv[freeslot]; // Copy item. myitem = mitm[monwpn]; myitem.link = freeslot; myitem.pos.set(-1, -1); // Remove "dropped by ally" flag. myitem.flags &= ~(ISFLAG_DROPPED_BY_ALLY); if (!myitem.slot) myitem.slot = index_to_letter(myitem.link); origin_set_monster(myitem, mon); note_inscribe_item(myitem); dec_mitm_item_quantity(monwpn, myitem.quantity); you.m_quiver->on_inv_quantity_changed(freeslot, myitem.quantity); burden_change(); mprf("You wield %s %s!", mon->name(DESC_NOCAP_ITS).c_str(), you.inv[freeslot].name(DESC_PLAIN).c_str()); you.equip[EQ_WEAPON] = freeslot; wield_effects(freeslot, true); you.wield_change = true; you.m_quiver->on_weapon_changed(); return (XOM_BAD_SWAP_WEAPONS); } // Swap places with a random monster and, depending on severity, also // between monsters. This can be pretty bad if there are a lot of // hostile monsters around. static int _xom_rearrange_pieces(int sever, bool debug = false) { if (player_stair_delay()) return (XOM_DID_NOTHING); std::vector mons; for (monster_iterator mi(&you); mi; ++mi) mons.push_back(*mi); if (mons.empty()) return (XOM_DID_NOTHING); if (debug) return (XOM_GOOD_SWAP_MONSTERS); god_speaks(GOD_XOM, _get_xom_speech("rearrange the pieces").c_str()); const int num_mons = mons.size(); // Swap places with a random monster. monsters *mon = mons[random2(num_mons)]; swap_with_monster(mon); // Sometimes confuse said monster. if (coinflip()) _confuse_monster(*mon, sever); if (num_mons > 1 && x_chance_in_y(sever, 70)) { bool did_message = false; const int max_repeats = std::min(num_mons / 2, 8); const int repeats = std::min(random2(sever / 10) + 1, max_repeats); for (int i = 0; i < repeats; ++i) { const int mon1 = random2(num_mons); int mon2 = mon1; while (mon1 == mon2) mon2 = random2(num_mons); if (_swap_monsters(mons[mon1], mons[mon2])) { if (!did_message) { mpr("Some monsters swap places."); did_message = true; } if (one_chance_in(3)) _confuse_monster(*mons[mon1], sever); if (one_chance_in(3)) _confuse_monster(*mons[mon2], sever); } } } take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "swap monsters"), true); return (XOM_GOOD_SWAP_MONSTERS); } static int _xom_animate_monster_weapon(int sever, bool debug = false) { std::vector mons_wpn; for (monster_iterator mi(&you); mi; ++mi) { if (mi->wont_attack() || mi->is_summoned() || mons_itemuse(*mi) < MONUSE_STARTING_EQUIPMENT || (mi->flags & MF_HARD_RESET)) { continue; } const int mweap = mi->inv[MSLOT_WEAPON]; if (mweap == NON_ITEM) continue; const item_def weapon = mitm[mweap]; if (weapon.base_type == OBJ_WEAPONS && !(weapon.flags & ISFLAG_SUMMONED) && weapon.quantity == 1 && !is_range_weapon(weapon) && !is_special_unrandom_artefact(weapon) && !get_weapon_brand(weapon) != SPWPN_DISTORTION) { mons_wpn.push_back(*mi); } } if (mons_wpn.empty()) return (XOM_DID_NOTHING); if (debug) return (XOM_GOOD_ANIMATE_MON_WPN); god_speaks(GOD_XOM, _get_xom_speech("animate monster weapon").c_str()); const int num_mons = mons_wpn.size(); // Pick a random monster... monsters *mon = mons_wpn[random2(num_mons)]; // ...and get its weapon. const int wpn = mon->inv[MSLOT_WEAPON]; ASSERT(wpn != NON_ITEM); const int dur = std::min(2 + (random2(sever) / 5), 6); mgen_data mg(MONS_DANCING_WEAPON, BEH_FRIENDLY, &you, dur, SPELL_TUKIMAS_DANCE, mon->pos(), mon->mindex(), 0, GOD_XOM); mg.non_actor_summoner = "Xom"; const int monster = create_monster(mg); if (monster == -1) return (XOM_DID_NOTHING); // Make the monster unwield its weapon. mon->unequip(*(mon->mslot_item(MSLOT_WEAPON)), MSLOT_WEAPON, 0, true); mon->inv[MSLOT_WEAPON] = NON_ITEM; mprf("%s %s dances into the air!", apostrophise(mon->name(DESC_CAP_THE)).c_str(), mitm[wpn].name(DESC_PLAIN).c_str()); destroy_item(menv[monster].inv[MSLOT_WEAPON]); menv[monster].inv[MSLOT_WEAPON] = wpn; mitm[wpn].set_holding_monster(monster); menv[monster].colour = mitm[wpn].colour; return (XOM_GOOD_ANIMATE_MON_WPN); } static int _xom_give_mutations(bool good, bool debug = false) { bool rc = false; if (you.can_safely_mutate()) { if (debug) return (good ? XOM_GOOD_MUTATION : XOM_BAD_MUTATION); const char* lookup = (good ? "good mutations" : "random mutations"); god_speaks(GOD_XOM, _get_xom_speech(lookup).c_str()); const int num_tries = random2(4) + 1; static char mut_buf[80]; snprintf(mut_buf, sizeof(mut_buf), "give %smutation%s", #ifdef NOTE_DEBUG_XOM good ? "good " : "random ", #else "", #endif num_tries > 1 ? "s" : ""); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, mut_buf), true); mpr("Your body is suffused with distortional energy."); set_hp(1 + random2(you.hp), false); deflate_hp(you.hp_max / 2, true); bool failMsg = true; for (int i = num_tries; i > 0; --i) { if (mutate(good ? RANDOM_GOOD_MUTATION : RANDOM_XOM_MUTATION, failMsg, false, true, false, false, good || !_xom_feels_nasty())) { rc = true; } else failMsg = false; } } if (rc) return (good ? XOM_GOOD_MUTATION : XOM_BAD_MUTATION); return (XOM_DID_NOTHING); } // Summons a permanent ally. static int _xom_send_major_ally(int sever, bool debug = false) { if (debug) return (XOM_GOOD_MAJOR_ALLY); const monster_type mon_type = _xom_random_demon(sever); const bool is_demonic = (mons_class_holiness(mon_type) == MH_DEMONIC); beh_type beha = BEH_FRIENDLY; // There's a chance that a non-demon may be hostile. if (!is_demonic && one_chance_in(4)) beha = BEH_HOSTILE; mgen_data mg(_xom_random_demon(sever, one_chance_in(8)), beha, (beha == BEH_FRIENDLY) ? &you : 0, 0, 0, you.pos(), MHITYOU, MG_FORCE_BEH, GOD_XOM); mg.non_actor_summoner = "Xom"; const int summons = create_monster(mg); if (summons != -1) { if (is_demonic) { god_speaks(GOD_XOM, _get_xom_speech("single major demon summon").c_str()); } else { god_speaks(GOD_XOM, _get_xom_speech("single major holy summon").c_str()); } player_angers_monster(&menv[summons]); // Take a note. static char summ_buf[80]; snprintf(summ_buf, sizeof(summ_buf), "sends permanent %s %s", beha == BEH_FRIENDLY ? "friendly" : "hostile", menv[summons].name(DESC_PLAIN).c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, summ_buf), true); return (XOM_GOOD_MAJOR_ALLY); } return (XOM_DID_NOTHING); } static int _xom_throw_divine_lightning(bool debug = false) { if (!player_in_a_dangerous_place()) return (XOM_DID_NOTHING); // Make sure there's at least one enemy within the lightning radius. bool found_hostile = false; for (radius_iterator ri(you.pos(), 2, true, true, true); ri; ++ri) { if (monsters* mon = monster_at(*ri)) { if (!mon->wont_attack()) { found_hostile = true; break; } } } // No hostiles within radius. if (!found_hostile) return (XOM_DID_NOTHING); if (debug) return (XOM_GOOD_LIGHTNING); bool protection = false; if (you.hp <= random2(201)) { you.attribute[ATTR_DIVINE_LIGHTNING_PROTECTION] = 1; protection = true; } god_speaks(GOD_XOM, "The area is suffused with divine lightning!"); bolt beam; beam.flavour = BEAM_ELECTRICITY; beam.type = dchar_glyph(DCHAR_FIRED_BURST); beam.damage = dice_def(3, 30); beam.target = you.pos(); beam.name = "blast of lightning"; beam.colour = LIGHTCYAN; beam.thrower = KILL_MISC; beam.beam_source = NON_MONSTER; beam.aux_source = "Xom's lightning strike"; beam.ex_size = 2; beam.is_explosion = true; beam.explode(); if (you.attribute[ATTR_DIVINE_LIGHTNING_PROTECTION]) { mpr("Your divine protection wanes."); you.attribute[ATTR_DIVINE_LIGHTNING_PROTECTION] = 0; } // Don't accidentally kill the player when doing a good act. if (you.escaped_death_cause == KILLED_BY_WILD_MAGIC && you.escaped_death_aux == "Xom's lightning strike") { you.hp = 1; you.reset_escaped_death(); } // Take a note. static char lightning_buf[80]; snprintf(lightning_buf, sizeof(lightning_buf), "divine lightning%s", protection ? " (protected)" : ""); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, lightning_buf), true); return (XOM_GOOD_LIGHTNING); } static int _xom_change_scenery(bool debug = false) { std::vector candidates; for (radius_iterator ri(&you.get_los()); ri; ++ri) { dungeon_feature_type feat = grd(*ri); if (feat >= DNGN_FOUNTAIN_BLUE && feat <= DNGN_DRY_FOUNTAIN_BLOOD) candidates.push_back(*ri); else if (feat >= DNGN_CLOSED_DOOR && feat <= DNGN_SECRET_DOOR || feat == DNGN_OPEN_DOOR && !actor_at(*ri) && igrd(*ri) == NON_ITEM) { candidates.push_back(*ri); } } const std::string speech = _get_xom_speech("scenery"); if (candidates.empty()) { if (!one_chance_in(8)) return (XOM_DID_NOTHING); // Place one or more altars to Xom. coord_def place; bool success = false; const int max_altars = std::max(1, random2(random2(14))); for (int tries = max_altars; tries > 0; --tries) { if ((random_near_space(you.pos(), place) || random_near_space(you.pos(), place, true)) && grd(place) == DNGN_FLOOR && you.see_cell(place)) { if (debug) return (XOM_GOOD_SCENERY); grd(place) = DNGN_ALTAR_XOM; success = true; } } if (success) { god_speaks(GOD_XOM, speech.c_str()); return (XOM_GOOD_SCENERY); } return (XOM_DID_NOTHING); } if (debug) return (XOM_GOOD_SCENERY); const int fountain_diff = (DNGN_DRY_FOUNTAIN_BLUE - DNGN_FOUNTAIN_BLUE); std::random_shuffle(candidates.begin(), candidates.end()); int doors_open = 0; int doors_close = 0; int fountains_flow = 0; int fountains_blood = 0; for (unsigned int i = 0; i < candidates.size(); ++i) { coord_def pos = candidates[i]; switch (grd(pos)) { case DNGN_CLOSED_DOOR: case DNGN_DETECTED_SECRET_DOOR: case DNGN_SECRET_DOOR: grd(pos) = DNGN_OPEN_DOOR; doors_open++; break; case DNGN_OPEN_DOOR: grd(pos) = DNGN_CLOSED_DOOR; doors_close++; break; case DNGN_DRY_FOUNTAIN_BLUE: case DNGN_DRY_FOUNTAIN_SPARKLING: case DNGN_DRY_FOUNTAIN_BLOOD: { if (x_chance_in_y(fountains_flow, 5)) continue; grd(pos) = (dungeon_feature_type) (grd(pos) - fountain_diff); fountains_flow++; break; } case DNGN_FOUNTAIN_BLUE: if (x_chance_in_y(fountains_blood, 3)) continue; grd(pos) = DNGN_FOUNTAIN_BLOOD; fountains_blood++; break; default: break; } } if (!doors_open && !doors_close && !fountains_flow && !fountains_blood) return (XOM_DID_NOTHING); god_speaks(GOD_XOM, speech.c_str()); std::vector effects; if (doors_open > 0) { snprintf(info, INFO_SIZE, "%s door%s burst%s open", doors_open == 1 ? "A" : doors_open == 2 ? "Two" : "Several", doors_open == 1 ? "" : "s", doors_open == 1 ? "s" : ""); effects.push_back(info); } if (doors_close > 0) { snprintf(info, INFO_SIZE, "%s%s door%s slam%s shut", doors_close == 1 ? "a" : doors_close == 2 ? "two" : "several", doors_open > 0 ? (doors_close == 1 ? "nother" : " other") : "", doors_close == 1 ? "" : "s", doors_close == 1 ? "s" : ""); std::string closed = info; if (effects.empty()) closed = uppercase_first(closed); effects.push_back(closed); } if (!effects.empty()) { mprf("%s!", comma_separated_line(effects.begin(), effects.end(), ", and ").c_str()); effects.clear(); } if (fountains_flow > 0) { snprintf(info, INFO_SIZE, "%s fountain%s start%s reflowing", fountains_flow == 1 ? "A" : "Some", fountains_flow == 1 ? "" : "s", fountains_flow == 1 ? "s" : ""); effects.push_back(info); } if (fountains_blood > 0) { snprintf(info, INFO_SIZE, "%s%s fountain%s start%s gushing blood", fountains_blood == 1 ? "a" : "some", fountains_flow > 0 ? (fountains_blood == 1 ? "nother" : " other") : "", fountains_blood == 1 ? "" : "s", fountains_blood == 1 ? "s" : ""); std::string fountains = info; if (effects.empty()) fountains = uppercase_first(fountains); effects.push_back(fountains); } if (!effects.empty()) { mprf("%s!", comma_separated_line(effects.begin(), effects.end(), ", and ").c_str()); } return (XOM_GOOD_SCENERY); } // The nicer stuff. Note: these things are not necessarily nice. static int _xom_is_good(int sever, int tension, bool debug = false) { int done = XOM_DID_NOTHING; // Did Xom (already) kill the player? if (_player_is_dead()) return (XOM_PLAYER_DEAD); god_acting gdact(GOD_XOM); // This series of random calls produces a poisson-looking // distribution: initial hump, plus a long-ish tail. // Don't make the player go berserk etc. if there's no danger. if (tension > random2(3) && x_chance_in_y(2, sever)) done = _xom_do_potion(debug); else if (x_chance_in_y(3, sever)) { // There are a lot less non-tension spells than tension ones, // so use them more rarely. if (tension > 0 || one_chance_in(3)) done = _xom_makes_you_cast_random_spell(sever, tension, debug); } else if (tension > 0 && x_chance_in_y(4, sever)) done = _xom_confuse_monsters(sever, debug); // It's pointless to send in help if there's no danger. else if (tension > random2(5) && x_chance_in_y(5, sever)) done = _xom_send_one_ally(sever, debug); else if (tension < random2(5) && x_chance_in_y(6, sever)) done = _xom_change_scenery(debug); else if (x_chance_in_y(7, sever)) done = _xom_give_item(sever, debug); // It's pointless to send in help if there's no danger. else if (tension > random2(10) && x_chance_in_y(8, sever)) done = _xom_send_allies(sever, debug); else if (tension > random2(8) && x_chance_in_y(9, sever)) done = _xom_animate_monster_weapon(sever, debug); else if (x_chance_in_y(10, sever)) done = _xom_polymorph_nearby_monster(true, debug); else if (tension > 0 && x_chance_in_y(11, sever)) done = _xom_rearrange_pieces(sever, debug); else if (random2(tension) < 15 && x_chance_in_y(12, sever)) done = _xom_give_item(sever, debug); else if (x_chance_in_y(13, sever) && you.level_type != LEVEL_ABYSS) { // Try something else if teleportation is impossible. if (!_teleportation_check()) return (XOM_DID_NOTHING); // This is not very interesting if the level is already fully // explored (presumably cleared). Even then, it may // occasionally happen. const int explored = _exploration_estimate(true); if (explored >= 80 && x_chance_in_y(explored, 120)) return (XOM_DID_NOTHING); if (debug) return (XOM_GOOD_TELEPORT); // The Xom teleportation train takes you on instant // teleportation to a few random areas, stopping randomly but // most likely in an area that is not dangerous to you. god_speaks(GOD_XOM, _get_xom_speech("teleportation journey").c_str()); int count = 0; do { count++; you_teleport_now(false); more(); if (one_chance_in(10) || count >= 7 + random2(5)) break; } while (x_chance_in_y(3, 4) || player_in_a_dangerous_place()); maybe_update_stashes(); // Take a note. static char tele_buf[80]; snprintf(tele_buf, sizeof(tele_buf), "%d-stop teleportation journey%s", count, #ifdef NOTE_DEBUG_XOM player_in_a_dangerous_place() ? " (dangerous)" : // see below #endif ""); take_note(Note(NOTE_XOM_EFFECT, you.piety, tension, tele_buf), true); done = XOM_GOOD_TELEPORT; } else if (random2(tension) < 5 && x_chance_in_y(14, sever)) { if (debug) return (XOM_GOOD_VITRIFY); // This can fail with radius 1, or in open areas. if (vitrify_area(random2avg(sever/4, 2) + 1)) { god_speaks(GOD_XOM, _get_xom_speech("vitrification").c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, tension, "vitrification"), true); done = XOM_GOOD_VITRIFY; } } else if (random2(tension) < 5 && x_chance_in_y(15, sever) && x_chance_in_y(16, how_mutated())) { done = _xom_give_mutations(true, debug); } // It's pointless to send in help if there's no danger. else if (tension > random2(15) && x_chance_in_y(16, sever)) done = _xom_send_major_ally(sever, debug); else if (tension > 0 && x_chance_in_y(17, sever)) done = _xom_throw_divine_lightning(debug); return (done); } // Is the equipment type usable, and is the slot empty? static bool _could_wear_eq(equipment_type eq) { if (!you_tran_can_wear(eq, true)) return (false); return (you.slot_item(eq) == NULL); } static item_def* _tran_get_eq(equipment_type eq) { if (you_tran_can_wear(eq, true)) return (you.slot_item(eq)); return (NULL); } // Which types of dungeon features are in view? static void _get_in_view(FixedVector& in_view) { in_view.init(false); for (radius_iterator ri(&you.get_los()); ri; ++ri) in_view[grd(*ri)] = true; } static void _xom_zero_miscast() { std::vector messages; std::vector priority; std::vector inv_items; for (int i = 0; i < ENDOFPACK; ++i) { const item_def &item(you.inv[i]); if (item.is_valid() && !item_is_equipped(item) && !item.is_critical()) { inv_items.push_back(i); } } // Assure that the messages vector has at least one element. messages.push_back("Nothing appears to happen... Ominous!"); /////////////////////////////////// // Dungeon feature dependent stuff. FixedVector in_view; _get_in_view(in_view); if (in_view[DNGN_LAVA]) messages.push_back("The lava spits out sparks!"); if (in_view[DNGN_SHALLOW_WATER] || in_view[DNGN_DEEP_WATER]) { messages.push_back("The water briefly bubbles."); messages.push_back("The water briefly swirls."); messages.push_back("The water briefly glows."); } if (in_view[DNGN_DEEP_WATER]) { messages.push_back("From the corner of your eye you spot something " "lurking in the deep water."); } if (in_view[DNGN_ORCISH_IDOL]) { if (you.species == SP_HILL_ORC) priority.push_back("The idol of Beogh turns to glare at you."); else priority.push_back("The orcish idol turns to glare at you."); } if (in_view[DNGN_GRANITE_STATUE]) priority.push_back("The granite statue turns to stare at you."); if (in_view[DNGN_WAX_WALL]) priority.push_back("The wax wall pulsates ominously."); if (in_view[DNGN_CLEAR_ROCK_WALL] || in_view[DNGN_CLEAR_STONE_WALL] || in_view[DNGN_CLEAR_PERMAROCK_WALL]) { messages.push_back("Dim shapes swim through the translucent wall."); } if (in_view[DNGN_GREEN_CRYSTAL_WALL]) messages.push_back("Dim shapes swim through the green crystal wall."); if (in_view[DNGN_METAL_WALL]) { messages.push_back("Tendrils of electricity crawl over the metal " "wall!"); } if (in_view[DNGN_FOUNTAIN_BLUE] || in_view[DNGN_FOUNTAIN_SPARKLING]) { priority.push_back("The water in the fountain briefly bubbles."); priority.push_back("The water in the fountain briefly swirls."); priority.push_back("The water in the fountain briefly glows."); } if (in_view[DNGN_DRY_FOUNTAIN_BLUE] || in_view[DNGN_DRY_FOUNTAIN_SPARKLING] || in_view[DNGN_PERMADRY_FOUNTAIN]) { priority.push_back("Water briefly sprays from the dry fountain."); priority.push_back("Dust puffs up from the dry fountain."); } if (in_view[DNGN_STONE_ARCH]) priority.push_back("The stone arch briefly shows a sunny meadow on " "the other side."); const dungeon_feature_type feat = grd(you.pos()); if (!feat_is_solid(feat) && feat_stair_direction(feat) == CMD_NO_CMD && !feat_is_trap(feat) && feat != DNGN_STONE_ARCH && feat != DNGN_OPEN_DOOR && feat != DNGN_ABANDONED_SHOP) { const std::string feat_name = feature_description(you.pos(), false, DESC_CAP_THE, false); if (you.airborne()) { // Kenku fly a lot, so don't put airborne messages into the // priority vector for them. std::vector* vec; if (you.species == SP_KENKU) vec = &messages; else vec = &priority; vec->push_back(feat_name + " seems to fall away from under you!"); vec->push_back(feat_name + " seems to rush up at you!"); if (feat_is_water(feat)) { priority.push_back("Something invisible splashes into the " "water beneath you!"); } } else if (feat_is_water(feat)) { priority.push_back("The water briefly recedes away from you."); priority.push_back("Something invisible splashes into the water " "beside you!"); } } if (feat_has_solid_floor(feat) && inv_items.size() > 0) { int idx = inv_items[random2(inv_items.size())]; const item_def &item(you.inv[idx]); std::string name; if (item.quantity == 1) name = item.name(DESC_CAP_YOUR, false, false, false); else { name = "One of "; name += item.name(DESC_NOCAP_YOUR, false, false, false); } messages.push_back(name + " falls out of your pack, then " "immediately jumps back in!"); } //////////////////////////////////////////// // Body, player spcies, transformations, etc const int transform = you.attribute[ATTR_TRANSFORMATION]; if (you.species == SP_MUMMY && you_tran_can_wear(EQ_BODY_ARMOUR)) { messages.push_back("You briefly get tangled in your bandages."); if (!you.airborne() && !you.swimming()) messages.push_back("You trip over your bandages."); } if (transform != TRAN_SPIDER) { std::string str = "A monocle briefly appears over your "; str += coinflip() ? "right" : "left"; str += " eye."; messages.push_back(str); } if (!player_genus(GENPC_DRACONIAN) && you.species != SP_MUMMY && (transform == TRAN_NONE || transform == TRAN_BLADE_HANDS)) { messages.push_back("Your eyebrows briefly feel incredibly bushy."); messages.push_back("Your eyebrows wriggle."); } if (you.species != SP_NAGA && (you.species != SP_MERFOLK || !you.swimming()) && !you.airborne()) { messages.push_back("You do an impromptu tapdance."); } /////////////////////////// // Equipment related stuff. item_def* item; if (_could_wear_eq(EQ_WEAPON)) { std::string str = "A fancy cane briefly appears in your "; str += you.hand_name(false); str += "."; messages.push_back(str); } if (_tran_get_eq(EQ_CLOAK) != NULL) messages.push_back("Your cloak billows in an unfelt wind."); if ((item = _tran_get_eq(EQ_HELMET))) { std::string str = "Your "; str += item->name(DESC_BASENAME, false, false, false); str += " leaps into the air, briefly spins, then lands back on " "your head!"; messages.push_back(str); } if ((item = _tran_get_eq(EQ_BOOTS)) && item->sub_type == ARM_BOOTS && !you.cannot_act()) { std::string name = item->name(DESC_BASENAME, false, false, false); name = replace_all(name, "pair of ", ""); std::string str = "You compulsively click the heels of your "; str += name; str += " together three times."; } if ((item = _tran_get_eq(EQ_SHIELD))) { std::string str = "Your "; str += item->name(DESC_BASENAME, false, false, false); str += " spins!"; messages.push_back(str); str = "Your "; str += item->name(DESC_BASENAME, false, false, false); str += " briefly flashes a lurid colour!"; messages.push_back(str); } if ((item = _tran_get_eq(EQ_BODY_ARMOUR))) { std::string str; std::string name = item->name(DESC_BASENAME, false, false, false); if (name.find("dragon") != std::string::npos) { str = "The scales on your "; str += name; str += " wiggle briefly."; } else if (item->sub_type == ARM_ANIMAL_SKIN) { str = "The fur on your "; str += name; str += " grows longer at an alarming rate, then retracts back " "to normal."; } else if (item->sub_type == ARM_LEATHER_ARMOUR) { str = "Your "; str += name; str += " briefly grows fur, then returns to normal."; } else if (item->sub_type == ARM_ROBE) { str = "You briefly become tangled in your "; str += pluralise(name); str += "."; } else if (item->sub_type >= ARM_RING_MAIL && item->sub_type <= ARM_PLATE_MAIL) { str = "Your "; str += name; str += " briefly appears rusty."; } if (!str.empty()) messages.push_back(str); } //////// // Misc. if (inv_items.size() > 0) { int idx = inv_items[random2(inv_items.size())]; item = &you.inv[idx]; std::string name = item->name(DESC_CAP_YOUR, false, false, false); std::string verb = coinflip() ? "glow" : "vibrate"; if (item->quantity == 1) verb += "s"; messages.push_back(name + " briefly " + verb + "."); } if (!priority.empty() && coinflip()) mpr(priority[random2(priority.size())].c_str()); else mpr(messages[random2(messages.size())].c_str()); } static void _get_hand_type(std::string &hand, bool &can_plural) { hand = ""; can_plural = true; const int transform = you.attribute[ATTR_TRANSFORMATION]; std::vector hand_vec; std::vector plural_vec; bool plural; hand_vec.push_back(you.hand_name(false, &plural)); plural_vec.push_back(plural); if (you.species != SP_NAGA || transform_changed_physiology()) { item_def* item; if ((item = _tran_get_eq(EQ_BOOTS)) && item->sub_type == ARM_BOOTS) { hand_vec.push_back("boot"); plural = true; } else hand_vec.push_back(you.foot_name(false, &plural)); plural_vec.push_back(plural); } if (transform == TRAN_SPIDER) { hand_vec.push_back("mandible"); plural_vec.push_back(true); } else if (you.species != SP_MUMMY && !player_mutation_level(MUT_BEAK) || transform_changed_physiology()) { hand_vec.push_back("nose"); plural_vec.push_back(false); } if (transform == TRAN_BAT || you.species != SP_MUMMY && !transform_changed_physiology()) { hand_vec.push_back("ear"); plural_vec.push_back(true); } if (!transform_changed_physiology()) { hand_vec.push_back("elbow"); plural_vec.push_back(true); } ASSERT(hand_vec.size() == plural_vec.size()); ASSERT(hand_vec.size() > 0); const unsigned int choice = random2(hand_vec.size()); hand = hand_vec[choice]; can_plural = plural_vec[choice]; } static int _xom_miscast(const int max_level, const bool nasty, bool debug = false) { ASSERT(max_level >= 0 && max_level <= 3); const char* speeches[4] = { "zero miscast effect", "minor miscast effect", "medium miscast effect", "major miscast effect" }; const char* causes[4] = { "the mischief of Xom", "the capriciousness of Xom", "the capriciousness of Xom", "the severe capriciousness of Xom" }; const char* speech_str = speeches[max_level]; const char* cause_str = causes[max_level]; const int level = (nasty ? 1 + random2(max_level) : random2(max_level + 1)); if (debug) { switch (level) { case 0: return (XOM_BAD_MISCAST_PSEUDO); case 1: return (XOM_BAD_MISCAST_MINOR); case 2: return (XOM_BAD_MISCAST_MAJOR); case 3: return (XOM_BAD_MISCAST_NASTY); } } // Take a note. std::string desc = "miscast effect"; #ifdef NOTE_DEBUG_XOM static char level_buf[20]; snprintf(level_buf, sizeof(level_buf), " level %d%s", level, (nasty ? " (nasty)" : "")); desc += level_buf; #endif take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, desc.c_str()), true); if (level == 0 && one_chance_in(20)) { god_speaks(GOD_XOM, _get_xom_speech(speech_str).c_str()); _xom_zero_miscast(); return (XOM_BAD_MISCAST_PSEUDO); } std::string hand_str; bool can_plural; _get_hand_type(hand_str, can_plural); // If Xom's not being nasty, then prevent spell miscasts from // killing the player. const int lethality_margin = nasty ? 0 : random_range(1, 4); god_speaks(GOD_XOM, _get_xom_speech(speech_str).c_str()); MiscastEffect(&you, -GOD_XOM, SPTYP_RANDOM, level, cause_str, NH_DEFAULT, lethality_margin, hand_str, can_plural); // Not worth distinguishing unless debugging. return (XOM_BAD_MISCAST_MAJOR); } static int _xom_lose_stats(bool debug = false) { if (debug) return (XOM_BAD_STATLOSS); stat_type stat = STAT_RANDOM; int max = 3; // Don't kill the player unless Xom is being nasty. if (!_xom_feels_nasty()) { // Make sure not to lower strength so much that the player // will die once might wears off. char vals[3] = {you.strength - (you.duration[DUR_MIGHT] ? 5 : 0), you.dex - (you.duration[DUR_AGILITY] ? 5 : 0), you.intel - (you.duration[DUR_BRILLIANCE] ? 5 : 0)}; stat_type types[3] = {STAT_STRENGTH, STAT_DEXTERITY, STAT_INTELLIGENCE}; int tries = 0; do { int idx = random2(3); stat = types[idx]; max = std::min(3, vals[idx] - 1); } while (max < 2 && (++tries < 30)); if (tries >= 30) return (XOM_DID_NOTHING); } god_speaks(GOD_XOM, _get_xom_speech("lose stats").c_str()); const int loss = 1 + random2(max); lose_stat(stat, loss, true, "the vengeance of Xom" ); // Take a note. static char stat_buf[80]; snprintf(stat_buf, sizeof(stat_buf), "stat loss: -%d %s (%d/%d)", loss, (stat == STAT_STRENGTH ? "Str" : stat == STAT_DEXTERITY ? "Dex" : "Int"), (stat == STAT_STRENGTH ? you.strength : stat == STAT_DEXTERITY ? you.dex : you.intel), (stat == STAT_STRENGTH ? you.max_strength : stat == STAT_DEXTERITY ? you.max_dex : you.max_intel)); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, stat_buf), true); return (XOM_BAD_STATLOSS); } static int _xom_chaos_upgrade_nearby_monster(bool debug = false) { monsters *mon = choose_random_nearby_monster(0, _choose_chaos_upgrade); if (!mon) return (XOM_DID_NOTHING); if (debug) return (XOM_BAD_CHAOS_UPGRADE); god_speaks(GOD_XOM, _get_xom_speech("chaos upgrade").c_str()); mon_inv_type slots[] = {MSLOT_WEAPON, MSLOT_ALT_WEAPON, MSLOT_MISSILE}; bool rc = false; for (int i = 0; i < 3 && !rc; ++i) { item_def* const item = mon->mslot_item(slots[i]); if (item && _is_chaos_upgradeable(*item, mon)) { _do_chaos_upgrade(*item, mon); rc = true; } } ASSERT(rc); // Wake the monster up. behaviour_event(mon, ME_ALERT, MHITYOU); if (rc) { take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "chaos upgrade"), true); return (XOM_BAD_CHAOS_UPGRADE); } return (XOM_DID_NOTHING); } static int _xom_player_confusion_effect(int sever, bool debug = false) { if (!_xom_feels_nasty()) { // Don't confuse the player if standing next to lava or deep water. for (adjacent_iterator ai(you.pos()); ai; ++ai) if (in_bounds(*ai) && is_feat_dangerous(grd(*ai))) return (XOM_DID_NOTHING); } if (debug) return (XOM_BAD_CONFUSION); bool rc = false; // Looks like this will *always* succeed? if (confuse_player(random2(sever) + 1, false)) { // FIXME: Message order is a bit off here. god_speaks(GOD_XOM, _get_xom_speech("confusion").c_str()); rc = true; // Sometimes Xom gets carried away and starts confusing // other creatures too. bool mons_too = false; if (coinflip()) { for (monster_iterator mi(&you.get_los()); mi; ++mi) { if (!mons_class_is_confusable(mi->type) || one_chance_in(20)) { continue; } if (mi->add_ench(mon_enchant(ENCH_CONFUSION, 0, KC_FRIENDLY, random2(sever)))) { simple_monster_message(*mi, " looks rather confused."); } mons_too = true; } } // Take a note. std::string conf_msg = "confusion"; if (mons_too) conf_msg += " (+ monsters)"; take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, conf_msg.c_str()), true); } return (rc ? XOM_BAD_CONFUSION : XOM_DID_NOTHING); } static bool _valid_floor_grid(coord_def pos) { if (!in_bounds(pos)) return (false); return (grd(pos) == DNGN_FLOOR); } bool move_stair(coord_def stair_pos, bool away, bool allow_under) { if (!allow_under) ASSERT(stair_pos != you.pos()); dungeon_feature_type feat = grd(stair_pos); ASSERT(feat_stair_direction(feat) != CMD_NO_CMD); coord_def begin, towards; bool stairs_moved = false; if (away) { // If the staircase starts out under the player, first shove it // onto a neighbouring grid. if (allow_under && stair_pos == you.pos()) { coord_def new_pos(stair_pos); // Loop twice through all adjacent grids. In the first round, // only consider grids whose next neighbour in the direction // away from the player is also of type floor. If we didn't // find any matching grid, try again without that restriction. for (int tries = 0; tries < 2; ++tries) { int adj_count = 0; for (adjacent_iterator ai(stair_pos); ai; ++ai) if (grd(*ai) == DNGN_FLOOR && (tries || _valid_floor_grid(*ai + *ai - stair_pos)) && one_chance_in(++adj_count)) { new_pos = *ai; } if (!tries && new_pos != stair_pos) break; } if (new_pos == stair_pos) return (false); if (!slide_feature_over(stair_pos, new_pos)) return (false); stair_pos = new_pos; stairs_moved = true; } begin = you.pos(); towards = stair_pos; } else { // Can't move towards player if it's already adjacent. if (adjacent(you.pos(), stair_pos)) return (false); begin = stair_pos; towards = you.pos(); } ray_def ray; if (!find_ray(begin, towards, ray)) { mpr("Couldn't find ray between player and stairs.", MSGCH_ERROR); return (stairs_moved); } // Don't start off under the player. if (away) ray.advance(); bool found_stairs = false; int past_stairs = 0; while (in_bounds(ray.pos()) && you.see_cell(ray.pos()) && !cell_is_solid(ray.pos()) && ray.pos() != you.pos()) { if (ray.pos() == stair_pos) found_stairs = true; if (found_stairs) past_stairs++; ray.advance(); } past_stairs--; if (!away && cell_is_solid(ray.pos())) { // Transparent wall between stair and player. return (stairs_moved); } if (away && !found_stairs) { if (cell_is_solid(ray.pos())) { // Transparent wall between stair and player. return (stairs_moved); } mpr("Ray didn't cross stairs.", MSGCH_ERROR); } if (away && past_stairs <= 0) { // Stairs already at edge, can't move further away. return (stairs_moved); } if (!in_bounds(ray.pos()) || ray.pos() == you.pos()) ray.regress(); while (!you.see_cell(ray.pos()) || grd(ray.pos()) != DNGN_FLOOR) { ray.regress(); if (!in_bounds(ray.pos()) || ray.pos() == you.pos() || ray.pos() == stair_pos) { // No squares in path are a plain floor. return (stairs_moved); } } ASSERT(stair_pos != ray.pos()); std::string stair_str = feature_description(stair_pos, false, DESC_CAP_THE, false); mprf("%s slides %s you!", stair_str.c_str(), away ? "away from" : "towards"); // Animate stair moving. const feature_def &feat_def = get_feature_def(feat); bolt beam; beam.range = INFINITE_DISTANCE; beam.flavour = BEAM_VISUAL; beam.type = feat_def.symbol; beam.colour = feat_def.colour; beam.source = stair_pos; beam.target = ray.pos(); beam.name = "STAIR BEAM"; beam.draw_delay = 50; // Make beam animation slower than normal. beam.aimed_at_spot = true; beam.fire(); // Clear out "missile trails" viewwindow(false); if (!swap_features(stair_pos, ray.pos(), false, false)) { mprf(MSGCH_ERROR, "_move_stair(): failed to move %s", stair_str.c_str()); return (stairs_moved); } return (true); } static int _xom_repel_stairs(bool debug = false) { // Repeating the effect while it's still active is boring. if (you.duration[DUR_REPEL_STAIRS_MOVE] || you.duration[DUR_REPEL_STAIRS_CLIMB]) { return (XOM_DID_NOTHING); } std::vector stairs_avail; bool real_stairs = false; for (radius_iterator ri(&you.get_los()); ri; ++ri) { dungeon_feature_type feat = grd(*ri); if (feat_stair_direction(feat) != CMD_NO_CMD && feat != DNGN_ENTER_SHOP) { stairs_avail.push_back(*ri); if (feat_is_staircase(feat)) real_stairs = true; } } // Should only happen if there are stairs in view. if (stairs_avail.empty()) return (XOM_DID_NOTHING); if (debug) return (XOM_BAD_STAIRS); // Don't mention staircases if there aren't any nearby. std::string stair_msg = _get_xom_speech("repel stairs"); if (stair_msg.find("@staircase@") != std::string::npos) { std::string feat_name; if (!real_stairs) { if (feat_is_escape_hatch(grd(stairs_avail[0]))) feat_name = "escape hatch"; else feat_name = "gate"; } else feat_name = "staircase"; stair_msg = replace_all(stair_msg, "@staircase@", feat_name); } god_speaks(GOD_XOM, stair_msg.c_str()); you.duration[DUR_REPEL_STAIRS_MOVE] = 1000; if (one_chance_in(5) || feat_stair_direction(grd(you.pos())) != CMD_NO_CMD && grd(you.pos()) != DNGN_ENTER_SHOP) { you.duration[DUR_REPEL_STAIRS_CLIMB] = 500; } std::random_shuffle(stairs_avail.begin(), stairs_avail.end()); int count_moved = 0; for (unsigned int i = 0; i < stairs_avail.size(); i++) if (move_stair(stairs_avail[i], true, true)) count_moved++; if (!count_moved) { if (one_chance_in(8)) mpr("Nothing appears to happen... Ominous!"); else canned_msg(MSG_NOTHING_HAPPENS); } else take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "repel stairs"), true); return (XOM_BAD_STAIRS); } static int _xom_draining_torment_effect(int sever, bool debug = false) { const std::string speech = _get_xom_speech("draining or torment"); const bool nasty = _xom_feels_nasty(); int rc = XOM_DID_NOTHING; if (one_chance_in(4)) { // XP drain effect (25%). if (player_prot_life() < 3 && (nasty || you.experience > 0)) { if (debug) return (XOM_BAD_DRAINING); god_speaks(GOD_XOM, speech.c_str()); drain_exp(); if (random2(sever) > 3 && (nasty || you.experience > 0)) drain_exp(); if (random2(sever) > 3 && (nasty || you.experience > 0)) drain_exp(); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "draining"), true); return (XOM_BAD_DRAINING); } } else { // Torment effect (75%). if (!player_res_torment()) { if (debug) return (XOM_BAD_TORMENT); god_speaks(GOD_XOM, speech.c_str()); torment_player(0, TORMENT_XOM); // Take a note. static char torment_buf[80]; snprintf(torment_buf, sizeof(torment_buf), "torment (%d/%d hp)", you.hp, you.hp_max); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, torment_buf), true); return (XOM_BAD_TORMENT); } } return (rc); } static bool _has_min_animated_weapon_level() { if (you.penance[GOD_XOM]) return (true); if (_xom_is_bored()) return (you.max_level >= 4); return (you.max_level >= 7); } static int _xom_summon_hostiles(int sever, bool debug = false) { bool rc = false; const std::string speech = _get_xom_speech("hostile monster"); int result = XOM_DID_NOTHING; // Nasty, but fun. if (player_weapon_wielded() && _has_min_animated_weapon_level() && one_chance_in(4)) { if (debug) return (XOM_BAD_ANIMATE_WPN); const item_def& weapon = *you.weapon(); const std::string wep_name = weapon.name(DESC_PLAIN); rc = cast_tukimas_dance(100, GOD_XOM, true); if (rc) { static char wpn_buf[80]; snprintf(wpn_buf, sizeof(wpn_buf), "animates weapon (%s)", wep_name.c_str()); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, wpn_buf), true); result = XOM_BAD_ANIMATE_WPN; } } else { if (debug) return (XOM_BAD_SUMMON_DEMONS); // The number of demons is dependent on severity, though heavily // randomised. int numdemons = sever; for (int i = 0; i < 3; ++i) numdemons = random2(numdemons + 1); numdemons = std::min(numdemons + 1, 14); // Limit number of demons by experience level. if (!you.penance[GOD_XOM]) { const int maxdemons = (you.max_level * 2); if (numdemons > maxdemons) numdemons = maxdemons; } int num_summoned = 0; for (int i = 0; i < numdemons; ++i) { if (create_monster( mgen_data::hostile_at( _xom_random_demon(sever), "Xom", true, 4, MON_SUMM_WRATH, you.pos(), 0, GOD_XOM)) != -1) { num_summoned++; } } if (num_summoned > 0) { static char summ_buf[80]; snprintf(summ_buf, sizeof(summ_buf), "summons %d hostile demon%s", num_summoned, num_summoned > 1 ? "s" : ""); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, summ_buf), true); rc = true; result = XOM_BAD_SUMMON_DEMONS; } } if (rc) god_speaks(GOD_XOM, speech.c_str()); return (result); } static bool _has_min_banishment_level() { return (you.max_level >= 9); } // Rolls whether banishment will be averted. static bool _will_not_banish() { return x_chance_in_y(5, you.max_level); } // Disallow early banishment and make it much rarer later-on. // While Xom is bored, the chance is increased. static bool _allow_xom_banishment() { // Always allowed if under penance. if (you.penance[GOD_XOM]) return (true); // If Xom is bored, banishment becomes viable earlier. if (_xom_is_bored()) return (!_will_not_banish()); // Below the minimum experience level, only fake banishment is allowed. if (!_has_min_banishment_level()) { // Allow banishment; it will be retracted right away. if (one_chance_in(5) && x_chance_in_y(you.piety, 1000)) return (true); else return (false); } else if (_will_not_banish()) return (false); return (true); } static int _xom_maybe_reverts_banishment(bool debug = false) { // Never revert if Xom is bored or the player is under penance. if (_xom_feels_nasty()) return XOM_BAD_BANISHMENT; // Sometimes Xom will immediately revert the banishment. // Always so, if the banishment happened below the minimum exp level. if (!_has_min_banishment_level() || x_chance_in_y(you.piety, 1000)) { if (!debug) { more(); god_speaks(GOD_XOM, _get_xom_speech("revert banishment").c_str()); down_stairs(you.your_level, DNGN_EXIT_ABYSS); take_note(Note(NOTE_XOM_EFFECT, you.piety, -1, "revert banishment"), true); } return XOM_BAD_PSEUDO_BANISHMENT; } return XOM_BAD_BANISHMENT; } static int _xom_do_banishment(bool debug = false) { if (!_allow_xom_banishment()) return (XOM_DID_NOTHING); if (debug) return _xom_maybe_reverts_banishment(debug); god_speaks(GOD_XOM, _get_xom_speech("banishment").c_str()); // Handles note taking. banished(DNGN_ENTER_ABYSS, "Xom"); const int result = _xom_maybe_reverts_banishment(debug); return (result); } static int _xom_is_bad(int sever, int tension, bool debug = false) { int done = XOM_DID_NOTHING; bool nasty = (sever >= 5 && _xom_feels_nasty()); god_acting gdact(GOD_XOM); // Rough estimate of how bad a Xom effect hits the player, // scaled between 1 (harmless) and 5 (disastrous). int badness = 1; while (done == XOM_DID_NOTHING) { // Did Xom kill the player? if (_player_is_dead()) return (XOM_PLAYER_DEAD); if (!nasty && x_chance_in_y(3, sever)) done = _xom_miscast(0, nasty, debug); else if (!nasty && x_chance_in_y(4, sever)) done = _xom_miscast(1, nasty, debug); // It's pointless to confuse player if there's no danger nearby. else if (tension > 0 && x_chance_in_y(5, sever)) { done = _xom_player_confusion_effect(sever, debug); badness = (random2(tension) > 5 ? 2 : 1); } else if (x_chance_in_y(6, sever)) { done = _xom_lose_stats(debug); badness = 2; } else if (tension > 0 && x_chance_in_y(7, sever)) done = _xom_swap_weapons(debug); else if (x_chance_in_y(8, sever)) { done = _xom_miscast(2, nasty, debug); badness = 2; } else if (x_chance_in_y(9, sever)) { done = _xom_chaos_upgrade_nearby_monster(debug); badness = 2 + coinflip(); } else if (x_chance_in_y(10, sever) && you.level_type != LEVEL_ABYSS) { // Try something else if teleportation is impossible. if (!_teleportation_check()) return (XOM_DID_NOTHING); // This is not particularly exciting if the level is already // fully explored (presumably cleared). If Xom is feeling // nasty, this is likelier to happen if the level is // unexplored. const int explored = _exploration_estimate(true); if (nasty && (explored >= 40 || tension > 10) || explored >= 60 + random2(40)) { done = XOM_DID_NOTHING; continue; } if (debug) return (XOM_BAD_TELEPORT); // The Xom teleportation train takes you on instant // teleportation to a few random areas, stopping if either // an area is dangerous to you or randomly. god_speaks(GOD_XOM, _get_xom_speech("teleportation journey").c_str()); int count = 0; do { you_teleport_now(false); more(); if (count++ >= 7 + random2(5)) break; } while (x_chance_in_y(3, 4) && !player_in_a_dangerous_place()); maybe_update_stashes(); badness = player_in_a_dangerous_place() ? 3 : 1; // Take a note. static char tele_buf[80]; snprintf(tele_buf, sizeof(tele_buf), "%d-stop teleportation journey%s", count, #ifdef NOTE_DEBUG_XOM badness == 3 ? " (dangerous)" : ""); #else ""); #endif take_note(Note(NOTE_XOM_EFFECT, you.piety, tension, tele_buf), true); done = XOM_BAD_TELEPORT; } else if (x_chance_in_y(11, sever)) { done = _xom_polymorph_nearby_monster(false, debug); badness = 3; } else if (tension > 0 && x_chance_in_y(12, sever)) { done = _xom_repel_stairs(debug); badness = (you.duration[DUR_REPEL_STAIRS_CLIMB] ? 3 : 2); } else if (random2(tension) < 11 && x_chance_in_y(13, sever)) { done = _xom_give_mutations(false, debug); badness = 3; } else if (x_chance_in_y(14, sever)) { done = _xom_draining_torment_effect(sever, debug); badness = (random2(tension) > 5 ? 3 : 2); } else if (x_chance_in_y(15, sever)) { done = _xom_summon_hostiles(sever, debug); badness = 3 + coinflip(); } else if (x_chance_in_y(16, sever)) { done = _xom_miscast(3, nasty, debug); badness = 4 + coinflip(); } else if (one_chance_in(sever) && you.level_type != LEVEL_ABYSS) { done = _xom_do_banishment(debug); badness = (done == XOM_BAD_BANISHMENT ? 5 : 1); } } // If we got here because Xom was bored, reset gift timeout according // to the badness of the effect. if (done && !debug && _xom_is_bored()) { const int interest = random2avg(badness*60, 2); you.gift_timeout = std::min(interest, 255); #if defined(DEBUG_RELIGION) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "badness: %d, new interest: %d", badness, you.gift_timeout); #endif } return (done); } static char* _stat_ptrs[3] = {&you.strength, &you.intel, &you.dex}; static void _handle_accidental_death(const int orig_hp, const char orig_stats[], const FixedVector &orig_mutation) { // Did ouch() return early because the player died from the Xom // effect, even though neither is the player under penance nor is // Xom bored? if (!you.did_escape_death() && you.escaped_death_aux.empty() && !_player_is_dead()) { // The player is fine. return; } std::string speech_type = "accidental homicide"; const dungeon_feature_type feat = grd(you.pos()); switch (you.escaped_death_cause) { case NUM_KILLBY: case KILLED_BY_LEAVING: case KILLED_BY_WINNING: case KILLED_BY_QUITTING: speech_type = "weird death"; break; case KILLED_BY_LAVA: case KILLED_BY_WATER: if (!_feat_is_deadly(feat)) speech_type = "weird death"; break; case KILLED_BY_STUPIDITY: if (you.intel > 0) speech_type = "weird death"; break; case KILLED_BY_WEAKNESS: if (you.strength > 0) speech_type = "weird death"; break; case KILLED_BY_CLUMSINESS: if (you.dex > 0) speech_type = "weird death"; break; default: if (_feat_is_deadly(feat)) speech_type = "weird death"; if (you.strength <= 0 || you.intel <= 0 || you.dex <= 0) speech_type = "weird death"; break; } mpr("You die..."); god_speaks(GOD_XOM, _get_xom_speech(speech_type).c_str()); god_speaks(GOD_XOM, _get_xom_speech("resurrection").c_str()); if (you.hp <= 0) you.hp = std::min(orig_hp, you.hp_max); mutation_type dex_muts[5] = {MUT_GREY2_SCALES, MUT_METALLIC_SCALES, MUT_YELLOW_SCALES, MUT_RED2_SCALES, MUT_STRONG_STIFF}; for (int i = 0; i < 5; ++i) { mutation_type bad = dex_muts[i]; while (you.dex <= 0 && you.mutation[bad] > orig_mutation[bad]) delete_mutation(bad, true, true, true); } while (you.dex <= 0 && you.mutation[MUT_FLEXIBLE_WEAK] < orig_mutation[MUT_FLEXIBLE_WEAK]) { mutate(MUT_FLEXIBLE_WEAK, true, true, true); } while (you.strength <= 0 && you.mutation[MUT_FLEXIBLE_WEAK] > orig_mutation[MUT_FLEXIBLE_WEAK]) { delete_mutation(MUT_FLEXIBLE_WEAK, true, true, true); } while (you.strength <= 0 && you.mutation[MUT_STRONG_STIFF] < orig_mutation[MUT_STRONG_STIFF]) { mutate(MUT_STRONG_STIFF, true, true, true); } mutation_type bad_muts[3] = {MUT_WEAK, MUT_DOPEY, MUT_CLUMSY}; mutation_type good_muts[3] = {MUT_STRONG, MUT_CLEVER, MUT_AGILE}; for (int i = 0; i < 3; ++i) { while (*(_stat_ptrs[i]) <= 0) { mutation_type good = good_muts[i]; mutation_type bad = bad_muts[i]; if (you.mutation[bad] > orig_mutation[bad] || you.mutation[good] < orig_mutation[good]) { mutate(good, true, true, true); } else { *(_stat_ptrs[i]) = orig_stats[i]; break; } } } you.max_strength = std::max(you.max_strength, you.strength); you.max_intel = std::max(you.max_intel, you.intel); you.max_dex = std::max(you.max_dex, you.dex); if (_feat_is_deadly(feat)) you_teleport_now(false); } int xom_acts(bool niceness, int sever, int tension, bool debug) { #if defined(DEBUG_DIAGNOSTICS) || defined(DEBUG_RELIGION) || defined(DEBUG_XOM) mprf(MSGCH_DIAGNOSTICS, "xom_acts(%u, %d, %d); piety: %u, interest: %u\n", niceness, sever, tension, you.piety, you.gift_timeout); #endif #ifdef WIZARD if (_player_is_dead()) { // This should only happen if the player used wizard mode to // escape from death via stat loss, or if the player used wizard // mode to escape death from deep water or lava. ASSERT(you.wizard && !you.did_escape_death()); if (_feat_is_deadly(grd(you.pos()))) { mpr("Player is standing in deadly terrain, skipping Xom act.", MSGCH_DIAGNOSTICS); } else { mpr("Player is already dead, skipping Xom act.", MSGCH_DIAGNOSTICS); } return (XOM_PLAYER_DEAD); } #else ASSERT(!_player_is_dead()); #endif entry_cause_type old_entry_cause = you.entry_cause; sever = std::max(1, sever); god_type which_god = GOD_XOM; // Drawing the Xom card from Nemelex's decks of oddities or punishment. if (crawl_state.is_god_acting() && crawl_state.which_god_acting() != GOD_XOM) { which_god = crawl_state.which_god_acting(); if (crawl_state.is_god_retribution()) { niceness = false; simple_god_message(" asks Xom for help in punishing you, and " "Xom happily agrees.", which_god); } else { niceness = true; simple_god_message(" calls in a favour from Xom.", which_god); } } if (tension == -1) tension = get_tension(which_god); #if defined(DEBUG_RELIGION) || defined(DEBUG_XOM) || defined(DEBUG_TENSION) mprf(MSGCH_DIAGNOSTICS, "Xom tension: %d", tension); #endif const int orig_hp = you.hp; const char orig_stats[3] = {*(_stat_ptrs[0]), *(_stat_ptrs[1]), *(_stat_ptrs[2])}; const FixedVector orig_mutation = you.mutation; #ifdef NOTE_DEBUG_XOM static char xom_buf[100]; snprintf(xom_buf, sizeof(xom_buf), "xom_acts(%s, %d, %d), mood: %d", (niceness ? "true" : "false"), sever, tension, you.piety); take_note(Note(NOTE_MESSAGE, 0, 0, xom_buf), true); #endif const bool was_bored = _xom_is_bored(); const bool good_act = niceness && !one_chance_in(20); int result = XOM_DID_NOTHING; if (good_act) { // Make good acts at zero tension less likely, especially if Xom // is in a bad mood. if (tension == 0 && !x_chance_in_y(you.piety, MAX_PIETY)) { #ifdef NOTE_DEBUG_XOM take_note(Note(NOTE_MESSAGE, 0, 0, "suppress good act because of " "zero tension"), true); #endif return (debug ? XOM_GOOD_NOTHING : XOM_DID_NOTHING); } // Good stuff. while (result == XOM_DID_NOTHING) result = _xom_is_good(sever, tension, debug); if (debug) return (result); } else { if (!debug && was_bored && Options.note_xom_effects) take_note(Note(NOTE_MESSAGE, 0, 0, "XOM is BORED!"), true); #ifdef NOTE_DEBUG_XOM else if (niceness) { take_note(Note(NOTE_MESSAGE, 0, 0, "good act randomly turned bad"), true); } #endif // Make bad acts at non-zero tension less likely, especially if Xom // is in a good mood. if (!_xom_feels_nasty() && tension > random2(10) && x_chance_in_y(you.piety, MAX_PIETY)) { #ifdef NOTE_DEBUG_XOM snprintf(info, INFO_SIZE, "suppress bad act because of %d tension", tension); take_note(Note(NOTE_MESSAGE, 0, 0, info), true); #endif return (debug ? XOM_BAD_NOTHING : XOM_DID_NOTHING); } // Bad mojo. while (result == XOM_DID_NOTHING) result = _xom_is_bad(sever, tension, debug); if (debug) return (result); } _handle_accidental_death(orig_hp, orig_stats, orig_mutation); // Drawing the Xom card from Nemelex's decks of oddities or punishment. if (crawl_state.is_god_acting() && crawl_state.which_god_acting() != GOD_XOM) { if (old_entry_cause != you.entry_cause && you.entry_cause_god == GOD_XOM) { you.entry_cause_god = crawl_state.which_god_acting(); } } if (you.religion == GOD_XOM && one_chance_in(5)) { const std::string old_xom_favour = describe_xom_favour(); you.piety = random2(MAX_PIETY + 1); const std::string new_xom_favour = describe_xom_favour(); if (was_bored || old_xom_favour != new_xom_favour) { const std::string msg = "You are now " + new_xom_favour; god_speaks(you.religion, msg.c_str()); } #ifdef NOTE_DEBUG_XOM snprintf(info, INFO_SIZE, "reroll piety: %d", you.piety); take_note(Note(NOTE_MESSAGE, 0, 0, info), true); #endif } else if (was_bored) { // If we didn't reroll at least mention the new favour // now it's not "BORING thing" anymore. const std::string new_xom_favour = describe_xom_favour(); const std::string msg = "You are now " + new_xom_favour; god_speaks(you.religion, msg.c_str()); } // Not true, but also not important now. return (result); } static void _xom_check_less_runes(int runes_gone) { if (you.opened_zot || player_in_branch(BRANCH_HALL_OF_ZOT) || !(branches[BRANCH_HALL_OF_ZOT].branch_flags & BFLAG_HAS_ORB)) { return; } int runes_avail = you.attribute[ATTR_UNIQUE_RUNES] + you.attribute[ATTR_DEMONIC_RUNES] + you.attribute[ATTR_ABYSSAL_RUNES]; int was_avail = runes_avail + runes_gone; // No longer enough available runes to get into Zot. if (was_avail >= NUMBER_OF_RUNES_NEEDED && runes_avail < NUMBER_OF_RUNES_NEEDED) { xom_is_stimulated(128, "Xom snickers.", true); } } void xom_check_lost_item(const item_def& item) { if (item.base_type == OBJ_ORBS) xom_is_stimulated(255, "Xom laughs nastily.", true); else if (is_special_unrandom_artefact(item)) xom_is_stimulated(128, "Xom snickers.", true); else if (is_rune(item)) { // If you'd dropped it, check if that means you'd dropped your // third rune, and now you don't have enough to get into Zot. if (item.flags & ISFLAG_BEEN_IN_INV) _xom_check_less_runes(item.quantity); if (is_unique_rune(item)) xom_is_stimulated(255, "Xom snickers loudly.", true); else if (you.entry_cause == EC_SELF_EXPLICIT && !(item.flags & ISFLAG_BEEN_IN_INV)) { // Player voluntarily entered Pan or the Abyss looking for // runes, yet never found them. if (item.plus == RUNE_ABYSSAL && you.attribute[ATTR_ABYSSAL_RUNES] == 0) { // Ignore Abyss area shifts. if (you.level_type != LEVEL_ABYSS) { // Abyssal runes are a lot more trouble to find than // demonic runes, so they get twice the stimulation. xom_is_stimulated(128, "Xom snickers.", true); } } else if (item.plus == RUNE_DEMONIC && you.attribute[ATTR_DEMONIC_RUNES] == 0) { xom_is_stimulated(64, "Xom snickers softly.", true); } } } } void xom_check_destroyed_item(const item_def& item, int cause) { int amusement = 0; if (item.base_type == OBJ_ORBS) { xom_is_stimulated(255, "Xom laughs nastily.", true); return; } else if (is_special_unrandom_artefact(item)) xom_is_stimulated(128, "Xom snickers.", true); else if (is_rune(item)) { _xom_check_less_runes(item.quantity); if (is_unique_rune(item) || item.plus == RUNE_ABYSSAL) amusement = 255; else amusement = 64 * item.quantity; } xom_is_stimulated(amusement, (amusement > 128) ? "Xom snickers loudly." : (amusement > 64) ? "Xom snickers." : "Xom snickers softly.", true); } static bool _death_is_funny(const kill_method_type killed_by) { switch (killed_by) { // The less original deaths are considered boring. case KILLED_BY_MONSTER: case KILLED_BY_BEAM: case KILLED_BY_CLOUD: case KILLED_BY_FREEZING: case KILLED_BY_BURNING: case KILLED_BY_SELF_AIMED: case KILLED_BY_SOMETHING: case KILLED_BY_TRAP: return (false); default: // All others are fun (says Xom). return (true); } } void xom_death_message(const kill_method_type killed_by) { if (you.religion != GOD_XOM && (!you.worshipped[GOD_XOM] || coinflip())) return; const int death_tension = get_tension(GOD_XOM, false); // "Normal" deaths with only down to -2 hp and comparatively low tension // are considered particularly boring. if (!_death_is_funny(killed_by) && you.hp >= -1 * random2(3) && death_tension <= random2(10)) { god_speaks(GOD_XOM, _get_xom_speech("boring death").c_str()); } // Unusual methods of dying, really low hp, or high tension make // for funny deaths. else if (_death_is_funny(killed_by) || you.hp <= -10 || death_tension >= 20) { god_speaks(GOD_XOM, _get_xom_speech("laughter").c_str()); } // All others just get ignored by Xom. } static int _death_is_worth_saving(const kill_method_type killed_by, const char *aux) { switch (killed_by) { // These don't count. case KILLED_BY_LEAVING: case KILLED_BY_WINNING: case KILLED_BY_QUITTING: // These are too much hassle. case KILLED_BY_LAVA: case KILLED_BY_WATER: case KILLED_BY_DRAINING: case KILLED_BY_STARVATION: case KILLED_BY_ROTTING: // Don't protect the player from these. case KILLED_BY_SELF_AIMED: case KILLED_BY_TARGETTING: return (false); // Only if not caused by equipment. case KILLED_BY_STUPIDITY: case KILLED_BY_WEAKNESS: case KILLED_BY_CLUMSINESS: if (strstr(aux, "wielding") == NULL && strstr(aux, "wearing") == NULL && strstr(aux, "removing") == NULL) { return (true); } return (false); // Everything else is fair game. default: return (true); } } static std::string _get_death_type_keyword(const kill_method_type killed_by) { switch (killed_by) { case KILLED_BY_MONSTER: case KILLED_BY_BEAM: case KILLED_BY_BEOGH_SMITING: case KILLED_BY_TSO_SMITING: case KILLED_BY_DIVINE_WRATH: return "actor"; default: return "general"; } } bool xom_saves_your_life(const int dam, const int death_source, const kill_method_type death_type, const char *aux, bool see_source) { if (you.religion != GOD_XOM || _xom_feels_nasty()) return (false); // If this happens, don't bother. if (you.hp_max < 1 || you.experience_level < 1) return (false); // Generally a rare effect. if (!one_chance_in(20)) return (false); if (!_death_is_worth_saving(death_type, aux)) return (false); // In addition, the chance depends on the current tension and Xom's mood. const int death_tension = get_tension(GOD_XOM, false); if (death_tension < random2(5) || !xom_is_nice(death_tension)) return (false); // Fake death message. mpr("You die..."); more(); const std::string key = _get_death_type_keyword(death_type); std::string speech = _get_xom_speech("life saving " + key); if (speech.find("@xom_plaything@") != std::string::npos) { std::string toy_name = (you.piety > 180) ? "teddy bear" : (you.piety > 80) ? "toy" : "plaything"; speech = replace_all(speech, "@xom_plaything@", toy_name); } god_speaks(GOD_XOM, speech.c_str()); // Give back some hp. if (you.hp < 1) you.hp = 1 + random2(you.hp_max/4); // Make sure all stats are at least 1. if (you.strength < 1) you.strength = 1; if (you.dex < 1) you.dex = 1; if (you.intel < 1) you.intel = 1; god_speaks(GOD_XOM, "Xom revives you!"); // Ideally, this should contain the death cause but that is too much // trouble for now. take_note( Note(NOTE_XOM_REVIVAL) ); // Make sure Xom doesn't get bored within the next couple of turns. if (you.gift_timeout < 10) you.gift_timeout = 10; return (true); } #ifdef WIZARD struct xom_effect_count { std::string effect; int count; xom_effect_count(std::string e, int c) : effect(e), count(c) {}; }; static bool _sort_xom_effects(const xom_effect_count &a, const xom_effect_count &b) { if (a.count == b.count) return (a.effect < b.effect); return (a.count > b.count); } static const char* _xom_effect_to_name(int effect) { ASSERT(effect < XOM_PLAYER_DEAD); // See xom.h static const char* _xom_effect_names[] = { "bugginess", // good acts "nothing", "potion", "spell (tension)", "spell (no tension)", "mapping", "confuse monsters", "single ally", "animate monster weapon", "annoyance gift", "random item gift", "acquirement", "summon allies", "polymorph", "swap monsters", "teleportation", "vitrification", "mutation", "permanent ally", "lightning", "change scenery", // bad acts "nothing", "miscast (pseudo)", "miscast (minor)", "miscast (major)", "miscast (nasty)", "stat loss", "teleportation", "swap weapons", "chaos upgrade", "mutation", "polymorph", "repel stairs", "confusion", "draining", "torment", "animate weapon", "summon demons", "banishment (pseudo)", "banishment" }; std::string result = ""; if (effect > XOM_DID_NOTHING && effect < XOM_PLAYER_DEAD) { if (effect <= XOM_LAST_GOOD_ACT) result = "GOOD: "; else result = "BAD: "; } result += _xom_effect_names[effect]; return (result.c_str()); } // Loops over the entire piety spectrum and calls xom_acts() multiple // times for each value, then prints the results into a file. // TODO: Allow specification of niceness, tension, boredness, and repeats. void debug_xom_effects() { // Repeat N times. const int N = debug_prompt_for_int("How many iterations? ", true); if (N == 0) { canned_msg( MSG_OK ); return; } FILE *ostat = fopen("xom_debug.stat", "a"); if (!ostat) { mprf(MSGCH_ERROR, "Can't write 'xom_debug.stat'. Aborting."); return; } const int real_piety = you.piety; const god_type real_god = you.religion; you.religion = GOD_XOM; const int tension = get_tension(GOD_XOM); fprintf(ostat, "---- STARTING XOM DEBUG TESTING ----\n"); fprintf(ostat, "%s\n", dump_overview_screen(false).c_str()); fprintf(ostat, "%s", screenshot().c_str()); fprintf(ostat, "\n%s\n", mpr_monster_list().c_str()); fprintf(ostat, " --> Tension: %d\n", tension); if (you.penance[GOD_XOM]) fprintf(ostat, "You are under Xom's penance!\n"); else if (_xom_is_bored()) fprintf(ostat, "Xom is BORED.\n"); fprintf(ostat, "\nRunning %d times through entire mood cycle.\n", N); fprintf(ostat, "---- OUTPUT EFFECT PERCENTAGES ----\n"); std::vector mood_effects; std::vector > all_effects; std::vector moods; std::vector mood_good_acts; std::string old_mood = ""; std::string mood = ""; // Add an empty list to later add all effects to. all_effects.push_back(mood_effects); moods.push_back("total"); mood_good_acts.push_back(0); // count total good acts int mood_good = 0; for (int p = 0; p <= MAX_PIETY; ++p) { you.piety = p; int sever = abs(p - HALF_MAX_PIETY); mood = describe_xom_mood(); if (old_mood != mood) { if (old_mood != "") { all_effects.push_back(mood_effects); mood_effects.clear(); mood_good_acts.push_back(mood_good); mood_good_acts[0] += mood_good; mood_good = 0; } moods.push_back(mood); old_mood = mood; } // Repeat N times. for (int i = 0; i < N; ++i) { const bool niceness = xom_is_nice(tension); const int result = xom_acts(niceness, sever, tension, true); mood_effects.push_back(result); all_effects[0].push_back(result); if (result <= XOM_LAST_GOOD_ACT) mood_good++; } } all_effects.push_back(mood_effects); mood_effects.clear(); mood_good_acts.push_back(mood_good); mood_good_acts[0] += mood_good; const int num_moods = moods.size(); std::vector xom_ec_pairs; for (int i = 0; i < num_moods; ++i) { mood_effects = all_effects[i]; const int total = mood_effects.size(); if (i == 0) fprintf(ostat, "\nTotal effects (all piety ranges)\n"); else fprintf(ostat, "\nMood: You are %s\n", moods[i].c_str()); fprintf(ostat, "GOOD%7.2f%%\n", (100.0 * (float) mood_good_acts[i] / (float) total)); fprintf(ostat, "BAD %7.2f%%\n", (100.0 * (float) (total - mood_good_acts[i]) / (float) total)); std::sort(mood_effects.begin(), mood_effects.end()); xom_ec_pairs.clear(); int old_effect = XOM_DID_NOTHING; int count = 0; for (int k = 0; k < total; ++k) { if (mood_effects[k] != old_effect) { if (count > 0) { std::string name = _xom_effect_to_name(old_effect); xom_effect_count xec = xom_effect_count(name, count); xom_ec_pairs.push_back(xec); } old_effect = mood_effects[k]; count = 1; } else count++; } if (count > 0) { std::string name = _xom_effect_to_name(old_effect); xom_effect_count xec = xom_effect_count(name, count); xom_ec_pairs.push_back(xec); } std::sort(xom_ec_pairs.begin(), xom_ec_pairs.end(), _sort_xom_effects); for (unsigned int k = 0; k < xom_ec_pairs.size(); ++k) { xom_effect_count xec = xom_ec_pairs[k]; fprintf(ostat, "%7.2f%% %s\n", (100.0 * (float) xec.count / (float) total), xec.effect.c_str()); } } fprintf(ostat, "---- FINISHED XOM DEBUG TESTING ----\n"); fclose(ostat); mpr("Results written into 'xom_debug.stat'."); you.piety = real_piety; you.religion = real_god; } #endif // WIZARD