/* * File: mon-stuff.cc * Summary: Misc monster related functions. * Written by: Linley Henzell */ #include "AppHdr.h" #include "mon-stuff.h" #include "arena.h" #include "artefact.h" #include "attitude-change.h" #include "cloud.h" #include "cluautil.h" #include "coordit.h" #include "database.h" #include "delay.h" #include "dgnevent.h" #include "directn.h" #include "dlua.h" #include "exclude.h" #include "fprop.h" #include "files.h" #include "food.h" #include "godabil.h" #include "hiscores.h" #include "itemname.h" #include "itemprop.h" #include "items.h" #include "kills.h" #include "makeitem.h" #include "message.h" #include "misc.h" #include "mon-abil.h" #include "mon-behv.h" #include "mon-iter.h" #include "mon-place.h" #include "mgen_data.h" #include "coord.h" #include "mon-speak.h" #include "notes.h" #include "player.h" #include "random.h" #include "religion.h" #include "spl-mis.h" #include "spl-util.h" #include "state.h" #include "stuff.h" #include "env.h" #include "areas.h" #include "terrain.h" #include "transform.h" #include "traps.h" #include "tutorial.h" #include "view.h" #include "shout.h" #include "spells3.h" #include "viewchar.h" #include "stash.h" #include "xom.h" static bool _wounded_damaged(monster_type mon_type); int _make_mimic_item(monster_type type) { int it = items(0, OBJ_UNASSIGNED, 0, true, 0, 0); if (it == NON_ITEM) return NON_ITEM; item_def &item = mitm[it]; item.base_type = OBJ_UNASSIGNED; item.sub_type = 0; item.special = 0; item.colour = 0; item.flags = 0; item.quantity = 1; item.plus = 0; item.plus2 = 0; item.link = NON_ITEM; int prop; switch (type) { case MONS_WEAPON_MIMIC: item.base_type = OBJ_WEAPONS; item.sub_type = random2(WPN_MAX_NONBLESSED + 1); prop = random2(100); if (prop < 20) make_item_randart(item); else if (prop < 50) set_equip_desc(item, ISFLAG_GLOWING); else if (prop < 80) set_equip_desc(item, ISFLAG_RUNED); else if (prop < 85) set_equip_race(item, ISFLAG_ORCISH); else if (prop < 90) set_equip_race(item, ISFLAG_DWARVEN); else if (prop < 95) set_equip_race(item, ISFLAG_ELVEN); break; case MONS_ARMOUR_MIMIC: item.base_type = OBJ_ARMOUR; item.sub_type = random2(NUM_ARMOURS); prop = random2(100); if (prop < 20) make_item_randart(item); else if (prop < 40) set_equip_desc(item, ISFLAG_GLOWING); else if (prop < 60) set_equip_desc(item, ISFLAG_RUNED); else if (prop < 80) set_equip_desc(item, ISFLAG_EMBROIDERED_SHINY); else if (prop < 85) set_equip_race(item, ISFLAG_ORCISH); else if (prop < 90) set_equip_race(item, ISFLAG_DWARVEN); else if (prop < 95) set_equip_race(item, ISFLAG_ELVEN); break; case MONS_SCROLL_MIMIC: item.base_type = OBJ_SCROLLS; item.sub_type = random2(NUM_SCROLLS); break; case MONS_POTION_MIMIC: item.base_type = OBJ_POTIONS; do item.sub_type = random2(NUM_POTIONS); while (is_blood_potion(item)); break; case MONS_GOLD_MIMIC: default: item.base_type = OBJ_GOLD; item.quantity = 5 + random2(1000); break; } item_colour(item); // also sets special vals for scrolls/potions return (it); } const item_def *give_mimic_item(monsters *mimic) { ASSERT(mimic != NULL && mons_is_mimic(mimic->type)); mimic->destroy_inventory(); int it = _make_mimic_item(mimic->type); if (it == NON_ITEM) return 0; if (!mimic->pickup_misc(mitm[it], 0)) ASSERT("Mimic failed to pickup its item."); ASSERT(mimic->inv[MSLOT_MISCELLANY] != NON_ITEM); return (&mitm[mimic->inv[MSLOT_MISCELLANY]]); } const item_def &get_mimic_item(const monsters *mimic) { ASSERT(mimic != NULL && mons_is_mimic(mimic->type)); ASSERT(mimic->inv[MSLOT_MISCELLANY] != NON_ITEM); return (mitm[mimic->inv[MSLOT_MISCELLANY]]); } // Sets the colour of a mimic to match its description... should be called // whenever a mimic is created or teleported. -- bwr int get_mimic_colour( const monsters *mimic ) { ASSERT(mimic != NULL); return (get_mimic_item(mimic).colour); } // Monster curses a random player inventory item. bool curse_an_item( bool decay_potions, bool quiet ) { int count = 0; int item = ENDOFPACK; for (int i = 0; i < ENDOFPACK; i++) { if (!you.inv[i].is_valid()) continue; if (you.inv[i].base_type == OBJ_WEAPONS || you.inv[i].base_type == OBJ_ARMOUR || you.inv[i].base_type == OBJ_JEWELLERY || you.inv[i].base_type == OBJ_POTIONS) { if (you.inv[i] .cursed()) continue; if (you.inv[i].base_type != OBJ_POTIONS && !you_tran_can_wear(you.inv[i]) && item_is_equipped(you.inv[i])) { // Melded items cannot be cursed. continue; } if (you.inv[i].base_type == OBJ_POTIONS && (!decay_potions || you.inv[i].sub_type == POT_DECAY)) { continue; } // Item is valid for cursing, so we'll give it a chance. count++; if (one_chance_in( count )) item = i; } } // Any item to curse? if (item == ENDOFPACK) return (false); // Curse item. if (decay_potions && !quiet) // Just for mummies. mpr("You feel nervous for a moment...", MSGCH_MONSTER_SPELL); if (you.inv[item].base_type == OBJ_POTIONS) { int amount; // Decay at least two of the stack. if (you.inv[item].quantity <= 2) amount = you.inv[item].quantity; else amount = 2 + random2(you.inv[item].quantity - 1); split_potions_into_decay(item, amount); if (item_value(you.inv[item], true) / amount > 2) xom_is_stimulated(32 * amount); } else { do_curse_item( you.inv[item], false ); } return (true); } void monster_drop_ething(monsters *monster, bool mark_item_origins, int owner_id) { // Drop weapons & missiles last (ie on top) so others pick up. for (int i = NUM_MONSTER_SLOTS - 1; i >= 0; i--) { int item = monster->inv[i]; if (item != NON_ITEM) { const bool summoned_item = testbits(mitm[item].flags, ISFLAG_SUMMONED); if (summoned_item) { item_was_destroyed(mitm[item], monster->mindex()); destroy_item( item ); } else { if (monster->friendly() && mitm[item].is_valid()) mitm[item].flags |= ISFLAG_DROPPED_BY_ALLY; if (mark_item_origins && mitm[item].is_valid()) origin_set_monster(mitm[item], monster); // If a monster is swimming, the items are ALREADY underwater move_item_to_grid(&item, monster->pos(), monster->swimming()); } monster->inv[i] = NON_ITEM; } } } monster_type fill_out_corpse(const monsters* monster, item_def& corpse, bool allow_weightless) { ASSERT(!invalid_monster_type(monster->type)); corpse.clear(); int summon_type; if (monster->is_summoned(NULL, &summon_type) || (monster->flags & (MF_BANISHED | MF_HARD_RESET))) { return (MONS_NO_MONSTER); } monster_type corpse_class = mons_species(monster->type); // If this was a corpse that was temporarily animated then turn the // monster back into a corpse. if (mons_class_is_zombified(monster->type) && (summon_type == SPELL_ANIMATE_DEAD || summon_type == SPELL_ANIMATE_SKELETON || summon_type == MON_SUMM_ANIMATE)) { corpse_class = mons_zombie_base(monster); } if (corpse_class == MONS_DRACONIAN) { if (monster->type == MONS_TIAMAT) corpse_class = MONS_DRACONIAN; else corpse_class = draco_subspecies(monster); } if (monster->has_ench(ENCH_SHAPESHIFTER)) corpse_class = MONS_SHAPESHIFTER; else if (monster->has_ench(ENCH_GLOWING_SHAPESHIFTER)) corpse_class = MONS_GLOWING_SHAPESHIFTER; // Doesn't leave a corpse. if (mons_weight(corpse_class) == 0 && !allow_weightless) return (MONS_NO_MONSTER); corpse.flags = 0; corpse.base_type = OBJ_CORPSES; corpse.plus = corpse_class; corpse.plus2 = 0; // butcher work done corpse.sub_type = CORPSE_BODY; corpse.special = FRESHEST_CORPSE; // rot time corpse.quantity = 1; corpse.orig_monnum = monster->type + 1; corpse.props[MONSTER_NUMBER] = short(monster->number); corpse.colour = mons_class_colour(corpse_class); if (corpse.colour == BLACK) corpse.colour = monster->colour; if (!monster->mname.empty()) { corpse.props[CORPSE_NAME_KEY] = monster->mname; corpse.props[CORPSE_NAME_TYPE_KEY] = (long) (monster->flags & MF_NAME_MASK); } else if (mons_is_unique(monster->type)) { corpse.props[CORPSE_NAME_KEY] = mons_type_name(monster->type, DESC_PLAIN); corpse.props[CORPSE_NAME_TYPE_KEY] = (long) 0; } return (corpse_class); } bool explode_corpse(item_def& corpse, const coord_def& where) { // Don't want chunks to show up behind the player. los_def ld(where, opc_no_actor); if (monster_descriptor(corpse.plus, MDSC_LEAVES_HIDE) && mons_genus(corpse.plus) == MONS_DRAGON) { // Uh... dragon hide is tough stuff and it keeps the monster in // one piece? More importantly, it prevents a flavor feature // from becoming a trap for the unwary. return (false); } ld.update(); int nchunks = 1 + random2(mons_weight(corpse.plus) / 150); nchunks = stepdown_value(nchunks, 4, 4, 12, 12); int ntries = 0; corpse.base_type = OBJ_FOOD; corpse.sub_type = FOOD_CHUNK; int blood = nchunks * 3; if (food_is_rotten(corpse)) blood /= 3; blood_spray(where, static_cast(corpse.plus), blood); while (nchunks > 0 && ntries < 10000) { ++ntries; coord_def cp = where; cp.x += random_range(-LOS_RADIUS, LOS_RADIUS); cp.y += random_range(-LOS_RADIUS, LOS_RADIUS); dprf("Trying to scatter chunk to %d, %d...", cp.x, cp.y); if (! in_bounds(cp)) continue; if (! ld.see_cell(cp)) continue; dprf("Cell is visible..."); if (feat_is_solid(grd(cp)) || actor_at(cp)) continue; --nchunks; dprf("Success"); copy_item_to_grid(corpse, cp); } return (true); } // Returns the item slot of a generated corpse, or -1 if no corpse. int place_monster_corpse(const monsters *monster, bool silent, bool force) { // The game can attempt to place a corpse for an out-of-bounds monster // if a shifter turns into a giant spore and explodes. In this // case we place no corpse since the explosion means anything left // over would be scattered, tiny chunks of shifter. if (!in_bounds(monster->pos())) return (-1); // Don't attempt to place corpses within walls, either. // Currently, this only applies to (shapeshifter) rock worms. if (feat_is_wall(grd(monster->pos()))) return (-1); item_def corpse; const monster_type corpse_class = fill_out_corpse(monster, corpse); // Don't place a corpse? If a zombified monster is somehow capable // of leaving a corpse, then always place it. if (mons_class_is_zombified(monster->type)) force = true; if (corpse_class == MONS_NO_MONSTER || (!force && coinflip())) return (-1); int o = get_item_slot(); if (o == NON_ITEM) { item_was_destroyed(corpse); return (-1); } mitm[o] = corpse; origin_set_monster(mitm[o], monster); if ((monster->flags & MF_EXPLODE_KILL) && explode_corpse(corpse, monster->pos())) { // We already have a spray of chunks destroy_item(o); return (-1); } move_item_to_grid(&o, monster->pos(), !monster->swimming()); if (you.see_cell(monster->pos())) { if (force && !silent) { if (you.can_see(monster)) simple_monster_message(monster, " turns back into a corpse!"); else { mprf("%s appears out of nowhere!", mitm[o].name(DESC_CAP_A).c_str()); } } const bool poison = (mons_corpse_effect(corpse_class) == CE_POISONOUS && player_res_poison() <= 0); if (o != NON_ITEM) tutorial_dissection_reminder(!poison); } return (o == NON_ITEM ? -1 : o); } static void _tutorial_inspect_kill() { if (Tutorial.tutorial_events[TUT_KILLED_MONSTER]) learned_something_new(TUT_KILLED_MONSTER); } #ifdef DGL_MILESTONES static std::string _milestone_kill_verb(killer_type killer) { return (killer == KILL_RESET ? "banished " : "killed "); } static void _check_kill_milestone(const monsters *mons, killer_type killer, int i) { // XXX: See comment in monster_polymorph. bool is_unique = mons_is_unique(mons->type); if (mons->props.exists("original_was_unique")) is_unique = mons->props["original_was_unique"].get_bool(); // Don't give milestones for summoned ghosts {due} if (mons->type == MONS_PLAYER_GHOST && !mons->is_summoned()) { std::string milestone = _milestone_kill_verb(killer) + "the ghost of "; milestone += get_ghost_description(*mons, true); milestone += "."; mark_milestone("ghost", milestone); } // Or summoned uniques, which a summoned ghost is treated as {due} else if (is_unique && !mons->is_summoned()) { mark_milestone("unique", _milestone_kill_verb(killer) + mons->name(DESC_NOCAP_THE, true) + "."); } } #endif // DGL_MILESTONES static void _give_monster_experience(monsters *victim, int killer_index, int experience, bool victim_was_born_friendly) { if (invalid_monster_index(killer_index)) return; monsters *mon = &menv[killer_index]; if (!mon->alive()) return; if ((!victim_was_born_friendly || !mon->friendly()) && !mons_aligned(killer_index, victim->mindex())) { if (mon->gain_exp(experience)) { if (you.religion != GOD_SHINING_ONE && you.religion != GOD_BEOGH || player_under_penance() || !one_chance_in(3)) { return; } // Randomly bless the follower who gained experience. if (you.religion == GOD_SHINING_ONE && random2(you.piety) >= piety_breakpoint(0) || you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2)) { bless_follower(mon); } } } } static void _give_adjusted_experience(monsters *monster, killer_type killer, bool pet_kill, int killer_index, unsigned int *exp_gain, unsigned int *avail_gain) { const int experience = exper_value(monster); const bool created_friendly = testbits(monster->flags, MF_NO_REWARD); const bool was_neutral = testbits(monster->flags, MF_WAS_NEUTRAL); const bool no_xp = monster->has_ench(ENCH_ABJ) || !experience; const bool already_got_half_xp = testbits(monster->flags, MF_GOT_HALF_XP); bool need_xp_msg = false; if (created_friendly || was_neutral || no_xp) ; // No experience if monster was created friendly or summoned. else if (YOU_KILL(killer)) { int old_lev = you.experience_level; if (already_got_half_xp) gain_exp( experience / 2, exp_gain, avail_gain ); else gain_exp( experience, exp_gain, avail_gain ); if (old_lev == you.experience_level) need_xp_msg = true; } else if (pet_kill && !already_got_half_xp) { int old_lev = you.experience_level; gain_exp( experience / 2 + 1, exp_gain, avail_gain ); if (old_lev == you.experience_level) need_xp_msg = true; } // FIXME: Since giant spores get detached from mgrd early // on, we can't tell by this point if they were visible when // they exploded. Rather than bothering to remember this, we // just suppress the message. if (monster->type == MONS_GIANT_SPORE || monster->type == MONS_BALL_LIGHTNING) { need_xp_msg = false; } // Give a message for monsters dying out of sight. if (need_xp_msg && exp_gain > 0 && !you.can_see(monster) && !crawl_state.arena) { mpr("You feel a bit more experienced."); } if (MON_KILL(killer) && !no_xp) { _give_monster_experience( monster, killer_index, experience, created_friendly ); } } static bool _is_pet_kill(killer_type killer, int i) { if (!MON_KILL(killer)) return (false); if (i == ANON_FRIENDLY_MONSTER) return (true); if (invalid_monster_index(i)) return (false); const monsters *m = &menv[i]; if (m->friendly()) // This includes enslaved monsters. return (true); // Check if the monster was confused by you or a friendly, which // makes casualties to this monster collateral kills. const mon_enchant me = m->get_ench(ENCH_CONFUSION); return (me.ench == ENCH_CONFUSION && (me.who == KC_YOU || me.who == KC_FRIENDLY)); } // Elyvilon will occasionally (5% chance) protect the life of one of // your allies. static bool _ely_protect_ally(monsters *monster) { if (you.religion != GOD_ELYVILON) return (false); if (!monster->is_holy() && monster->holiness() != MH_NATURAL || !monster->friendly() || !you.can_see(monster) // for simplicity || !one_chance_in(20)) { return (false); } monster->hit_points = 1; snprintf(info, INFO_SIZE, " protects %s from harm!%s", monster->name(DESC_NOCAP_THE).c_str(), coinflip() ? "" : " You feel responsible."); simple_god_message(info); lose_piety(1); return (true); } // Elyvilon retribution effect: Heal hostile monsters that were about to // be killed by you or one of your friends. static bool _ely_heal_monster(monsters *monster, killer_type killer, int i) { if (you.religion == GOD_ELYVILON) return (false); god_type god = GOD_ELYVILON; if (!you.penance[god] || !god_hates_your_god(god)) return (false); const int ely_penance = you.penance[god]; if (monster->friendly() || !one_chance_in(10)) return (false); if (MON_KILL(killer) && !invalid_monster_index(i)) { monsters *mon = &menv[i]; if (!mon->friendly() || !one_chance_in(3)) return (false); if (!mons_near(monster)) return (false); } else if (!YOU_KILL(killer)) return (false); dprf("monster hp: %d, max hp: %d", monster->hit_points, monster->max_hit_points); monster->hit_points = std::min(1 + random2(ely_penance/3), monster->max_hit_points); dprf("new hp: %d, ely penance: %d", monster->hit_points, ely_penance); snprintf(info, INFO_SIZE, "%s heals %s%s", god_name(god, false).c_str(), monster->name(DESC_NOCAP_THE).c_str(), monster->hit_points * 2 <= monster->max_hit_points ? "." : "!"); god_speaks(god, info); dec_penance(god, 1 + random2(monster->hit_points/2)); return (true); } static bool _yred_enslave_soul(monsters *monster, killer_type killer) { if (you.religion == GOD_YREDELEMNUL && mons_enslaved_body_and_soul(monster) && mons_near(monster) && killer != KILL_RESET && killer != KILL_DISMISSED) { yred_make_enslaved_soul(monster, player_under_penance()); return (true); } return (false); } static bool _beogh_forcibly_convert_orc(monsters *monster, killer_type killer, int i) { if (you.religion == GOD_BEOGH && mons_species(monster->type) == MONS_ORC && !monster->is_summoned() && !monster->is_shapeshifter() && !player_under_penance() && you.piety >= piety_breakpoint(2) && mons_near(monster)) { bool convert = false; if (YOU_KILL(killer)) convert = true; else if (MON_KILL(killer) && !invalid_monster_index(i)) { monsters *mon = &menv[i]; if (is_follower(mon) && !one_chance_in(3)) convert = true; } // Orcs may convert to Beogh under threat of death, either from // you or, less often, your followers. In both cases, the // checks are made against your stats. You're the potential // messiah, after all. if (convert) { #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Death convert attempt on %s, HD: %d, " "your xl: %d", monster->name(DESC_PLAIN).c_str(), monster->hit_dice, you.experience_level); #endif if (random2(you.piety) >= piety_breakpoint(0) && random2(you.experience_level) >= random2(monster->hit_dice) // Bias beaten-up-conversion towards the stronger orcs. && random2(monster->hit_dice) > 2) { beogh_convert_orc(monster, true, MON_KILL(killer)); return (true); } } } return (false); } static bool _monster_avoided_death(monsters *monster, killer_type killer, int i) { if (monster->hit_points < -25 || monster->hit_points < -monster->max_hit_points || monster->max_hit_points <= 0 || monster->hit_dice < 1) { return (false); } // Elyvilon specials. if (_ely_protect_ally(monster)) return (true); if (_ely_heal_monster(monster, killer, i)) return (true); // Yredelemnul special. if (_yred_enslave_soul(monster, killer)) return (true); // Beogh special. if (_beogh_forcibly_convert_orc(monster, killer, i)) return (true); return (false); } static void _jiyva_died() { if (you.religion == GOD_JIYVA) return; remove_all_jiyva_altars(); if (!player_in_branch(BRANCH_SLIME_PITS)) return; if (silenced(you.pos())) { god_speaks(GOD_JIYVA, "With an infernal shudder, the power ruling " "this place vanishes!"); } else { god_speaks(GOD_JIYVA, "With infernal noise, the power ruling this " "place vanishes!"); } } static void _fire_monster_death_event(monsters *monster, killer_type killer, int i, bool polymorph) { int type = monster->type; // Treat whatever the Royal Jelly polymorphed into as if it were still // the Royal Jelly (but if a player chooses the character name // "shaped Royal Jelly" don't unlock the vaults when the player's // ghost is killed). if (monster->mname == "shaped Royal Jelly" && monster->type != MONS_PLAYER_GHOST) { type = MONS_ROYAL_JELLY; } // Banished monsters aren't technically dead, so no death event // for them. if (killer == KILL_RESET) return; dungeon_events.fire_event( dgn_event(DET_MONSTER_DIED, monster->pos(), 0, monster->mindex(), killer)); if (type == MONS_ROYAL_JELLY && !polymorph) { you.royal_jelly_dead = true; if (jiyva_is_dead()) _jiyva_died(); } } static void _mummy_curse(monsters* monster, killer_type killer, int index) { int pow; switch (killer) { // Mummy killed by trap or something other than the player or // another monster, so no curse. case KILL_MISC: // Mummy sent to the Abyss wasn't actually killed, so no curse. case KILL_RESET: case KILL_DISMISSED: return; default: break; } switch (monster->type) { case MONS_MENKAURE: case MONS_MUMMY: pow = 1; break; case MONS_GUARDIAN_MUMMY: pow = 3; break; case MONS_MUMMY_PRIEST: pow = 8; break; case MONS_GREATER_MUMMY: pow = 11; break; case MONS_KHUFU: pow = 15; break; default: mpr("Unknown mummy type.", MSGCH_DIAGNOSTICS); return; } // beam code might give an index of MHITYOU for the player. if (YOU_KILL(killer)) index = NON_MONSTER; // Killed by a Zot trap, a god, etc. if (index != NON_MONSTER && invalid_monster_index(index)) return; actor* target; if (index == NON_MONSTER) target = &you; else { // Mummies committing suicide don't cause a death curse. if (index == monster->mindex()) return; target = &menv[index]; } // Mummy was killed by a giant spore or ball lightning? if (!target->alive()) return; if ((monster->type == MONS_MUMMY || monster->type == MONS_MENKAURE) && YOU_KILL(killer)) curse_an_item(true); else { if (index == NON_MONSTER) { mpr("You feel extremely nervous for a moment...", MSGCH_MONSTER_SPELL); } else if (you.can_see(target)) { mprf(MSGCH_MONSTER_SPELL, "A malignant aura surrounds %s.", target->name(DESC_NOCAP_THE).c_str()); } MiscastEffect(target, monster->mindex(), SPTYP_NECROMANCY, pow, random2avg(88, 3), "a mummy death curse"); } } static bool _spore_goes_pop(monsters *monster, killer_type killer, int killer_index, bool pet_kill, bool wizard) { if (monster->hit_points > 0 || monster->hit_points <= -15 || wizard || killer == KILL_RESET || killer == KILL_DISMISSED) { return (false); } bolt beam; const int type = monster->type; beam.is_tracer = false; beam.is_explosion = true; beam.beam_source = monster->mindex(); beam.type = dchar_glyph(DCHAR_FIRED_BURST); beam.source = monster->pos(); beam.target = monster->pos(); beam.thrower = crawl_state.arena ? KILL_MON : monster->attitude == ATT_FRIENDLY ? KILL_YOU : KILL_MON; beam.aux_source.clear(); beam.attitude = monster->attitude; if (YOU_KILL(killer)) beam.aux_source = "set off by themselves"; else if (pet_kill) beam.aux_source = "set off by their pet"; const char* msg = NULL; const char* sanct_msg = NULL; if (type == MONS_GIANT_SPORE) { beam.flavour = BEAM_SPORE; beam.damage = dice_def(3, 15); beam.name = "explosion of spores"; beam.colour = LIGHTGREY; beam.ex_size = 2; msg = "The giant spore explodes!"; sanct_msg = "By Zin's power, the giant spore's explosion is " "contained."; } else if (type == MONS_BALL_LIGHTNING) { beam.flavour = BEAM_ELECTRICITY; beam.damage = dice_def(3, 20); beam.name = "blast of lightning"; beam.colour = LIGHTCYAN; beam.ex_size = coinflip() ? 3 : 2; msg = "The ball lightning explodes!"; sanct_msg = "By Zin's power, the ball lightning's explosion " "is contained."; } else { msg::streams(MSGCH_DIAGNOSTICS) << "Unknown spore type: " << static_cast(type) << std::endl; return (false); } bool saw = false; if (you.can_see(monster)) { saw = true; viewwindow(false); if (is_sanctuary(monster->pos())) mpr(sanct_msg, MSGCH_GOD); else mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, msg); } if (is_sanctuary(monster->pos())) return (false); // Detach monster from the grid first, so it doesn't get hit by // its own explosion. (GDL) mgrd(monster->pos()) = NON_MONSTER; // The explosion might cause a monster to be placed where the spore // used to be, so make sure that mgrd() doesn't get cleared a second // time (causing the new monster to become floating) when // monster->reset() is called. monster->set_position(coord_def(0,0)); // Exploding kills the monster a bit earlier than normal. monster->hit_points = -16; if (saw) viewwindow(false); // FIXME: show_more == mons_near(monster) beam.explode(); activate_ballistomycetes(monster, beam.target); // Monster died in explosion, so don't re-attach it to the grid. return (true); } void _monster_die_cloud(const monsters* monster, bool corpse, bool silent, bool summoned) { // Chaos spawn always leave behind a cloud of chaos. if (monster->type == MONS_CHAOS_SPAWN) { summoned = true; corpse = false; } if (!summoned) return; std::string prefix = " "; if (corpse) { if (mons_weight(mons_species(monster->type)) == 0) return; prefix = "'s corpse "; } std::string msg = summoned_poof_msg(monster); msg += "!"; cloud_type cloud = CLOUD_NONE; if (msg.find("smoke") != std::string::npos) cloud = random_smoke_type(); else if (msg.find("chaos") != std::string::npos) cloud = CLOUD_CHAOS; if (!silent) simple_monster_message(monster, (prefix + msg).c_str()); if (cloud != CLOUD_NONE) { place_cloud(cloud, monster->pos(), 1 + random2(3), monster->kill_alignment(), KILL_MON_MISSILE); } } // XXX: Another hackish function! May do weird things if multiple copies of // the band have been placed using wizard mode. {due} static void _elven_twin_died(monsters* twin, bool in_transit) { bool found_duvessa = false; bool found_dowan = false; monsters *monster; for (monster_iterator mi; mi; ++mi) { if (*mi == twin) continue; if (mi->type == MONS_DUVESSA || (mi->props.exists("original_name") && mi->props["original_name"].get_string() == "Duvessa")) { monster = *mi; found_duvessa = true; break; } else if (mi->type == MONS_DOWAN || (mi->props.exists("original_name") && mi->props["original_name"].get_string() == "Dowan")) { monster = *mi; found_dowan = true; break; } } if (!found_duvessa && !found_dowan) return; // Okay, let them climb stairs now. monster->props["can_climb"] = "yes"; if (!in_transit) monster->props["speech_prefix"] = "twin_died"; else monster->props["speech_prefix"] = "twin_banished"; // If you've stabbed one of them, the other one is likely asleep still. if (monster->asleep()) behaviour_event(monster, ME_DISTURB, MHITNOT, monster->pos()); // Will generate strings such as 'Duvessa_Duvessa_dies' or, alternately // 'Dowan_Dowan_dies', but as neither will match, these can safely be // ignored. std::string key = "_" + monster->name(DESC_CAP_THE, true) + "_" + twin->name(DESC_CAP_THE) + "_dies_"; if (mons_near(monster) && !monster->observable()) key += "invisible_"; else if (!mons_near(monster)) key += "distance_"; std::string death_message = getSpeakString(key); // Check if they can speak or not: they may have been polymorphed. if (mons_near(monster) && !death_message.empty() && monster->can_speak()) mons_speaks_msg(monster, death_message, MSGCH_TALK, silenced(you.pos())); else if (monster->can_speak()) mprf("%s", death_message.c_str()); if (found_duvessa) { if (mons_near(monster)) // Provides its own flavour message. monster->go_berserk(true); else // She'll go berserk the next time she sees you monster->flags |= MF_GOING_BERSERK; } else if (found_dowan) { if (monster->observable()) { monster->add_ench(ENCH_HASTE); simple_monster_message(monster, " seems to find hidden reserves of power!"); } else monster->props["dowan_upgrade"] = bool(true); monster->spells[0] = SPELL_THROW_ICICLE; monster->spells[1] = SPELL_BLINK; monster->spells[3] = SPELL_STONE_ARROW; monster->spells[4] = SPELL_HASTE; // Nothing with 6. } } void pikel_band_neutralise () { bool message_made = false; for (monster_iterator mi; mi; ++mi) { if (mi->type == MONS_SLAVE && testbits(mi->flags, MF_BAND_MEMBER) && mi->props.exists("pikel_band")) { if (mi->observable() && !message_made) { mpr("With Pikel's spell broken, the former slaves thank you for their freedom."); message_made = true; } mi->flags |= MF_NAME_DESCRIPTOR | MF_NAME_REPLACE; mi->mname = "freed slave"; mons_pacify(*mi); } } } static void _hogs_to_humans() { // Simplification: if, in a rare event, another hog which was not created // as a part of Kirke's band happens to be on the level, the player can't // tell them apart anyway. // On the other hand, hogs which left the level are too far away to be // affected by the magic of Kirke's death. int any = 0, human = 0; for (monster_iterator mi; mi; ++mi) { if (mi->type != MONS_HOG) continue; // Shapeshifters will stop being a hog when they feel like it. if (mi->is_shapeshifter()) continue; const bool could_see = you.can_see(*mi); monsters orig; if (mi->props.exists(ORIG_MONSTER_KEY)) // Copy it, since the instance in props will get deleted // as soon a **mi is assigned to. orig = mi->props[ORIG_MONSTER_KEY].get_monster(); else { orig.type = MONS_HUMAN; orig.attitude = mi->attitude; define_monster(orig); } // Keep at same spot. const coord_def pos = mi->pos(); // Preserve relative HP. const float hp = (float) mi->hit_points / (float) mi->max_hit_points; // Preserve some flags. const unsigned long preserve_flags = mi->flags & ~(MF_JUST_SUMMONED | MF_WAS_IN_VIEW); // Preserve enchantments. mon_enchant_list enchantments = mi->enchantments; // Restore original monster. **mi = orig; mi->set_position(pos); mi->enchantments = enchantments; mi->hit_points = std::max(1, (int) (mi->max_hit_points * hp)); mi->flags = mi->flags | preserve_flags; const bool can_see = you.can_see(*mi); // A monster changing factions while in the arena messes up // arena book-keeping. if (!crawl_state.arena) { // * A monster's attitude shouldn't downgrade from friendly // or good-neutral because you helped it. It'd suck to // lose a permanent ally that way. // // * A monster has to be smart enough to realize that you // helped it. if (mi->attitude == ATT_HOSTILE && mons_intel(*mi) >= I_NORMAL) { mi->attitude = ATT_GOOD_NEUTRAL; mi->flags |= MF_WAS_NEUTRAL; } } behaviour_event(*mi, ME_EVAL); if (could_see && can_see) { any++; if (mi->type == MONS_HUMAN) human++; } else if (could_see && !can_see) mpr("The hog vanishes!"); else if (!could_see && can_see) mprf("%s appears from out of thin air!", mi->name(DESC_CAP_A).c_str()); } if (any == 1) { if (any == human) mpr("No longer under Kirke's spell, the hog turns into a human!"); else mpr("No longer under Kirke's spell, the hog returns to its " "original form!"); } else if (any > 1) { if (any == human) mpr("No longer under Kirke's spell, all hogs revert to their " "human forms!"); else mpr("No longer under Kirke's spell, all hogs revert to their " "original forms!"); } // Revert the player as well. if (you.attribute[ATTR_TRANSFORMATION] == TRAN_PIG) untransform(); } static int _tentacle_too_far(monsters *head, monsters *tentacle) { // The Shoals produce no disjoint bodies of water. // If this ever changes, we'd need to check if the head and tentacle // are still in the same pool. // XXX: Actually, using Fedhas's Sunlight power you can separate pools... return grid_distance(head->pos(), tentacle->pos()) > KRAKEN_TENTACLE_RANGE; } void mons_relocated(monsters *monster) { if (mons_base_type(monster) == MONS_KRAKEN) { int headnum = monster->mindex(); if (invalid_monster_index(headnum)) return; for (monster_iterator mi; mi; ++mi) { if (mi->type == MONS_KRAKEN_TENTACLE && (int)mi->number == headnum && _tentacle_too_far(monster, *mi)) { monster_die(*mi, KILL_RESET, -1, true, false); } } } else if (monster->type == MONS_KRAKEN_TENTACLE) { if (invalid_monster_index(monster->number) || menv[monster->number].type != MONS_KRAKEN || _tentacle_too_far(&menv[monster->number], monster)) { monster_die(monster, KILL_RESET, -1, true, false); } } } static int _destroy_tentacles(monsters *head) { int tent = 0; int headnum = head->mindex(); if (invalid_monster_index(headnum)) return 0; for (monster_iterator mi; mi; ++mi) { if (mi->type == MONS_KRAKEN_TENTACLE && (int)mi->number == headnum) { if (mons_near(*mi)) tent++; mi->hurt(*mi, INSTANT_DEATH); } } return tent; } static std::string _killer_type_name(killer_type killer) { switch(killer) { case KILL_NONE: return ("none"); case KILL_YOU: return ("you"); case KILL_MON: return ("mon"); case KILL_YOU_MISSILE: return ("you_missile"); case KILL_MON_MISSILE: return ("mon_missile"); case KILL_YOU_CONF: return ("you_conf"); case KILL_MISCAST: return ("miscast"); case KILL_MISC: return ("misc"); case KILL_RESET: return ("reset"); case KILL_DISMISSED: return ("dismissed"); } ASSERT(false); return(""); } // Returns the slot of a possibly generated corpse or -1. int monster_die(monsters *monster, killer_type killer, int killer_index, bool silent, bool wizard) { if (invalid_monster(monster)) return (-1); // If a monster was banished to the Abyss and then killed there, // then its death wasn't a banishment. if (you.level_type == LEVEL_ABYSS) monster->flags &= ~MF_BANISHED; if (!silent && _monster_avoided_death(monster, killer, killer_index)) { monster->flags &= ~MF_EXPLODE_KILL; return (-1); } // If the monster was calling the tide, let go now. monster->del_ench(ENCH_TIDE); crawl_state.inc_mon_acting(monster); ASSERT(!( YOU_KILL(killer) && crawl_state.arena )); if (monster->props.exists("monster_dies_lua_key")) { lua_stack_cleaner clean(dlua); dlua_chunk &chunk = monster->props["monster_dies_lua_key"]; if (!chunk.load(dlua)) { push_monster(dlua, monster); clua_pushcxxstring(dlua, _killer_type_name(killer)); lua_pushnumber(dlua, killer_index); lua_pushboolean(dlua, silent); lua_pushboolean(dlua, wizard); dlua.callfn(NULL, 5, 0); } else { mprf(MSGCH_ERROR, "Lua death function for monster '%s' didn't load: %s", monster->full_name(DESC_PLAIN).c_str(), dlua.error.c_str()); } } mons_clear_trapping_net(monster); you.remove_beholder(monster); // Clear auto exclusion now the monster is killed -- if we know about it. if (mons_near(monster) || wizard) remove_auto_exclude(monster); int summon_type = 0; int duration = 0; const bool summoned = monster->is_summoned(&duration, &summon_type); const int monster_killed = monster->mindex(); const bool hard_reset = testbits(monster->flags, MF_HARD_RESET); const bool gives_xp = (!summoned && !mons_class_flag(monster->type, M_NO_EXP_GAIN)); bool drop_items = !hard_reset; const bool mons_reset(killer == KILL_RESET || killer == KILL_DISMISSED); const bool submerged = monster->submerged(); bool in_transit = false; #ifdef DGL_MILESTONES if (!crawl_state.arena) _check_kill_milestone(monster, killer, killer_index); #endif // Award experience for suicide if the suicide was caused by the // player. if (MON_KILL(killer) && monster_killed == killer_index) { if (monster->confused_by_you()) { ASSERT(!crawl_state.arena); killer = KILL_YOU_CONF; } } else if (MON_KILL(killer) && monster->has_ench(ENCH_CHARM)) { ASSERT(!crawl_state.arena); killer = KILL_YOU_CONF; // Well, it was confused in a sense... (jpeg) } // Take note! if (!mons_reset && !crawl_state.arena && MONST_INTERESTING(monster)) { take_note(Note(NOTE_KILL_MONSTER, monster->type, monster->friendly(), monster->full_name(DESC_NOCAP_A).c_str())); } // From time to time Trog gives you a little bonus. if (killer == KILL_YOU && you.berserk()) { if (you.religion == GOD_TROG && !player_under_penance() && you.piety > random2(1000)) { const int bonus = (3 + random2avg( 10, 2 )) / 2; you.increase_duration(DUR_BERSERKER, bonus); you.increase_duration(DUR_MIGHT, bonus); haste_player(bonus * 2); mpr("You feel the power of Trog in you as your rage grows.", MSGCH_GOD, GOD_TROG); } else if (wearing_amulet(AMU_RAGE) && one_chance_in(30)) { const int bonus = (2 + random2(4)) / 2;; you.increase_duration(DUR_BERSERKER, bonus); you.increase_duration(DUR_MIGHT, bonus); haste_player(bonus * 2); mpr("Your amulet glows a violent red."); } } if (you.prev_targ == monster_killed) { you.prev_targ = MHITNOT; crawl_state.cancel_cmd_repeat(); } if (killer == KILL_YOU) crawl_state.cancel_cmd_repeat(); const bool pet_kill = _is_pet_kill(killer, killer_index); bool did_death_message = false; if (monster->type == MONS_GIANT_SPORE || monster->type == MONS_BALL_LIGHTNING) { did_death_message = _spore_goes_pop(monster, killer, killer_index, pet_kill, wizard); } else if (monster->type == MONS_FIRE_VORTEX || monster->type == MONS_SPATIAL_VORTEX) { if (!silent && !mons_reset) { simple_monster_message(monster, " dissipates!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } if (monster->type == MONS_FIRE_VORTEX && !wizard && !mons_reset && !submerged) { place_cloud(CLOUD_FIRE, monster->pos(), 2 + random2(4), monster->kill_alignment(), KILL_MON_MISSILE); } if (killer == KILL_RESET) killer = KILL_DISMISSED; } else if (monster->type == MONS_SIMULACRUM_SMALL || monster->type == MONS_SIMULACRUM_LARGE) { if (!silent && !mons_reset) { simple_monster_message(monster, " vapourises!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } if (!wizard && !mons_reset && !submerged) { place_cloud(CLOUD_COLD, monster->pos(), 2 + random2(4), monster->kill_alignment(), KILL_MON_MISSILE); } if (killer == KILL_RESET) killer = KILL_DISMISSED; } else if (monster->type == MONS_DANCING_WEAPON) { if (!hard_reset) { if (killer == KILL_RESET) killer = KILL_DISMISSED; } if (!silent && !hard_reset) { int w_idx = monster->inv[MSLOT_WEAPON]; if (w_idx != NON_ITEM && !(mitm[w_idx].flags & ISFLAG_SUMMONED)) { simple_monster_message(monster, " falls from the air.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } else killer = KILL_RESET; } } const bool death_message = !silent && !did_death_message && mons_near(monster) && monster->visible_to(&you); const bool exploded = monster->flags & MF_EXPLODE_KILL; const bool created_friendly = testbits(monster->flags, MF_NO_REWARD); bool anon = (killer_index == ANON_FRIENDLY_MONSTER); const mon_holy_type targ_holy = monster->holiness(); switch (killer) { case KILL_YOU: // You kill in combat. case KILL_YOU_MISSILE: // You kill by missile or beam. case KILL_YOU_CONF: // You kill by confusion. { const bool bad_kill = god_hates_killing(you.religion, monster); const bool was_neutral = testbits(monster->flags, MF_WAS_NEUTRAL); // killing friendlies is good, more bloodshed! // Undead feel no pain though, tormenting them is not as satisfying. const bool good_kill = !created_friendly && gives_xp || (is_evil_god(you.religion) && !monster->is_summoned() && (targ_holy == MH_NATURAL || targ_holy == MH_HOLY)); if (death_message) { if (killer == KILL_YOU_CONF && (anon || !invalid_monster_index(killer_index))) { mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, "%s is %s!", monster->name(DESC_CAP_THE).c_str(), exploded ? "blown up" : _wounded_damaged(monster->type) ? "destroyed" : "killed"); } else { mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, "You %s %s!", exploded ? "blow up" : _wounded_damaged(monster->type) ? "destroy" : "kill", monster->name(DESC_NOCAP_THE).c_str()); } if ((created_friendly || was_neutral) && gives_xp) mpr("That felt strangely unrewarding."); } // Killing triggers tutorial lesson. if (gives_xp) _tutorial_inspect_kill(); // Prevent summoned creatures from being good kills. if (bad_kill || good_kill) { if (targ_holy == MH_NATURAL) { did_god_conduct(DID_KILL_LIVING, monster->hit_dice, true, monster); if (monster->is_unholy()) { did_god_conduct(DID_KILL_NATURAL_UNHOLY, monster->hit_dice, true, monster); } if (monster->is_evil()) { did_god_conduct(DID_KILL_NATURAL_EVIL, monster->hit_dice, true, monster); } } else if (targ_holy == MH_UNDEAD) { did_god_conduct(DID_KILL_UNDEAD, monster->hit_dice, true, monster); } else if (targ_holy == MH_DEMONIC) { did_god_conduct(DID_KILL_DEMON, monster->hit_dice, true, monster); } // Zin hates unclean and chaotic beings. if (monster->is_unclean()) { did_god_conduct(DID_KILL_UNCLEAN, monster->hit_dice, true, monster); } if (monster->is_chaotic()) { did_god_conduct(DID_KILL_CHAOTIC, monster->hit_dice, true, monster); } // jmf: Trog hates wizards. if (monster->is_actual_spellcaster()) { did_god_conduct(DID_KILL_WIZARD, monster->hit_dice, true, monster); } // Beogh hates priests of other gods. if (monster->is_priest()) { did_god_conduct(DID_KILL_PRIEST, monster->hit_dice, true, monster); } if (mons_is_slime(monster)) { did_god_conduct(DID_KILL_SLIME, monster->hit_dice, true, monster); } if (fedhas_protects(monster)) { did_god_conduct(DID_KILL_PLANT, monster->hit_dice, true, monster); } // Cheibriados hates fast monsters. if (mons_is_fast(monster) && !monster->cannot_move()) { did_god_conduct(DID_KILL_FAST, monster->hit_dice, true, monster); } // Holy kills are always noticed. if (monster->is_holy()) { did_god_conduct(DID_KILL_HOLY, monster->hit_dice, true, monster); } } // Divine health and mana restoration doesn't happen when // killing born-friendly monsters. The mutation still // applies, however. if (player_mutation_level(MUT_DEATH_STRENGTH) || (good_kill && (you.religion == GOD_MAKHLEB || you.religion == GOD_SHINING_ONE && monster->is_evil()) && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0))) { if (you.hp < you.hp_max) { mpr("You feel a little better."); inc_hp(monster->hit_dice + random2(monster->hit_dice), false); } } if (good_kill && (you.religion == GOD_MAKHLEB || you.religion == GOD_VEHUMET || you.religion == GOD_SHINING_ONE && monster->is_evil()) && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0)) { if (you.magic_points < you.max_magic_points) { mpr("You feel your power returning."); inc_mp(1 + random2(monster->hit_dice / 2), false); } } // Randomly bless a follower. if (!created_friendly && gives_xp && (you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2)) && !player_under_penance()) { bless_follower(); } if (you.duration[DUR_DEATH_CHANNEL] && monster->holiness() == MH_NATURAL && mons_can_be_zombified(monster) && gives_xp) { const monster_type spectre_type = mons_species(monster->type); // Don't allow 0-headed hydras to become spectral hydras. if (spectre_type != MONS_HYDRA || monster->number != 0) { const int spectre = create_monster( mgen_data(MONS_SPECTRAL_THING, BEH_FRIENDLY, &you, 0, 0, monster->pos(), MHITYOU, 0, static_cast(you.attribute[ATTR_DIVINE_DEATH_CHANNEL]), spectre_type, monster->number)); if (spectre != -1) { if (death_message) mpr("A glowing mist starts to gather..."); name_zombie(&menv[spectre], monster); } } } break; } case KILL_MON: // Monster kills in combat. case KILL_MON_MISSILE: // Monster kills by missile or beam. if (!silent) { const char* msg = exploded ? " is blown up!" : _wounded_damaged(monster->type) ? " is destroyed!" : " dies!"; simple_monster_message(monster, msg, MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } if (crawl_state.arena) break; // No piety loss if god gifts killed by other monsters. // Also, dancing weapons aren't really friendlies. if (monster->friendly() && monster->type != MONS_DANCING_WEAPON) { did_god_conduct(DID_FRIEND_DIED, 1 + (monster->hit_dice / 2), true, monster); } if (pet_kill && fedhas_protects(monster)) { did_god_conduct(DID_ALLY_KILLED_PLANT, 1 + (monster->hit_dice / 2), true, monster); } // Trying to prevent summoning abuse here, so we're trying to // prevent summoned creatures from being done_good kills. Only // affects creatures which were friendly when summoned. if (!created_friendly && gives_xp && pet_kill && (anon || !invalid_monster_index(killer_index))) { bool notice = false; monsters *killer_mon = NULL; if (!anon) { killer_mon = &menv[killer_index]; // If the killer is already dead treat it like an // anonymous monster. if (killer_mon->type == MONS_NO_MONSTER) anon = true; } const mon_holy_type killer_holy = anon ? MH_NATURAL : killer_mon->holiness(); if (you.religion == GOD_SHINING_ONE || you.religion == GOD_YREDELEMNUL || you.religion == GOD_KIKUBAAQUDGHA || you.religion == GOD_VEHUMET || you.religion == GOD_MAKHLEB || you.religion == GOD_LUGONU || !anon && mons_is_god_gift(killer_mon)) { if (killer_holy == MH_UNDEAD) { const bool confused = anon ? false : !killer_mon->friendly(); // Yes, these are hacks, but they make sure that // confused monsters doing kills are not // referred to as "slaves", and I think it's // okay that e.g. Yredelemnul ignores kills done // by confused monsters as opposed to enslaved // or friendly ones. (jpeg) if (targ_holy == MH_NATURAL) { notice |= did_god_conduct( !confused ? DID_LIVING_KILLED_BY_UNDEAD_SLAVE : DID_LIVING_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_UNDEAD) { notice |= did_god_conduct( !confused ? DID_UNDEAD_KILLED_BY_UNDEAD_SLAVE : DID_UNDEAD_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_DEMONIC) { notice |= did_god_conduct( !confused ? DID_DEMON_KILLED_BY_UNDEAD_SLAVE : DID_DEMON_KILLED_BY_SERVANT, monster->hit_dice); } } // Yes, we are splitting undead pets from the others // as a way to focus Necromancy vs. Summoning // (ignoring Haunt here)... at least we're being // nice and putting the natural creature summons // together with the demonic ones. Note that // Vehumet gets a free pass here since those // followers are assumed to come from summoning // spells... the others are from invocations (TSO, // Makhleb, Kiku). - bwr else if (targ_holy == MH_NATURAL) { notice |= did_god_conduct(DID_LIVING_KILLED_BY_SERVANT, monster->hit_dice); if (monster->is_unholy()) { notice |= did_god_conduct( DID_NATURAL_UNHOLY_KILLED_BY_SERVANT, monster->hit_dice); } if (monster->is_evil()) { notice |= did_god_conduct( DID_NATURAL_EVIL_KILLED_BY_SERVANT, monster->hit_dice); } } else if (targ_holy == MH_UNDEAD) { notice |= did_god_conduct(DID_UNDEAD_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_DEMONIC) { notice |= did_god_conduct(DID_DEMON_KILLED_BY_SERVANT, monster->hit_dice); } } // Holy kills are always noticed. if (monster->is_holy()) { if (killer_holy == MH_UNDEAD) { const bool confused = anon ? false : !killer_mon->friendly(); // Yes, this is a hack, but it makes sure that // confused monsters doing kills are not // referred to as "slaves", and I think it's // okay that Yredelemnul ignores kills done by // confused monsters as opposed to enslaved or // friendly ones. (jpeg) notice |= did_god_conduct( !confused ? DID_HOLY_KILLED_BY_UNDEAD_SLAVE : DID_HOLY_KILLED_BY_SERVANT, monster->hit_dice); } else notice |= did_god_conduct(DID_HOLY_KILLED_BY_SERVANT, monster->hit_dice); } if (you.religion == GOD_VEHUMET && notice && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0)) { // Vehumet - only for non-undead servants (coding // convenience, no real reason except that Vehumet // prefers demons). if (you.magic_points < you.max_magic_points) { mpr("You feel your power returning."); inc_mp(1 + random2(monster->hit_dice / 2), false); } } if (you.religion == GOD_SHINING_ONE && monster->is_evil() && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0) && !invalid_monster_index(killer_index)) { // Randomly bless the follower who killed. if (!one_chance_in(3) && killer_mon->alive() && bless_follower(killer_mon)) { break; } if (killer_mon->alive() && killer_mon->hit_points < killer_mon->max_hit_points) { simple_monster_message(killer_mon, " looks invigorated."); killer_mon->heal(1 + random2(monster->hit_dice / 4)); } } if (you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2) && !player_under_penance() && !one_chance_in(3) && !invalid_monster_index(killer_index)) { // Randomly bless the follower who killed. bless_follower(killer_mon); } } break; // Monster killed by trap/inanimate thing/itself/poison not from you. case KILL_MISC: if (!silent) { const char* msg = exploded ? " is blown up!" : _wounded_damaged(monster->type) ? " is destroyed!" : " dies!"; simple_monster_message(monster, msg, MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } break; case KILL_RESET: // Monster doesn't die, just goes back to wherever it came from. // This must only be called by monsters running out of time (or // abjuration), because it uses the beam variables! Or does it??? // Pacified monsters leave the level when this happens. // KILL_RESET monsters no longer lose their whole inventory, only // items they were generated with. if (monster->pacified() || !monster->needs_transit()) { // A banished monster that doesn't go on the transit list // loses all items. if (!monster->is_summoned()) drop_items = false; break; } // Monster goes to the Abyss. monster->flags |= MF_BANISHED; monster->set_transit(level_id(LEVEL_ABYSS)); in_transit = true; drop_items = false; // Make monster stop patrolling and/or travelling. monster->patrol_point.reset(); monster->travel_path.clear(); monster->travel_target = MTRAV_NONE; break; case KILL_DISMISSED: break; default: drop_items = false; break; } // Make sure Boris has a foe to address. if (monster->foe == MHITNOT) { if (!monster->wont_attack() && !crawl_state.arena) monster->foe = MHITYOU; else if (!invalid_monster_index(killer_index)) monster->foe = killer_index; } if (!silent && !wizard && you.see_cell(monster->pos())) { // Make sure that the monster looks dead. if (monster->alive() && !in_transit && (!summoned || duration > 0)) monster->hit_points = -1; mons_speaks(monster); } if (monster->type == MONS_BORIS && !in_transit) { // XXX: Actual blood curse effect for Boris? -- bwr // Now that Boris is dead, he's a valid target for monster // creation again. -- bwr you.unique_creatures[monster->type] = false; // And his vault can be placed again. you.uniq_map_names.erase("uniq_boris"); } else if (monster->type == MONS_KIRKE || (monster->props.exists("original_name") && monster->props["original_name"].get_string() == "Kirke") && !in_transit) { _hogs_to_humans(); } else if (monster->type == MONS_PIKEL || (monster->props.exists("original_name") && monster->props["original_name"].get_string() == "Pikel")) { // His slaves don't care if he's dead or not, just whether or not // he goes away. pikel_band_neutralise(); } else if (mons_base_type(monster) == MONS_KRAKEN) { if (_destroy_tentacles(monster) && !in_transit) { mpr("The kraken is slain, and its tentacles slide " "back into the water like the carrion they now are."); } } else if (((monster->type == MONS_DOWAN || monster->type == MONS_DUVESSA) || (monster->props.exists("original_name") && (monster->props["original_name"].get_string() == "Dowan" || monster->props["original_name"].get_string() == "Duvessa"))) && mons_near(monster)) { _elven_twin_died(monster, in_transit); } else if (mons_is_mimic(monster->type)) drop_items = false; else if (!monster->is_summoned()) { if (mons_genus(monster->type) == MONS_MUMMY) _mummy_curse(monster, killer, killer_index); } if(monster->type == MONS_BALLISTOMYCETE) activate_ballistomycetes(monster, monster->pos()); if (!wizard && !submerged) _monster_die_cloud(monster, !mons_reset, silent, summoned); int corpse = -1; if (!mons_reset) { // Have to add case for disintegration effect here? {dlb} if (!summoned) corpse = place_monster_corpse(monster, silent); } unsigned int exp_gain = 0, avail_gain = 0; if (!mons_reset) _give_adjusted_experience(monster, killer, pet_kill, killer_index, &exp_gain, &avail_gain); if (!mons_reset && !crawl_state.arena) { you.kills->record_kill(monster, killer, pet_kill); kill_category kc = (killer == KILL_YOU || killer == KILL_YOU_MISSILE) ? KC_YOU : (pet_kill)? KC_FRIENDLY : KC_OTHER; PlaceInfo& curr_PlaceInfo = you.get_place_info(); PlaceInfo delta; delta.mon_kill_num[kc]++; delta.mon_kill_exp += exp_gain; delta.mon_kill_exp_avail += avail_gain; you.global_info += delta; you.global_info.assert_validity(); curr_PlaceInfo += delta; curr_PlaceInfo.assert_validity(); } _fire_monster_death_event(monster, killer, killer_index, false); if (crawl_state.arena) arena_monster_died(monster, killer, killer_index, silent, corpse); const coord_def mwhere = monster->pos(); if (drop_items) monster_drop_ething(monster, YOU_KILL(killer) || pet_kill); else { // Destroy the items belonging to MF_HARD_RESET monsters so they // don't clutter up mitm[]. monster->destroy_inventory(); } if (!silent && !wizard && !mons_reset && corpse != -1 && !(monster->flags & MF_KNOWN_MIMIC) && monster->is_shapeshifter()) { simple_monster_message(monster, "'s shape twists and changes " "as it dies."); } // If we kill an invisible monster reactivate autopickup. if (mons_near(monster) && !monster->visible_to(&you)) autotoggle_autopickup(false); crawl_state.dec_mon_acting(monster); monster_cleanup(monster); // Force redraw for monsters that die. if (you.see_cell(mwhere)) { view_update_at(mwhere); update_screen(); } return (corpse); } // Clean up after a dead monster. void monster_cleanup(monsters *monster) { crawl_state.mon_gone(monster); unsigned int monster_killed = monster->mindex(); monster->reset(); for (monster_iterator mi; mi; ++mi) if (mi->foe == monster_killed) mi->foe = MHITNOT; if (you.pet_target == monster_killed) you.pet_target = MHITNOT; } // If you're invis and throw/zap whatever, alerts menv to your position. void alert_nearby_monsters(void) { // Judging from the above comment, this function isn't // intended to wake up monsters, so we're only going to // alert monsters that aren't sleeping. For cases where an // event should wake up monsters and alert them, I'd suggest // calling noisy() before calling this function. -- bwr for (monster_iterator mi(&you.get_los()); mi; ++mi) if (!mi->asleep()) behaviour_event(*mi, ME_ALERT, MHITYOU); } static bool _valid_morph(monsters *monster, monster_type new_mclass) { const dungeon_feature_type current_tile = grd(monster->pos()); // 'morph targets are _always_ "base" classes, not derived ones. new_mclass = mons_species(new_mclass); // [ds] Non-base draconians are much more trouble than their HD // suggests. if (mons_genus(new_mclass) == MONS_DRACONIAN && new_mclass != MONS_DRACONIAN && !player_in_branch(BRANCH_HALL_OF_ZOT) && !one_chance_in(10)) { return (false); } // Various inappropriate polymorph targets. if (mons_class_holiness(new_mclass) != monster->holiness() || mons_class_flag(new_mclass, M_UNIQUE) // no uniques || mons_class_flag(new_mclass, M_NO_EXP_GAIN) // not helpless || new_mclass == mons_species(monster->type) // must be different || new_mclass == MONS_PROGRAM_BUG // These require manual setting of mons.base_monster to indicate // what they are a skeleton/zombie/simulacrum/spectral thing of, // which we currently aren't smart enough to handle. || mons_class_is_zombified(new_mclass) // These require manual setting of the ghost demon struct to // indicate their characteristics, which we currently aren't // smart enough to handle. || mons_is_ghost_demon(new_mclass) // Only for use by game testers or in the arena. || new_mclass == MONS_TEST_SPAWNER // Other poly-unsuitable things. || new_mclass == MONS_ORB_GUARDIAN || new_mclass == MONS_DWARF || mons_is_statue(new_mclass) || mons_is_projectile(new_mclass) // The spell on Prince Ribbit can't be broken so easily. || (new_mclass == MONS_HUMAN && monster->type == MONS_PRINCE_RIBBIT)) { return (false); } // Determine if the monster is happy on current tile. return (monster_habitable_grid(new_mclass, current_tile)); } static bool _is_poly_power_unsuitable( poly_power_type power, int src_pow, int tgt_pow, int relax ) { switch (power) { case PPT_LESS: return (tgt_pow > src_pow - 3 + (relax * 3) / 2) || (power == PPT_LESS && (tgt_pow < src_pow - (relax / 2))); case PPT_MORE: return (tgt_pow < src_pow + 2 - relax) || (power == PPT_MORE && (tgt_pow > src_pow + relax)); default: case PPT_SAME: return (tgt_pow < src_pow - relax) || (tgt_pow > src_pow + (relax * 3) / 2); } } // If targetc == RANDOM_MONSTER, then relpower indicates the desired // power of the new monster, relative to the current monster. // Relaxation still takes effect when needed, no matter what relpower // says. bool monster_polymorph(monsters *monster, monster_type targetc, poly_power_type power, bool force_beh) { ASSERT(!(monster->flags & MF_TAKING_STAIRS)); ASSERT(!(monster->flags & MF_BANISHED) || you.level_type == LEVEL_ABYSS); std::string str_polymon; int source_power, target_power, relax; int tries = 1000; // Used to be mons_power, but that just returns hit_dice // for the monster class. By using the current hit dice // the player gets the opportunity to use draining more // effectively against shapeshifters. -- bwr source_power = monster->hit_dice; relax = 1; if (targetc == RANDOM_MONSTER) { do { // Pick a monster that's guaranteed happy at this grid. targetc = random_monster_at_grid(monster->pos()); // Valid targets are always base classes ([ds] which is unfortunate // in that well-populated monster classes will dominate polymorphs). targetc = mons_species(targetc); target_power = mons_power(targetc); if (one_chance_in(200)) relax++; if (relax > 50) return (simple_monster_message(monster, " shudders.")); } while (tries-- && (!_valid_morph(monster, targetc) || _is_poly_power_unsuitable(power, source_power, target_power, relax))); } if (!_valid_morph(monster, targetc)) { return (simple_monster_message(monster, " looks momentarily different.")); } // Messaging. bool can_see = you.can_see(monster); bool can_see_new = !mons_class_flag(targetc, M_INVIS) || you.can_see_invisible(); bool need_note = false; std::string old_name = monster->full_name(DESC_CAP_A); // If old monster is visible to the player, and is interesting, // then note why the interesting monster went away. if (can_see && MONST_INTERESTING(monster)) need_note = true; std::string new_name = ""; if (monster->type == MONS_OGRE && targetc == MONS_TWO_HEADED_OGRE) str_polymon = " grows a second head"; else { if (monster->is_shapeshifter()) str_polymon = " changes into "; else if (targetc == MONS_PULSATING_LUMP) str_polymon = " degenerates into "; else if (you.religion == GOD_JIYVA && (targetc == MONS_DEATH_OOZE || targetc == MONS_OOZE || targetc == MONS_JELLY || targetc == MONS_BROWN_OOZE || targetc == MONS_SLIME_CREATURE || targetc == MONS_GIANT_AMOEBA || targetc == MONS_ACID_BLOB || targetc == MONS_AZURE_JELLY)) { // Message used for the Slimify ability. str_polymon = " quivers uncontrollably and liquefies into "; } else str_polymon = " evaporates and reforms as "; if (!can_see_new) { new_name = "something unseen"; str_polymon += "something you cannot see"; } else { str_polymon += mons_type_name(targetc, DESC_NOCAP_A); if (targetc == MONS_PULSATING_LUMP) str_polymon += " of flesh"; } } str_polymon += "!"; bool player_messaged = can_see && simple_monster_message(monster, str_polymon.c_str()); // Even if the monster transforms from one type that can behold the // player into a different type which can also behold the player, // the polymorph disrupts the beholding process. Do this before // changing monster->type, since unbeholding can only happen while // the monster is still a mermaid/siren. you.remove_beholder(monster); // Inform listeners that the original monster is gone. _fire_monster_death_event(monster, KILL_MISC, NON_MONSTER, true); // the actual polymorphing: unsigned long flags = monster->flags & ~(MF_INTERESTING | MF_SEEN | MF_ATT_CHANGE_ATTEMPT | MF_WAS_IN_VIEW | MF_BAND_MEMBER | MF_HONORARY_UNDEAD | MF_KNOWN_MIMIC); std::string name; // Preserve the names of uniques and named monsters. if (!monster->mname.empty()) name = monster->mname; else if (mons_is_unique(monster->type)) { flags |= MF_INTERESTING; name = monster->name(DESC_PLAIN, true); if (monster->type == MONS_ROYAL_JELLY) { name = "shaped Royal Jelly"; flags |= MF_NAME_SUFFIX; } else if (monster->type == MONS_LERNAEAN_HYDRA) { name = "shaped Lernaean hydra"; flags |= MF_NAME_SUFFIX; } // "Blork the orc" and similar. const size_t the_pos = name.find(" the "); if (the_pos != std::string::npos) name = name.substr(0, the_pos); } const monster_type real_targetc = (monster->has_ench(ENCH_GLOWING_SHAPESHIFTER)) ? MONS_GLOWING_SHAPESHIFTER : (monster->has_ench(ENCH_SHAPESHIFTER)) ? MONS_SHAPESHIFTER : targetc; const god_type god = (player_will_anger_monster(real_targetc) || (you.religion == GOD_BEOGH && mons_species(real_targetc) != MONS_ORC)) ? GOD_NO_GOD : monster->god; if (god == GOD_NO_GOD) flags &= ~MF_GOD_GIFT; const int old_hp = monster->hit_points; const int old_hp_max = monster->max_hit_points; const bool old_mon_caught = monster->caught(); const char old_ench_countdown = monster->ench_countdown; // XXX: mons_is_unique should be converted to monster::is_unique, and that // function should be testing the value of props["original_was_unique"] // which would make things a lot simpler. // See also _check_kill_milestone. bool old_mon_unique = mons_is_unique(monster->type); if (monster->props.exists("original_was_unique")) if (monster->props["original_was_unique"].get_bool()) old_mon_unique = true; mon_enchant abj = monster->get_ench(ENCH_ABJ); mon_enchant charm = monster->get_ench(ENCH_CHARM); mon_enchant temp_pacif= monster->get_ench(ENCH_TEMP_PACIF); mon_enchant shifter = monster->get_ench(ENCH_GLOWING_SHAPESHIFTER, ENCH_SHAPESHIFTER); mon_enchant sub = monster->get_ench(ENCH_SUBMERGED); mon_enchant summon = monster->get_ench(ENCH_SUMMON); mon_enchant tp = monster->get_ench(ENCH_TP); monster_spells spl = monster->spells; const bool need_save_spells = (!name.empty() && (!monster->can_use_spells() || monster->is_actual_spellcaster())); // deal with mons_sec monster->type = targetc; monster->base_monster = MONS_NO_MONSTER; monster->number = 0; // Note: define_monster() will clear out all enchantments! - bwr define_monster(monster->mindex()); monster->mname = name; monster->props["original_name"] = name; monster->props["original_was_unique"] = old_mon_unique; monster->flags = flags; monster->god = god; // Forget various speech/shout Lua functions. monster->props.erase("speech_key"); monster->props.erase("speech_prefix"); monster->props.erase("speech_func"); monster->props.erase("shout_func"); // Keep spells for named monsters, but don't override innate ones // for dragons and the like. This means that Sigmund polymorphed // into a goblin will still cast spells, but if he ends up as a // swamp drake he'll breathe fumes and, if polymorphed further, // won't remember his spells anymore. if (need_save_spells && (!monster->can_use_spells() || monster->is_actual_spellcaster())) { monster->spells = spl; } monster->add_ench(abj); monster->add_ench(charm); monster->add_ench(temp_pacif); monster->add_ench(shifter); monster->add_ench(sub); monster->add_ench(summon); monster->add_ench(tp); // Allows for handling of submerged monsters which polymorph into // monsters that can't submerge on this square. if (monster->has_ench(ENCH_SUBMERGED) && !monster_can_submerge(monster, grd(monster->pos()))) { monster->del_ench(ENCH_SUBMERGED); } monster->ench_countdown = old_ench_countdown; if (mons_class_flag(monster->type, M_INVIS)) monster->add_ench(ENCH_INVIS); if (!player_messaged && you.can_see(monster)) { mprf("%s appears out of thin air!", monster->name(DESC_CAP_A).c_str()); autotoggle_autopickup(false); player_messaged = true; } monster->hit_points = monster->max_hit_points * ((old_hp * 100) / old_hp_max) / 100 + random2(monster->max_hit_points); monster->hit_points = std::min(monster->max_hit_points, monster->hit_points); // Don't kill it. monster->hit_points = std::max(monster->hit_points, 1); monster->speed_increment = 67 + random2(6); monster_drop_ething(monster); // New monster type might be interesting. mark_interesting_monst(monster); if (new_name.empty()) new_name = monster->full_name(DESC_NOCAP_A); if (need_note || can_see && you.can_see(monster) && MONST_INTERESTING(monster)) { take_note(Note(NOTE_POLY_MONSTER, 0, 0, old_name.c_str(), new_name.c_str())); if (you.can_see(monster)) monster->flags |= MF_SEEN; } // If new monster is visible to player, then we've seen it. if (you.can_see(monster)) { seen_monster(monster); // If the player saw both the beginning and end results of a // shifter changing, then s/he knows it must be a shifter. if (can_see && shifter.ench != ENCH_NONE) monster->flags |= MF_KNOWN_MIMIC; } if (old_mon_caught) check_net_will_hold_monster(monster); if (!force_beh) player_angers_monster(monster); // Xom likes watching monsters being polymorphed. xom_is_stimulated(monster->is_shapeshifter() ? 16 : power == PPT_LESS || monster->friendly() ? 32 : power == PPT_SAME ? 64 : 128); return (player_messaged); } // If the returned value is mon.pos(), then nothing was found. static coord_def _random_monster_nearby_habitable_space(const monsters& mon, bool allow_adjacent, bool respect_los) { const bool respect_sanctuary = mon.wont_attack(); coord_def target; int tries; for (tries = 0; tries < 150; ++tries) { const coord_def delta(random2(13) - 6, random2(13) - 6); // Check that we don't get something too close to the // starting point. if (delta.origin()) continue; if (delta.rdist() == 1 && !allow_adjacent) continue; // Update target. target = delta + mon.pos(); // Check that the target is valid and survivable. if (!in_bounds(target)) continue; if (!monster_habitable_grid(&mon, grd(target))) continue; if (respect_sanctuary && is_sanctuary(target)) continue; if (target == you.pos()) continue; // Check that we didn't go through clear walls. if (num_feats_between(mon.pos(), target, DNGN_CLEAR_ROCK_WALL, DNGN_CLEAR_PERMAROCK_WALL, true, true) > 0) { continue; } if (respect_los && !mon.see_cell(target)) continue; // Survived everything, break out (with a good value of target.) break; } if (tries == 150) target = mon.pos(); return (target); } bool monster_blink(monsters *monster, bool quiet) { coord_def near = _random_monster_nearby_habitable_space(*monster, false, true); return (monster->blink_to(near, quiet)); } bool mon_can_be_slimified(monsters *monster) { const mon_holy_type holi = monster->holiness(); return (!(monster->flags & MF_GOD_GIFT) && !mons_is_insubstantial(monster->type) && (holi == MH_UNDEAD || holi == MH_NATURAL && !mons_is_slime(monster)) ); } void slimify_monster(monsters *mon, bool hostile) { if (mon->holiness() == MH_UNDEAD) monster_polymorph(mon, MONS_DEATH_OOZE); else { const int x = mon->hit_dice + (coinflip() ? 1 : -1) * random2(5); if (x < 3) monster_polymorph(mon, MONS_OOZE); else if (x >= 3 && x < 5) monster_polymorph(mon, MONS_JELLY); else if (x >= 5 && x < 7) monster_polymorph(mon, MONS_BROWN_OOZE); else if (x >= 7 && x <= 11) { if (coinflip()) monster_polymorph(mon, MONS_SLIME_CREATURE); else monster_polymorph(mon, MONS_GIANT_AMOEBA); } else { if (coinflip()) monster_polymorph(mon, MONS_ACID_BLOB); else monster_polymorph(mon, MONS_AZURE_JELLY); } } if (!mons_eats_items(mon)) mon->add_ench(ENCH_EAT_ITEMS); if (!hostile) mon->attitude = ATT_STRICT_NEUTRAL; else mon->attitude = ATT_HOSTILE; mons_make_god_gift(mon, GOD_JIYVA); // Don't want shape-shifters to shift into non-slimes. mon->del_ench(ENCH_GLOWING_SHAPESHIFTER); mon->del_ench(ENCH_SHAPESHIFTER); } static bool _habitat_okay( const monsters *monster, dungeon_feature_type targ ) { return (monster_habitable_grid(monster, targ)); } // This doesn't really swap places, it just sets the monster's // position equal to the player (the player has to be moved afterwards). // It also has a slight problem with the fact that if the player is // levitating over an inhospitable habitat for the monster the monster // will be put in a place it normally couldn't go (this could be a // feature because it prevents insta-killing). In order to prevent // that little problem, we go looking for a square for the monster // to "scatter" to instead... and if we can't find one the monster // just refuses to be swapped (not a bug, this is intentionally // avoiding the insta-kill). Another option is to look a bit // wider for a vaild square (either by a last attempt blink, or // by looking at a wider radius)... insta-killing should be a // last resort in this function (especially since Tome, Dig, and // Summoning can be used to set up death traps). If worse comes // to worse, at least consider making the Swap spell not work // when the player is over lava or water (if the player wants to // swap pets to their death, we can let that go). -- bwr bool swap_places(monsters *monster) { coord_def loc; if (swap_check(monster, loc)) { swap_places(monster, loc); return true; } return false; } // Swap monster to this location. Player is swapped elsewhere. bool swap_places(monsters *monster, const coord_def &loc) { ASSERT(map_bounds(loc)); ASSERT(_habitat_okay(monster, grd(loc))); if (monster_at(loc)) { mpr("Something prevents you from swapping places."); return (false); } mpr("You swap places."); mgrd(monster->pos()) = NON_MONSTER; monster->moveto(loc); mgrd(monster->pos()) = monster->mindex(); return true; } // Returns true if this is a valid swap for this monster. If true, then // the valid location is set in loc. (Otherwise loc becomes garbage.) bool swap_check(monsters *monster, coord_def &loc, bool quiet) { loc = you.pos(); // Don't move onto dangerous terrain. if (is_feat_dangerous(grd(monster->pos()))) { canned_msg(MSG_UNTHINKING_ACT); return (false); } if (mons_is_projectile(monster->type)) { if (!quiet) mpr("It's unwise to walk into this."); return (false); } if (monster->caught()) { if (!quiet) simple_monster_message(monster, " is held in a net!"); return (false); } // First try: move monster onto your position. bool swap = _habitat_okay( monster, grd(loc) ); // Choose an appropriate habitat square at random around the target. if (!swap) { int num_found = 0; for (adjacent_iterator ai(you.pos()); ai; ++ai) if (!monster_at(*ai) && _habitat_okay(monster, grd(*ai)) && one_chance_in(++num_found)) { loc = *ai; } if (num_found) swap = true; } if (!swap && !quiet) { // Might not be ideal, but it's better than insta-killing // the monster... maybe try for a short blink instead? -- bwr simple_monster_message( monster, " resists." ); // FIXME: AI_HIT_MONSTER isn't ideal. interrupt_activity( AI_HIT_MONSTER, monster ); } return (swap); } // Given an adjacent monster, returns true if the monster can hit it // (the monster should not be submerged, be submerged in shallow water // if the monster has a polearm, or be submerged in anything if the // monster has tentacles). bool monster_can_hit_monster(monsters *monster, const monsters *targ) { if (!targ->submerged() || monster->has_damage_type(DVORP_TENTACLE)) return (true); if (grd(targ->pos()) != DNGN_SHALLOW_WATER) return (false); const item_def *weapon = monster->weapon(); return (weapon && weapon_skill(*weapon) == SK_POLEARMS); } void mons_get_damage_level(const monsters* monster, std::string& desc, mon_dam_level_type& dam_level) { if (monster->hit_points <= monster->max_hit_points / 6) { desc += "almost "; desc += _wounded_damaged(monster->type) ? "destroyed" : "dead"; dam_level = MDAM_ALMOST_DEAD; return; } if (monster->hit_points <= monster->max_hit_points / 4) { desc += "severely "; dam_level = MDAM_SEVERELY_DAMAGED; } else if (monster->hit_points <= monster->max_hit_points / 3) { desc += "heavily "; dam_level = MDAM_HEAVILY_DAMAGED; } else if (monster->hit_points <= monster->max_hit_points * 3 / 4) { desc += "moderately "; dam_level = MDAM_MODERATELY_DAMAGED; } else if (monster->hit_points < monster->max_hit_points) { desc += "lightly "; dam_level = MDAM_LIGHTLY_DAMAGED; } else { desc += "not "; dam_level = MDAM_OKAY; } desc += _wounded_damaged(monster->type) ? "damaged" : "wounded"; } std::string get_wounds_description(const monsters *monster) { if (!monster->alive() || monster->hit_points == monster->max_hit_points) return ""; if (monster_descriptor(monster->type, MDSC_NOMSG_WOUNDS)) return ""; std::string desc; mon_dam_level_type dam_level; mons_get_damage_level(monster, desc, dam_level); desc.insert(0, " is "); desc += "."; return desc; } void print_wounds(const monsters *monster) { if (!monster->alive() || monster->hit_points == monster->max_hit_points) return; if (monster_descriptor(monster->type, MDSC_NOMSG_WOUNDS)) return; std::string desc; mon_dam_level_type dam_level; mons_get_damage_level(monster, desc, dam_level); desc.insert(0, " is "); desc += "."; simple_monster_message(monster, desc.c_str(), MSGCH_MONSTER_DAMAGE, dam_level); } // (true == 'damaged') [constructs, undead, etc.] // and (false == 'wounded') [living creatures, etc.] {dlb} static bool _wounded_damaged(monster_type mon_type) { // this schema needs to be abstracted into real categories {dlb}: const mon_holy_type holi = mons_class_holiness(mon_type); return (holi == MH_UNDEAD || holi == MH_NONLIVING || holi == MH_PLANT); } // If _mons_find_level_exits() is ever expanded to handle more grid // types, this should be expanded along with it. static void _mons_indicate_level_exit(const monsters *mon) { const dungeon_feature_type feat = grd(mon->pos()); const bool is_shaft = (get_trap_type(mon->pos()) == TRAP_SHAFT); if (feat_is_gate(feat)) simple_monster_message(mon, " passes through the gate."); else if (feat_is_travelable_stair(feat)) { command_type dir = feat_stair_direction(feat); simple_monster_message(mon, make_stringf(" %s the %s.", dir == CMD_GO_UPSTAIRS ? "goes up" : dir == CMD_GO_DOWNSTAIRS ? "goes down" : "takes", feat_is_escape_hatch(feat) ? "escape hatch" : "stairs").c_str()); } else if (is_shaft) { simple_monster_message(mon, make_stringf(" %s the shaft.", mons_flies(mon) ? "goes down" : "jumps into").c_str()); } } void make_mons_leave_level(monsters *mon) { if (mon->pacified()) { if (you.can_see(mon)) _mons_indicate_level_exit(mon); // Pacified monsters leaving the level take their stuff with // them. mon->flags |= MF_HARD_RESET; monster_die(mon, KILL_DISMISSED, NON_MONSTER); } } // Checks whether there is a straight path from p1 to p2 that passes // through features >= allowed. // If it exists, such a path may be missed; on the other hand, it // is not guaranteed that p2 is visible from p1 according to LOS rules. // Not symmetric. bool can_go_straight(const coord_def& p1, const coord_def& p2, dungeon_feature_type allowed) { if (distance(p1, p2) > get_los_radius_sq()) return (false); dungeon_feature_type max_disallowed = DNGN_MAXOPAQUE; if (allowed != DNGN_UNSEEN) max_disallowed = static_cast(allowed - 1); return (!num_feats_between(p1, p2, DNGN_UNSEEN, max_disallowed, true, true)); } // The default suitable() function for choose_random_nearby_monster(). bool choose_any_monster(const monsters* mon) { return (true); } // Find a nearby monster and return its index, including you as a // possibility with probability weight. suitable() should return true // for the type of monster wanted. // If prefer_named is true, named monsters (including uniques) are twice // as likely to get chosen compared to non-named ones. // If prefer_priest is true, priestly monsters (including uniques) are // twice as likely to get chosen compared to non-priestly ones. monsters *choose_random_nearby_monster(int weight, bool (*suitable)(const monsters* mon), bool in_sight, bool prefer_named, bool prefer_priest) { return choose_random_monster_on_level(weight, suitable, in_sight, true, prefer_named, prefer_priest); } monsters *choose_random_monster_on_level(int weight, bool (*suitable)(const monsters* mon), bool in_sight, bool near_by, bool prefer_named, bool prefer_priest) { monsters *chosen = NULL; // A radius_iterator with radius == max(GXM, GYM) will sweep the // whole level. radius_iterator ri(you.pos(), near_by ? 9 : std::max(GXM, GYM), true, in_sight); for (; ri; ++ri) { if (monsters *mon = monster_at(*ri)) { if (suitable(mon)) { // FIXME: if the intent is to favour monsters // named by $DEITY, we should set a flag on the // monster (something like MF_DEITY_PREFERRED) and // use that instead of checking the name, given // that other monsters can also have names. // True, but it's currently only used for orcs, and // Blork and Urug also being preferred to non-named orcs // is fine, I think. Once more gods name followers (and // prefer them) that should be changed, of course. (jpeg) // Named or priestly monsters have doubled chances. int mon_weight = 1; if (prefer_named && mon->is_named()) mon_weight++; if (prefer_priest && mon->is_priest()) mon_weight++; if (x_chance_in_y(mon_weight, (weight += mon_weight))) chosen = mon; } } } return chosen; } // Note that this function *completely* blocks messaging for monsters // distant or invisible to the player ... look elsewhere for a function // permitting output of "It" messages for the invisible {dlb} // Intentionally avoids info and str_pass now. -- bwr bool simple_monster_message(const monsters *monster, const char *event, msg_channel_type channel, int param, description_level_type descrip) { if (mons_near(monster) && (channel == MSGCH_MONSTER_SPELL || channel == MSGCH_FRIEND_SPELL || monster->visible_to(&you))) { std::string msg = monster->name(descrip); msg += event; msg = apostrophise_fixup(msg); if (channel == MSGCH_PLAIN && monster->wont_attack()) channel = MSGCH_FRIEND_ACTION; mpr(msg.c_str(), channel, param); return (true); } return (false); } bool mons_avoids_cloud(const monsters *monster, cloud_struct cloud, bool placement) { bool extra_careful = placement; cloud_type cl_type = cloud.type; if (placement) extra_careful = true; // Berserk monsters are less careful and will blindly plow through any // dangerous cloud, just to kill you. {due} if (!extra_careful && monster->berserk()) return (false); switch (cl_type) { case CLOUD_MIASMA: // Even the dumbest monsters will avoid miasma if they can. return (!monster->res_rotting()); case CLOUD_FIRE: case CLOUD_FOREST_FIRE: if (monster->res_fire() > 1) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && monster->res_fire() < 0) return (true); if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); break; case CLOUD_STINK: if (monster->res_poison() > 0) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 0) return (true); if (x_chance_in_y(monster->hit_dice - 1, 5)) return (false); if (monster->hit_points >= random2avg(19, 2)) return (false); break; case CLOUD_COLD: if (monster->res_cold() > 1) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && monster->res_cold() < 0) return (true); if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); break; case CLOUD_POISON: if (monster->res_poison() > 0) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 0) return (true); if (monster->hit_points >= random2avg(37, 4)) return (false); break; case CLOUD_GREY_SMOKE: if (placement) return (false); // This isn't harmful, but dumb critters might think so. if (mons_intel(monster) > I_ANIMAL || coinflip()) return (false); if (monster->res_fire() > 0) return (false); if (monster->hit_points >= random2avg(19, 2)) return (false); break; case CLOUD_RAIN: // Fiery monsters dislike the rain. if (monster->is_fiery() && extra_careful) return (true); // We don't care about what's underneath the rain cloud if we can fly. if (monster->flight_mode() != FL_NONE) return (false); // These don't care about deep water. if (monster_habitable_grid(monster, DNGN_DEEP_WATER)) return (false); // This position could become deep water, and they might drown. if (grd(cloud.pos) == DNGN_SHALLOW_WATER) return (true); // Otherwise, it's safe for everyone else. return (false); break; default: break; } // Exceedingly dumb creatures will wander into harmful clouds. if (is_harmless_cloud(cl_type) || mons_intel(monster) == I_PLANT && !extra_careful) { return (false); } // If we get here, the cloud is potentially harmful. return (true); } // Like the above, but allow a monster to move from one damaging cloud // to another, even if they're of different types. bool mons_avoids_cloud(const monsters *monster, int cloud_num, cloud_type *cl_type, bool placement) { if (cloud_num == EMPTY_CLOUD) { if (cl_type != NULL) *cl_type = CLOUD_NONE; return (false); } const cloud_struct &cloud = env.cloud[cloud_num]; if (cl_type != NULL) *cl_type = cloud.type; // Is the target cloud okay? if (!mons_avoids_cloud(monster, cloud, placement)) return (false); // If we're already in a cloud that we'd want to avoid then moving // from one to the other is okay. if (!in_bounds(monster->pos()) || monster->pos() == cloud.pos) return (true); const int our_cloud_num = env.cgrid(monster->pos()); if (our_cloud_num == EMPTY_CLOUD) return (true); const cloud_struct &our_cloud = env.cloud[our_cloud_num]; return (!mons_avoids_cloud(monster, our_cloud, true)); } // Returns a rough estimate of damage from throwing the wielded weapon. int mons_thrown_weapon_damage(const item_def *weap, bool only_returning_weapons) { if (!weap || (only_returning_weapons && get_weapon_brand(*weap) != SPWPN_RETURNING)) { return (0); } return std::max(0, (property(*weap, PWPN_DAMAGE) + weap->plus2 / 2)); } int mons_weapon_damage_rating(const item_def &launcher) { return (property(launcher, PWPN_DAMAGE) + launcher.plus2); } // Returns a rough estimate of damage from firing/throwing missile. int mons_missile_damage(monsters *mons, const item_def *launch, const item_def *missile) { if (!missile || (!launch && !is_throwable(mons, *missile))) return (0); const int missile_damage = property(*missile, PWPN_DAMAGE) / 2 + 1; const int launch_damage = launch? property(*launch, PWPN_DAMAGE) : 0; return std::max(0, launch_damage + missile_damage); } // Given the monster's current weapon and alt weapon (either or both of // which may be NULL), works out whether using missiles or throwing the // main weapon (with returning brand) is better. If using missiles that // need a launcher, sets *launcher to the launcher. // // If the monster has no ranged weapon attack, returns NON_ITEM. // int mons_pick_best_missile(monsters *mons, item_def **launcher, bool ignore_melee) { *launcher = NULL; item_def *melee = NULL, *launch = NULL; int melee_weapon_count = 0; for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) { if (item_def *item = mons->mslot_item(static_cast(i))) { if (is_range_weapon(*item)) launch = item; else if (!ignore_melee) { melee = item; ++melee_weapon_count; } } } const item_def *missiles = mons->missiles(); if (launch && missiles && !missiles->launched_by(*launch)) launch = NULL; const int n_usable_melee_weapons(mons_wields_two_weapons(mons) ? 2 : 1); const int tdam = mons_thrown_weapon_damage( melee, melee_weapon_count == n_usable_melee_weapons && melee->quantity == 1); const int fdam = mons_missile_damage(mons, launch, missiles); if (!tdam && !fdam) return (NON_ITEM); else if (tdam >= fdam) return (melee->index()); else { *launcher = launch; return (missiles->index()); } } int mons_natural_regen_rate(monsters *monster) { // A HD divider ranging from 3 (at 1 HD) to 1 (at 8 HD). int divider = std::max(div_rand_round(15 - monster->hit_dice, 4), 1); // The undead have a harder time regenerating. Golems have it worse. switch (monster->holiness()) { case MH_UNDEAD: divider *= (mons_enslaved_soul(monster)) ? 2 : 4; break; // And golems have it worse. case MH_NONLIVING: divider *= 5; break; default: break; } return (std::max(div_rand_round(monster->hit_dice, divider), 1)); } void mons_check_pool(monsters *monster, const coord_def &oldpos, killer_type killer, int killnum) { // Levitating/flying monsters don't make contact with the terrain. if (monster->airborne()) return; dungeon_feature_type grid = grd(monster->pos()); if ((grid == DNGN_LAVA || grid == DNGN_DEEP_WATER) && !monster_habitable_grid(monster, grid)) { const bool message = mons_near(monster); // Don't worry about invisibility. You should be able to see if // something has fallen into the lava. if (message && (oldpos == monster->pos() || grd(oldpos) != grid)) { mprf("%s falls into the %s!", monster->name(DESC_CAP_THE).c_str(), grid == DNGN_LAVA ? "lava" : "water"); } if (grid == DNGN_LAVA && monster->res_fire() >= 2) grid = DNGN_DEEP_WATER; // Even fire resistant monsters perish in lava, but inanimate // monsters can survive deep water. if (grid == DNGN_LAVA || monster->can_drown()) { if (message) { if (grid == DNGN_LAVA) { simple_monster_message(monster, " is incinerated.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } else if (mons_genus(monster->type) == MONS_MUMMY) { simple_monster_message(monster, " falls apart.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } else { simple_monster_message(monster, " drowns.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } } if (killer == KILL_NONE) { // Self-kill. killer = KILL_MON; killnum = monster->mindex(); } // Yredelemnul special, redux: It's the only one that can // work on drowned monsters. if (!_yred_enslave_soul(monster, killer)) monster_die(monster, killer, killnum, true); } } } bool monster_descriptor(int which_class, mon_desc_type which_descriptor) { if (which_descriptor == MDSC_LEAVES_HIDE) { switch (which_class) { case MONS_DRAGON: case MONS_TROLL: case MONS_ICE_DRAGON: case MONS_STEAM_DRAGON: case MONS_MOTTLED_DRAGON: case MONS_STORM_DRAGON: case MONS_GOLDEN_DRAGON: case MONS_SWAMP_DRAGON: case MONS_YAK: case MONS_SHEEP: return (true); default: return (false); } } if (which_descriptor == MDSC_REGENERATES) { switch (which_class) { case MONS_CACODEMON: case MONS_DEEP_TROLL: case MONS_HELLWING: case MONS_IMP: case MONS_IRON_TROLL: case MONS_LEMURE: case MONS_ROCK_TROLL: case MONS_SLIME_CREATURE: case MONS_SNORG: case MONS_PURGY: case MONS_TROLL: case MONS_HYDRA: case MONS_KILLER_KLOWN: case MONS_LERNAEAN_HYDRA: case MONS_DISSOLUTION: return (true); default: return (false); } } if (which_descriptor == MDSC_NOMSG_WOUNDS) { // Zombified monsters other than spectral things don't show // wounds. if (mons_class_is_zombified(which_class) && which_class != MONS_SPECTRAL_THING) { return (true); } switch (which_class) { case MONS_RAKSHASA: case MONS_RAKSHASA_FAKE: return (true); default: return (false); } } return (false); } monsters *get_current_target() { if (invalid_monster_index(you.prev_targ)) return NULL; monsters* mon = &menv[you.prev_targ]; if (mon->alive() && you.can_see(mon)) return mon; else return NULL; } void seen_monster(monsters *monster) { // If the monster is in the auto_exclude list, automatically // set an exclusion. set_auto_exclude(monster); // Monster was viewed this turn monster->flags |= MF_WAS_IN_VIEW; if (monster->flags & MF_SEEN) return; // First time we've seen this particular monster. monster->flags |= MF_SEEN; if (!mons_is_mimic(monster->type)) { if (Tutorial.tutorial_left) tutorial_monster_seen(*monster); if (MONST_INTERESTING(monster)) { take_note( Note(NOTE_SEEN_MONSTER, monster->type, 0, monster->name(DESC_NOCAP_A, true).c_str())); } } } //--------------------------------------------------------------- // // shift_monster // // Moves a monster to approximately p and returns true if // the monster was moved. // //--------------------------------------------------------------- bool shift_monster(monsters *mon, coord_def p) { coord_def result; int count = 0; if (p.origin()) p = mon->pos(); for (adjacent_iterator ai(p); ai; ++ai) { // Don't drop on anything but vanilla floor right now. if (grd(*ai) != DNGN_FLOOR) continue; if (actor_at(*ai)) continue; if (one_chance_in(++count)) result = *ai; } if (count > 0) { mgrd(mon->pos()) = NON_MONSTER; mon->moveto(result); mgrd(result) = mon->mindex(); } return (count > 0); } // Make all of the monster's original equipment disappear, unless it's a fixed // artefact or unrand artefact. static void _vanish_orig_eq(monsters* mons) { for (int i = 0; i < NUM_MONSTER_SLOTS; ++i) { if (mons->inv[i] == NON_ITEM) continue; item_def &item(mitm[mons->inv[i]]); if (!item.is_valid()) continue; if (item.orig_place != 0 || item.orig_monnum != 0 || !item.inscription.empty() || is_unrandom_artefact(item) || (item.flags & (ISFLAG_DROPPED | ISFLAG_THROWN | ISFLAG_NOTED_GET | ISFLAG_BEEN_IN_INV) ) ) { continue; } item.flags |= ISFLAG_SUMMONED; } } int dismiss_monsters(std::string pattern) { // Make all of the monsters' original equipment disappear unless "keepitem" // is found in the regex (except for fixed arts and unrand arts). const bool keep_item = strip_tag(pattern, "keepitem"); // Dismiss by regex text_pattern tpat(pattern); int ndismissed = 0; for (monster_iterator mi; mi; ++mi) { if (mi->alive() && (tpat.empty() || tpat.matches(mi->name(DESC_PLAIN, true)))) { if (!keep_item) _vanish_orig_eq(*mi); monster_die(*mi, KILL_DISMISSED, NON_MONSTER, false, true); ++ndismissed; } } return (ndismissed); } bool is_item_jelly_edible(const item_def &item) { // Don't eat artefacts. if (is_artefact(item)) return (false); // Shouldn't eat stone things // - but what about wands and rings? if (item.base_type == OBJ_MISSILES && (item.sub_type == MI_STONE || item.sub_type == MI_LARGE_ROCK)) { return (false); } // Don't eat special game items. if (item.base_type == OBJ_ORBS || (item.base_type == OBJ_MISCELLANY && (item.sub_type == MISC_RUNE_OF_ZOT || item.sub_type == MISC_HORN_OF_GERYON))) { return (false); } return (true); } bool monster_random_space(const monsters *monster, coord_def& target, bool forbid_sanctuary) { int tries = 0; while (tries++ < 1000) { target = random_in_bounds(); // Don't land on top of another monster. if (actor_at(target)) continue; if (is_sanctuary(target) && forbid_sanctuary) continue; if (monster_habitable_grid(monster, grd(target))) return (true); } return (false); } bool monster_random_space(monster_type mon, coord_def& target, bool forbid_sanctuary) { monsters dummy; dummy.type = mon; return monster_random_space(&dummy, target, forbid_sanctuary); } void monster_teleport(monsters *monster, bool instan, bool silent) { if (!instan) { if (monster->del_ench(ENCH_TP)) { if (!silent) simple_monster_message(monster, " seems more stable."); } else { if (!silent) simple_monster_message(monster, " looks slightly unstable."); monster->add_ench( mon_enchant(ENCH_TP, 0, KC_OTHER, random_range(20, 30)) ); } return; } bool was_seen = you.can_see(monster) && !mons_is_lurking(monster); if (!silent) simple_monster_message(monster, " disappears!"); const coord_def oldplace = monster->pos(); // Pick the monster up. mgrd(oldplace) = NON_MONSTER; coord_def newpos; if (monster_random_space(monster, newpos, !monster->wont_attack())) monster->moveto(newpos); mgrd(monster->pos()) = monster->mindex(); // Mimics change form/colour when teleported. if (mons_is_mimic(monster->type)) { monster_type old_type = monster->type; monster->type = static_cast( MONS_GOLD_MIMIC + random2(5)); monster->destroy_inventory(); give_mimic_item(monster); monster->colour = get_mimic_colour(monster); was_seen = false; // If it's changed form, you won't recognise it. // This assumes that a non-gold mimic turning into another item of // the same description is really, really unlikely. if (old_type != MONS_GOLD_MIMIC || monster->type != MONS_GOLD_MIMIC) was_seen = false; } const bool now_visible = mons_near(monster); if (!silent && now_visible) { if (was_seen) simple_monster_message(monster, " reappears nearby!"); else { // Even if it doesn't interrupt an activity (the player isn't // delayed, the monster isn't hostile) we still want to give // a message. activity_interrupt_data ai(monster, "thin air"); if (!interrupt_activity(AI_SEE_MONSTER, ai)) simple_monster_message(monster, " appears out of thin air!"); } } if (monster->visible_to(&you) && now_visible) handle_seen_interrupt(monster); // Leave a purple cloud. place_cloud(CLOUD_TLOC_ENERGY, oldplace, 1 + random2(3), monster->kill_alignment(), KILL_MON_MISSILE); monster->check_redraw(oldplace); monster->apply_location_effects(oldplace); mons_relocated(monster); // Teleporting mimics change form - if they reappear out of LOS, they are // no longer known. if (mons_is_mimic(monster->type)) { if (now_visible) monster->flags |= MF_KNOWN_MIMIC; else monster->flags &= ~MF_KNOWN_MIMIC; } } void mons_clear_trapping_net(monsters *mon) { if (!mon->caught()) return; const int net = get_trapping_net(mon->pos()); if (net != NON_ITEM) remove_item_stationary(mitm[net]); mon->del_ench(ENCH_HELD, true); } bool mons_clonable(const monsters* mon, bool needs_adjacent) { // No uniques or ghost demon monsters. Also, figuring out the name // for the clone of a named monster isn't worth it. if (mons_is_unique(mon->type) || mons_is_ghost_demon(mon->type) || mon->is_named()) { return (false); } if (needs_adjacent) { // Is there space for the clone? bool square_found = false; for (int i = 0; i < 8; i++) { const coord_def p = mon->pos() + Compass[i]; if (in_bounds(p) && !actor_at(p) && monster_habitable_grid(mon, grd(p))) { square_found = true; break; } } if (!square_found) return (false); } // Is the monster carrying an artefact? for (int i = 0; i < NUM_MONSTER_SLOTS; i++) { const int index = mon->inv[i]; if (index == NON_ITEM) continue; if (is_artefact(mitm[index])) return (false); } return (true); } int clone_mons(const monsters* orig, bool quiet, bool* obvious, coord_def pos) { // Is there an open slot in menv? monsters* mons = get_free_monster(); if (!mons) return (NON_MONSTER); if (!in_bounds(pos)) { // Find an adjacent square. int squares = 0; for (int i = 0; i < 8; i++) { const coord_def p = orig->pos() + Compass[i]; if (in_bounds(p) && !actor_at(p) && monster_habitable_grid(orig, grd(p))) { if (one_chance_in(++squares)) pos = p; } } if (squares == 0) return (NON_MONSTER); } ASSERT( !actor_at(pos) ); *mons = *orig; mons->set_position(pos); mgrd(pos) = mons->mindex(); // Duplicate objects, or unequip them if they can't be duplicated. for (int i = 0; i < NUM_MONSTER_SLOTS; i++) { const int old_index = orig->inv[i]; if (old_index == NON_ITEM) continue; const int new_index = get_item_slot(0); if (new_index == NON_ITEM) { mons->unequip(mitm[old_index], i, 0, true); mons->inv[i] = NON_ITEM; continue; } mons->inv[i] = new_index; mitm[new_index] = mitm[old_index]; mitm[new_index].set_holding_monster(mons->mindex()); } bool _obvious; if (obvious == NULL) obvious = &_obvious; *obvious = false; if (you.can_see(orig) && you.can_see(mons)) { if (!quiet) simple_monster_message(orig, " is duplicated!"); *obvious = true; } mark_interesting_monst(mons, mons->behaviour); if (you.can_see(mons)) { handle_seen_interrupt(mons); viewwindow(false); } if (crawl_state.arena) arena_placed_monster(mons); return (mons->mindex()); } std::string summoned_poof_msg(const monsters* monster, bool plural) { int summon_type = 0; bool valid_mon = false; if (monster != NULL && !invalid_monster(monster)) { (void) monster->is_summoned(NULL, &summon_type); valid_mon = true; } std::string msg = "disappear%s in a puff of smoke"; bool no_chaos = false; switch (summon_type) { case SPELL_SHADOW_CREATURES: msg = "dissolve%s into shadows"; no_chaos = true; break; case MON_SUMM_CHAOS: msg = "degenerate%s into a cloud of primal chaos"; break; case MON_SUMM_WRATH: case MON_SUMM_AID: if (valid_mon && is_good_god(monster->god)) { msg = "dissolve%s into sparkling lights"; no_chaos = true; } break; } if (valid_mon) { if (monster->god == GOD_XOM && !no_chaos && one_chance_in(10) || monster->type == MONS_CHAOS_SPAWN) { msg = "degenerate%s into a cloud of primal chaos"; } if (monster->is_holy() && summon_type != SPELL_SHADOW_CREATURES && summon_type != MON_SUMM_CHAOS) { msg = "dissolve%s into sparkling lights"; } } // Conjugate. msg = make_stringf(msg.c_str(), plural ? "" : "s"); return (msg); } std::string summoned_poof_msg(const int midx, const item_def &item) { if (midx == NON_MONSTER) return summoned_poof_msg(static_cast(NULL), item); else return summoned_poof_msg(&menv[midx], item); } std::string summoned_poof_msg(const monsters* monster, const item_def &item) { ASSERT(item.flags & ISFLAG_SUMMONED); return summoned_poof_msg(monster, item.quantity > 1); } bool mons_reaped(actor *killer, monsters *victim) { beh_type beh; unsigned short hitting; if (killer->atype() == ACT_PLAYER) { hitting = MHITYOU; beh = BEH_FRIENDLY; } else { monsters *mon = dynamic_cast(killer); beh = SAME_ATTITUDE(mon); // Get a new foe for the zombie to target. behaviour_event(mon, ME_EVAL); hitting = mon->foe; } int midx = NON_MONSTER; if (animate_remains(victim->pos(), CORPSE_BODY, beh, hitting, killer, "", GOD_NO_GOD, true, true, true, &midx) <= 0) { return (false); } monsters *zombie = &menv[midx]; if (you.can_see(victim)) mprf("%s turns into a zombie!", victim->name(DESC_CAP_THE).c_str()); else if (you.can_see(zombie)) mprf("%s appears out of thin air!", zombie->name(DESC_CAP_THE).c_str()); player_angers_monster(zombie); return (true); }