From 020b3d8b5ccc12e63f38d0f81c828ff4569a7933 Mon Sep 17 00:00:00 2001 From: Matthew Cline Date: Sat, 31 Oct 2009 19:33:56 -0700 Subject: monster.cc: Split of monsters clasxs methods First step in splitting up mon-util.cc: move monsters class methods out into monster.cc --- crawl-ref/source/monster.cc | 5611 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 5611 insertions(+) create mode 100644 crawl-ref/source/monster.cc (limited to 'crawl-ref/source/monster.cc') diff --git a/crawl-ref/source/monster.cc b/crawl-ref/source/monster.cc new file mode 100644 index 0000000000..a12666dc2e --- /dev/null +++ b/crawl-ref/source/monster.cc @@ -0,0 +1,5611 @@ +/* + * File: monster.cc + * Summary: Monsters class methods + * Written by: Linley Henzell + */ + +#include "AppHdr.h" + +#include "beam.h" +#include "cloud.h" +#include "delay.h" +#include "dgnevent.h" +#include "directn.h" +#include "fight.h" +#include "ghost.h" +#include "goditem.h" +#include "items.h" +#include "kills.h" +#include "misc.h" +#include "monplace.h" +#include "monstuff.h" +#include "mstuff2.h" +#include "mtransit.h" +#include "random.h" +#include "religion.h" +#include "shopping.h" // for item values +#include "spells3.h" +#include "state.h" +#include "traps.h" +#include "tutorial.h" +#include "view.h" +#include "xom.h" + +struct mon_spellbook +{ + mon_spellbook_type type; + spell_type spells[NUM_MONSTER_SPELL_SLOTS]; +}; + +static mon_spellbook mspell_list[] = { +#include "mon-spll.h" +}; + +// Macro that saves some typing, nothing more. +#define smc get_monster_data(mc) + +monsters::monsters() + : type(MONS_NO_MONSTER), hit_points(0), max_hit_points(0), hit_dice(0), + ac(0), ev(0), speed(0), speed_increment(0), + target(), patrol_point(), travel_target(MTRAV_NONE), + inv(NON_ITEM), spells(), attitude(ATT_HOSTILE), behaviour(BEH_WANDER), + foe(MHITYOU), enchantments(), flags(0L), experience(0), number(0), + colour(BLACK), foe_memory(0), shield_blocks(0), god(GOD_NO_GOD), ghost(), + seen_context("") +{ + travel_path.clear(); +} + +// Empty destructor to keep auto_ptr happy with incomplete ghost_demon type. +monsters::~monsters() +{ +} + +monsters::monsters(const monsters &mon) +{ + init_with(mon); +} + +monsters &monsters::operator = (const monsters &mon) +{ + if (this != &mon) + init_with(mon); + return (*this); +} + +void monsters::reset() +{ + mname.clear(); + enchantments.clear(); + ench_countdown = 0; + inv.init(NON_ITEM); + + flags = 0; + experience = 0L; + type = MONS_NO_MONSTER; + base_monster = MONS_NO_MONSTER; + hit_points = 0; + max_hit_points = 0; + hit_dice = 0; + ac = 0; + ev = 0; + speed_increment = 0; + attitude = ATT_HOSTILE; + behaviour = BEH_SLEEP; + foe = MHITNOT; + number = 0; + + if (in_bounds(pos())) + mgrd(pos()) = NON_MONSTER; + + position.reset(); + patrol_point.reset(); + travel_target = MTRAV_NONE; + travel_path.clear(); + ghost.reset(NULL); + seen_context = ""; +} + +void monsters::init_with(const monsters &mon) +{ + mname = mon.mname; + type = mon.type; + base_monster = mon.base_monster; + hit_points = mon.hit_points; + max_hit_points = mon.max_hit_points; + hit_dice = mon.hit_dice; + ac = mon.ac; + ev = mon.ev; + speed = mon.speed; + speed_increment = mon.speed_increment; + position = mon.position; + target = mon.target; + patrol_point = mon.patrol_point; + travel_target = mon.travel_target; + travel_path = mon.travel_path; + inv = mon.inv; + spells = mon.spells; + attitude = mon.attitude; + behaviour = mon.behaviour; + foe = mon.foe; + enchantments = mon.enchantments; + flags = mon.flags; + experience = mon.experience; + number = mon.number; + colour = mon.colour; + foe_memory = mon.foe_memory; + god = mon.god; + + if (mon.ghost.get()) + ghost.reset(new ghost_demon(*mon.ghost)); + else + ghost.reset(NULL); +} + +mon_attitude_type monsters::temp_attitude() const +{ + if (has_ench(ENCH_CHARM)) + return ATT_FRIENDLY; + else if (has_ench(ENCH_NEUTRAL)) + return ATT_NEUTRAL; + else + return attitude; +} + +bool monsters::swimming() const +{ + const dungeon_feature_type grid = grd(pos()); + return (feat_is_watery(grid) && mons_primary_habitat(this) == HT_WATER); +} + +static bool _player_near_water() +{ + for (adjacent_iterator ai(you.pos()); ai; ++ai) + if (feat_is_water(grd(*ai))) + return (true); + return (false); +} + +bool monsters::wants_submerge() const +{ + // Krakens never retreat when food (the player) is in range. + if (type == MONS_KRAKEN) + if (_player_near_water()) + return (false); + + // If we're in distress, we usually want to submerge. + if (env.cgrid(pos()) != EMPTY_CLOUD + || (hit_points < max_hit_points / 2 + && random2(max_hit_points + 1) >= hit_points)) + { + return (true); + } + + // Trapdoor spiders only hide themselves under the floor when they + // can't see their prey. + if (type == MONS_TRAPDOOR_SPIDER) + { + const actor* _foe = get_foe(); + return (_foe == NULL || !can_see(_foe)); + } + + const bool has_ranged_attack = (type == MONS_ELECTRIC_EEL + || type == MONS_LAVA_SNAKE + || mons_genus(type) == MONS_MERMAID + && you.species != SP_MERFOLK); + + int roll = 8; + // Shallow water takes a little more effort to submerge in, so we're + // less likely to bother. + if (grd(pos()) == DNGN_SHALLOW_WATER) + roll = roll * 7 / 5; + + const actor *tfoe = get_foe(); + if (tfoe && grid_distance(tfoe->pos(), pos()) > 1 && !has_ranged_attack) + roll /= 2; + + // Don't submerge if we just unsubmerged to shout + return (one_chance_in(roll) && seen_context != "bursts forth shouting"); +} + +bool monsters::submerged() const +{ + // FIXME, switch to 4.1's MF_SUBMERGED system which is much cleaner. + // Can't find any reference to MF_SUBMERGED anywhere. Don't know what + // this means. - abrahamwl + if (has_ench(ENCH_SUBMERGED)) + return (true); + + if (grd(pos()) == DNGN_DEEP_WATER + && !monster_habitable_grid(this, DNGN_DEEP_WATER)) + { + return (true); + } + + return (false); +} + +bool monsters::extra_balanced() const +{ + return (mons_genus(type) == MONS_NAGA); +} + +bool monsters::floundering() const +{ + const dungeon_feature_type grid = grd(pos()); + return (feat_is_water(grid) + && !cannot_fight() + // Can't use monster_habitable_grid() because that'll return + // true for non-water monsters in shallow water. + && mons_primary_habitat(this) != HT_WATER + && !mons_amphibious(this) + && !mons_flies(this) + && !extra_balanced()); +} + +bool monsters::can_pass_through_feat(dungeon_feature_type grid) const +{ + return mons_can_pass(this, grid); +} + +bool monsters::is_habitable_feat(dungeon_feature_type actual_grid) const +{ + return monster_habitable_grid(this, actual_grid); +} + +bool monsters::can_drown() const +{ + // Presumably a shark in lava or a lavafish in deep water could + // drown, but that should never happen, so this simple check should + // be enough. + switch (mons_primary_habitat(this)) + { + case HT_WATER: + case HT_LAVA: + return (false); + default: + break; + } + + // Mummies can fall apart in water or be incinerated in lava. + // Ghouls, vampires, and demons can drown in water or lava. Others + // just "sink like a rock", to never be seen again. + return (!res_asphyx() + || mons_genus(type) == MONS_MUMMY + || mons_genus(type) == MONS_GHOUL + || mons_genus(type) == MONS_VAMPIRE + || holiness() == MH_DEMONIC); +} + +size_type monsters::body_size(size_part_type /* psize */, bool /* base */) const +{ + const monsterentry *e = get_monster_data(type); + return (e ? e->size : SIZE_MEDIUM); +} + +int monsters::body_weight() const +{ + int mclass = type; + + switch (mclass) + { + case MONS_SPECTRAL_THING: + case MONS_SPECTRAL_WARRIOR: + case MONS_ELECTRIC_GOLEM: + case MONS_RAKSHASA_FAKE: + return (0); + + case MONS_ZOMBIE_SMALL: + case MONS_ZOMBIE_LARGE: + case MONS_SKELETON_SMALL: + case MONS_SKELETON_LARGE: + case MONS_SIMULACRUM_SMALL: + case MONS_SIMULACRUM_LARGE: + mclass = number; + break; + + default: + break; + } + + int weight = mons_weight(mclass); + + // weight == 0 in the monster entry indicates "no corpse". Can't + // use CE_NOCORPSE, because the corpse-effect field is used for + // corpseless monsters to indicate what happens if their blood + // is sucked. Grrrr. + if (weight == 0 && !mons_is_insubstantial(type)) + { + const monsterentry *entry = get_monster_data(mclass); + switch (entry->size) + { + case SIZE_TINY: + weight = 150; + break; + case SIZE_LITTLE: + weight = 300; + break; + case SIZE_SMALL: + weight = 425; + break; + case SIZE_MEDIUM: + weight = 550; + break; + case SIZE_LARGE: + weight = 1300; + break; + case SIZE_BIG: + weight = 1500; + break; + case SIZE_GIANT: + weight = 1800; + break; + case SIZE_HUGE: + weight = 2200; + break; + default: + mpr("ERROR: invalid monster body weight"); + perror("monsters::body_weight(): invalid monster body weight"); + end(0); + } + + switch (mclass) + { + case MONS_IRON_DEVIL: + weight += 550; + break; + + case MONS_STONE_GOLEM: + case MONS_EARTH_ELEMENTAL: + case MONS_CRYSTAL_GOLEM: + weight *= 2; + break; + + case MONS_IRON_DRAGON: + case MONS_IRON_GOLEM: + weight *= 3; + break; + + case MONS_QUICKSILVER_DRAGON: + case MONS_SILVER_STATUE: + weight *= 4; + break; + + case MONS_WOOD_GOLEM: + weight *= 2; + weight /= 3; + break; + + case MONS_FLYING_SKULL: + case MONS_CURSE_SKULL: + case MONS_SKELETAL_DRAGON: + case MONS_SKELETAL_WARRIOR: + weight /= 2; + break; + + case MONS_SHADOW_FIEND: + case MONS_SHADOW_IMP: + case MONS_SHADOW_DEMON: + weight /= 3; + break; + } + + switch (mons_char(mclass)) + { + case 'L': + weight /= 2; + break; + + case 'p': + weight = 0; + break; + } + } + + if (type == MONS_SKELETON_SMALL || type == MONS_SKELETON_LARGE) + weight /= 2; + + return (weight); +} + +int monsters::total_weight() const +{ + int burden = 0; + + for (int i = 0; i < NUM_MONSTER_SLOTS; i++) + if (inv[i] != NON_ITEM) + burden += item_mass(mitm[inv[i]]) * mitm[inv[i]].quantity; + + return (body_weight() + burden); +} + +int monsters::damage_brand(int which_attack) +{ + const item_def *mweap = weapon(which_attack); + + if (!mweap) + { + if (mons_is_ghost_demon(type)) + return (ghost->brand); + + return (SPWPN_NORMAL); + } + + return (!is_range_weapon(*mweap) ? get_weapon_brand(*mweap) : SPWPN_NORMAL); +} + +int monsters::damage_type(int which_attack) +{ + const item_def *mweap = weapon(which_attack); + + if (!mweap) + { + const mon_attack_def atk = mons_attack_spec(this, which_attack); + return ((atk.type == AT_CLAW) ? DVORP_CLAWING : + (atk.type == AT_TENTACLE_SLAP) ? DVORP_TENTACLE + : DVORP_CRUSHING); + } + + return (get_vorpal_type(*mweap)); +} + +item_def *monsters::missiles() +{ + return (inv[MSLOT_MISSILE] != NON_ITEM ? &mitm[inv[MSLOT_MISSILE]] : NULL); +} + +int monsters::missile_count() +{ + if (const item_def *missile = missiles()) + return (missile->quantity); + + return (0); +} + +item_def *monsters::launcher() +{ + item_def *weap = mslot_item(MSLOT_WEAPON); + if (weap && is_range_weapon(*weap)) + return (weap); + + weap = mslot_item(MSLOT_ALT_WEAPON); + return (weap && is_range_weapon(*weap) ? weap : NULL); +} + +// Does not check whether the monster can dual-wield - that is the +// caller's responsibility. +static int _mons_offhand_weapon_index(const monsters *m) +{ + return (m->inv[MSLOT_ALT_WEAPON]); +} + +item_def *monsters::weapon(int which_attack) +{ + const mon_attack_def attk = mons_attack_spec(this, which_attack); + if (attk.type != AT_HIT) + return (NULL); + + // Even/odd attacks use main/offhand weapon. + if (which_attack > 1) + which_attack &= 1; + + // This randomly picks one of the wielded weapons for monsters that can use + // two weapons. Not ideal, but better than nothing. fight.cc does it right, + // for various values of right. + int weap = inv[MSLOT_WEAPON]; + + if (which_attack && mons_wields_two_weapons(this)) + { + const int offhand = _mons_offhand_weapon_index(this); + if (offhand != NON_ITEM + && (weap == NON_ITEM || which_attack == 1 || coinflip())) + { + weap = offhand; + } + } + + return (weap == NON_ITEM ? NULL : &mitm[weap]); +} + +bool monsters::can_wield(const item_def& item, bool ignore_curse, + bool ignore_brand, bool ignore_shield, + bool ignore_transform) const +{ + // Monsters can only wield weapons or go unarmed (OBJ_UNASSIGNED + // means unarmed). + if (item.base_type != OBJ_WEAPONS && item.base_type != OBJ_UNASSIGNED) + return (false); + + // These *are* weapons, so they can't wield another weapon or + // unwield themselves. + if (type == MONS_DANCING_WEAPON) + return (false); + + // MF_HARD_RESET means that all items the monster is carrying will + // disappear when it does, so it can't accept new items or give up + // the ones it has. + if (flags & MF_HARD_RESET) + return (false); + + // Summoned items can only be held by summoned monsters. + if ((item.flags & ISFLAG_SUMMONED) && !is_summoned()) + return (false); + + item_def* weap1 = NULL; + if (inv[MSLOT_WEAPON] != NON_ITEM) + weap1 = &mitm[inv[MSLOT_WEAPON]]; + + int avail_slots = 1; + item_def* weap2 = NULL; + if (mons_wields_two_weapons(this)) + { + if (!weap1 || hands_reqd(*weap1, body_size()) != HANDS_TWO) + avail_slots = 2; + + const int offhand = _mons_offhand_weapon_index(this); + if (offhand != NON_ITEM) + weap2 = &mitm[offhand]; + } + + // If we're already wielding it, then of course we can wield it. + if (&item == weap1 || &item == weap2) + return (true); + + // Barehanded needs two hands. + const bool two_handed = item.base_type == OBJ_UNASSIGNED + || hands_reqd(item, body_size()) == HANDS_TWO; + + item_def* _shield = NULL; + if (inv[MSLOT_SHIELD] != NON_ITEM) + { + ASSERT(!(weap1 && weap2)); + + if (two_handed && !ignore_shield) + return (false); + + _shield = &mitm[inv[MSLOT_SHIELD]]; + } + + if (!ignore_curse) + { + int num_cursed = 0; + if (weap1 && item_cursed(*weap1)) + num_cursed++; + if (weap2 && item_cursed(*weap2)) + num_cursed++; + if (_shield && item_cursed(*_shield)) + num_cursed++; + + if (two_handed && num_cursed > 0 || num_cursed >= avail_slots) + return (false); + } + + return could_wield(item, ignore_brand, ignore_transform); +} + +bool monsters::could_wield(const item_def &item, bool ignore_brand, + bool /* ignore_transform */) const +{ + ASSERT(is_valid_item(item)); + + // These *are* weapons, so they can't wield another weapon. + if (type == MONS_DANCING_WEAPON) + return (false); + + // Monsters can't use unrandarts with special effects. + if (is_special_unrandom_artefact(item) && !crawl_state.arena) + return (false); + + // Wimpy monsters (e.g. kobold, goblin) can't use halberds, etc. + if (!check_weapon_wieldable_size(item, body_size())) + return (false); + + if (!ignore_brand) + { + const int brand = get_weapon_brand(item); + + // Draconians won't use dragon slaying weapons. + if (brand == SPWPN_DRAGON_SLAYING && is_dragonkind(this)) + return (false); + + // Orcs won't use orc slaying weapons. + if (brand == SPWPN_ORC_SLAYING && is_orckind(this)) + return (false); + + // Demonic/undead monsters won't use holy weapons. + if (is_unholy() && is_holy_item(item)) + return (false); + + // Holy monsters and monsters that are gifts of good gods won't + // use evil weapons. + if ((mons_is_holy(this) || is_good_god(god)) + && is_evil_item(item)) + { + return (false); + } + + // Holy monsters that aren't gifts of chaotic gods and monsters + // that are gifts of good gods won't use chaotic weapons. + if (((mons_is_holy(this) && !is_chaotic_god(this->god)) + || is_good_god(god)) + && is_chaotic_item(item)) + { + return (false); + } + } + + return (true); +} + +bool monsters::can_throw_large_rocks() const +{ + return (type == MONS_STONE_GIANT + || ::mons_species(this->type) == MONS_CYCLOPS + || ::mons_species(this->type) == MONS_OGRE); +} + +bool monsters::has_spell_of_type(unsigned disciplines) const +{ + for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) + { + if (spells[i] == SPELL_NO_SPELL) + continue; + + if (spell_typematch(spells[i], disciplines)) + return (true); + } + return (false); +} + +static bool _needs_ranged_attack(const monsters *mon) +{ + // Prevent monsters that have conjurations from grabbing missiles. + if (mon->has_spell_of_type(SPTYP_CONJURATION)) + return (false); + + // Same for summonings, but make an exception for friendlies. + if (!mons_friendly(mon) && mon->has_spell_of_type(SPTYP_SUMMONING)) + return (false); + + // Blademasters don't want to throw stuff. + if (mon->type == MONS_DEEP_ELF_BLADEMASTER) + return (false); + + return (true); +} + +bool monsters::can_use_missile(const item_def &item) const +{ + // Don't allow monsters to pick up missiles without the corresponding + // launcher. The opposite is okay, and sufficient wandering will + // hopefully take the monster to a stack of appropriate missiles. + + if (!_needs_ranged_attack(this)) + return (false); + + if (item.base_type == OBJ_WEAPONS + || item.base_type == OBJ_MISSILES && !has_launcher(item)) + { + return (is_throwable(this, item)); + } + + // Darts and stones are allowed even without launcher. + if (item.sub_type == MI_DART || item.sub_type == MI_STONE) + return (true); + + item_def *launch; + for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) + { + launch = mslot_item(static_cast(i)); + if (launch && fires_ammo_type(*launch) == item.sub_type) + return (true); + } + + // No fitting launcher in inventory. + return (false); +} + +void monsters::swap_slots(mon_inv_type a, mon_inv_type b) +{ + const int swap = inv[a]; + inv[a] = inv[b]; + inv[b] = swap; +} + +void monsters::equip_weapon(item_def &item, int near, bool msg) +{ + if (msg && !need_message(near)) + msg = false; + + if (msg) + { + snprintf(info, INFO_SIZE, " wields %s.", + item.name(DESC_NOCAP_A, false, false, true, false, + ISFLAG_CURSED).c_str()); + msg = simple_monster_message(this, info); + } + + const int brand = get_weapon_brand(item); + if (brand == SPWPN_PROTECTION) + ac += 5; + + if (msg) + { + bool message_given = true; + switch (brand) + { + case SPWPN_FLAMING: + mpr("It bursts into flame!"); + break; + case SPWPN_FREEZING: + mpr("It glows with a cold blue light!"); + break; + case SPWPN_HOLY_WRATH: + mpr("It softly glows with a divine radiance!"); + break; + case SPWPN_ELECTROCUTION: + mpr("You hear the crackle of electricity.", MSGCH_SOUND); + break; + case SPWPN_VENOM: + mpr("It begins to drip with poison!"); + break; + case SPWPN_DRAINING: + mpr("You sense an unholy aura."); + break; + case SPWPN_FLAME: + mpr("It bursts into flame!"); + break; + case SPWPN_FROST: + mpr("It is covered in frost."); + break; + case SPWPN_RETURNING: + mpr("It wiggles slightly."); + break; + case SPWPN_DISTORTION: + mpr("Its appearance distorts for a moment."); + break; + case SPWPN_CHAOS: + mpr("It is briefly surrounded by a scintillating aura of " + "random colours."); + break; + case SPWPN_PENETRATION: + mprf("%s %s briefly pass through it before %s manages to get a " + "firm grip on it.", + pronoun(PRONOUN_CAP_POSSESSIVE).c_str(), + hand_name(true).c_str(), + pronoun(PRONOUN_NOCAP).c_str()); + break; + case SPWPN_REAPING: + mpr("It is briefly surrounded by shifting shadows."); + break; + + default: + // A ranged weapon without special message is known to be unbranded. + if (brand != SPWPN_NORMAL || !is_range_weapon(item)) + message_given = false; + } + + if (message_given) + { + if (is_artefact(item) && !is_special_unrandom_artefact(item)) + artefact_wpn_learn_prop(item, ARTP_BRAND); + else + set_ident_flags(item, ISFLAG_KNOW_TYPE); + } + } +} + +void monsters::equip_armour(item_def &item, int near) +{ + if (need_message(near)) + { + snprintf(info, INFO_SIZE, " wears %s.", + item.name(DESC_NOCAP_A).c_str()); + simple_monster_message(this, info); + } + + const equipment_type eq = get_armour_slot(item); + if (eq != EQ_SHIELD) + { + ac += property( item, PARM_AC ); + + const int armour_plus = item.plus; + ASSERT(abs(armour_plus) < 20); + if (abs(armour_plus) < 20) + ac += armour_plus; + } + + // Shields can affect evasion. + ev += property( item, PARM_EVASION ) / 2; + if (ev < 1) + ev = 1; // This *shouldn't* happen. +} + +void monsters::equip(item_def &item, int slot, int near) +{ + switch (item.base_type) + { + case OBJ_WEAPONS: + { + bool give_msg = (slot == MSLOT_WEAPON || mons_wields_two_weapons(this)); + equip_weapon(item, near, give_msg); + break; + } + case OBJ_ARMOUR: + equip_armour(item, near); + break; + default: + break; + } +} + +void monsters::unequip_weapon(item_def &item, int near, bool msg) +{ + if (msg && !need_message(near)) + msg = false; + + if (msg) + { + snprintf(info, INFO_SIZE, " unwields %s.", + item.name(DESC_NOCAP_A, false, false, true, false, + ISFLAG_CURSED).c_str()); + msg = simple_monster_message(this, info); + } + + const int brand = get_weapon_brand(item); + if (brand == SPWPN_PROTECTION) + ac -= 5; + + if (msg && brand != SPWPN_NORMAL) + { + bool message_given = true; + switch (brand) + { + case SPWPN_FLAMING: + mpr("It stops flaming."); + break; + + case SPWPN_HOLY_WRATH: + mpr("It stops glowing."); + break; + + case SPWPN_ELECTROCUTION: + mpr("It stops crackling."); + break; + + case SPWPN_VENOM: + mpr("It stops dripping with poison."); + break; + + case SPWPN_DISTORTION: + mpr("Its appearance distorts for a moment."); + break; + + default: + message_given = false; + } + if (message_given) + { + if (is_artefact(item) && !is_special_unrandom_artefact(item)) + artefact_wpn_learn_prop(item, ARTP_BRAND); + else + set_ident_flags(item, ISFLAG_KNOW_TYPE); + } + } +} + +void monsters::unequip_armour(item_def &item, int near) +{ + if (need_message(near)) + { + snprintf(info, INFO_SIZE, " takes off %s.", + item.name(DESC_NOCAP_A).c_str()); + simple_monster_message(this, info); + } + + const equipment_type eq = get_armour_slot(item); + if (eq != EQ_SHIELD) + { + ac -= property( item, PARM_AC ); + + const int armour_plus = item.plus; + ASSERT(abs(armour_plus) < 20); + if (abs(armour_plus) < 20) + ac -= armour_plus; + } + + ev -= property( item, PARM_EVASION ) / 2; + if (ev < 1) + ev = 1; // This *shouldn't* happen. +} + +bool monsters::unequip(item_def &item, int slot, int near, bool force) +{ + if (!force && item.cursed()) + return (false); + + if (!force && you.can_see(this)) + set_ident_flags(item, ISFLAG_KNOW_CURSE); + + switch (item.base_type) + { + case OBJ_WEAPONS: + { + bool give_msg = (slot == MSLOT_WEAPON || mons_wields_two_weapons(this)); + + unequip_weapon(item, near, give_msg); + break; + } + case OBJ_ARMOUR: + unequip_armour(item, near); + break; + + default: + break; + } + + return (true); +} + +void monsters::lose_pickup_energy() +{ + if (const monsterentry* entry = find_monsterentry()) + { + const int delta = speed * entry->energy_usage.pickup_percent / 100; + if (speed_increment > 25 && delta < speed_increment) + speed_increment -= delta; + } +} + +void monsters::pickup_message(const item_def &item, int near) +{ + if (need_message(near)) + { + mprf("%s picks up %s.", + name(DESC_CAP_THE).c_str(), + item.base_type == OBJ_GOLD ? "some gold" + : item.name(DESC_NOCAP_A).c_str()); + } +} + +bool monsters::pickup(item_def &item, int slot, int near, bool force_merge) +{ + ASSERT(is_valid_item(item)); + + const monsters *other_mon = item.holding_monster(); + + if (other_mon != NULL) + { + if (other_mon == this) + { + if (inv[slot] == item.index()) + { + mprf(MSGCH_DIAGNOSTICS, "Monster %s already holding item %s.", + name(DESC_PLAIN, true).c_str(), + item.name(DESC_PLAIN, false, true).c_str()); + return (false); + } + else + { + mprf(MSGCH_DIAGNOSTICS, "Item %s thinks it's already held by " + "monster %s.", + item.name(DESC_PLAIN, false, true).c_str(), + name(DESC_PLAIN, true).c_str()); + } + } + else if (other_mon->type == MONS_NO_MONSTER) + { + mprf(MSGCH_DIAGNOSTICS, "Item %s, held by dead monster, being " + "picked up by monster %s.", + item.name(DESC_PLAIN, false, true).c_str(), + name(DESC_PLAIN, true).c_str()); + } + else + { + mprf(MSGCH_DIAGNOSTICS, "Item %s, held by monster %s, being " + "picked up by monster %s.", + item.name(DESC_PLAIN, false, true).c_str(), + other_mon->name(DESC_PLAIN, true).c_str(), + name(DESC_PLAIN, true).c_str()); + } + } + + // If a monster chooses a two-handed weapon as main weapon, it will + // first have to drop any shield it might wear. + // (Monsters will always favour damage over protection.) + if ((slot == MSLOT_WEAPON || slot == MSLOT_ALT_WEAPON) + && inv[MSLOT_SHIELD] != NON_ITEM + && hands_reqd(item, body_size()) == HANDS_TWO) + { + if (!drop_item(MSLOT_SHIELD, near)) + return (false); + } + + // Similarly, monsters won't pick up shields if they're + // wielding (or alt-wielding) a two-handed weapon. + if (slot == MSLOT_SHIELD) + { + const item_def* wpn = mslot_item(MSLOT_WEAPON); + const item_def* alt = mslot_item(MSLOT_ALT_WEAPON); + if (wpn && hands_reqd(*wpn, body_size()) == HANDS_TWO) + return (false); + if (alt && hands_reqd(*alt, body_size()) == HANDS_TWO) + return (false); + } + + if (inv[slot] != NON_ITEM) + { + item_def &dest(mitm[inv[slot]]); + if (items_stack(item, dest, force_merge)) + { + dungeon_events.fire_position_event( + dgn_event(DET_ITEM_PICKUP, pos(), 0, item.index(), + monster_index(this)), + pos()); + + pickup_message(item, near); + inc_mitm_item_quantity( inv[slot], item.quantity ); + merge_item_stacks(item, dest); + destroy_item(item.index()); + equip(item, slot, near); + lose_pickup_energy(); + return (true); + } + return (false); + } + + dungeon_events.fire_position_event( + dgn_event(DET_ITEM_PICKUP, pos(), 0, item.index(), + monster_index(this)), + pos()); + + const int item_index = item.index(); + unlink_item(item_index); + + inv[slot] = item_index; + + item.set_holding_monster(mindex()); + + pickup_message(item, near); + equip(item, slot, near); + lose_pickup_energy(); + return (true); +} + +bool monsters::drop_item(int eslot, int near) +{ + if (eslot < 0 || eslot >= NUM_MONSTER_SLOTS) + return (false); + + int item_index = inv[eslot]; + if (item_index == NON_ITEM) + return (true); + + item_def* pitem = &mitm[item_index]; + + // Unequip equipped items before dropping them; unequip() prevents + // cursed items from being removed. + bool was_unequipped = false; + if (eslot == MSLOT_WEAPON || eslot == MSLOT_ARMOUR + || eslot == MSLOT_ALT_WEAPON && mons_wields_two_weapons(this)) + { + if (!unequip(*pitem, eslot, near)) + return (false); + was_unequipped = true; + } + + bool on_floor = true; + + if (pitem->flags & ISFLAG_SUMMONED) + { + on_floor = false; + + if (need_message(near)) + mprf("%s %s as %s drops %s!", + pitem->name(DESC_CAP_THE).c_str(), + summoned_poof_msg(this, *pitem).c_str(), + name(DESC_NOCAP_THE).c_str(), + pitem->quantity > 1 ? "them" : "it"); + + item_was_destroyed(*pitem, mindex()); + destroy_item(item_index); + } + else if (!move_item_to_grid(&item_index, pos())) + { + // Re-equip item if we somehow failed to drop it. + if (was_unequipped) + equip(*pitem, eslot, near); + + return (false); + } + + // move_item_to_grid could change item_index, so + // update pitem. + pitem = &mitm[item_index]; + + if (on_floor) + { + if (mons_friendly(this)) + pitem->flags |= ISFLAG_DROPPED_BY_ALLY; + + if (need_message(near)) + { + mprf("%s drops %s.", name(DESC_CAP_THE).c_str(), + pitem->name(DESC_NOCAP_A).c_str()); + } + + dungeon_feature_type feat = grd(pos()); + if (feat_destroys_items(feat)) + { + if ( player_can_hear(pos()) ) + mprf(MSGCH_SOUND, feat_item_destruction_message(feat)); + + item_was_destroyed(*pitem, mindex()); + unlink_item(item_index); + } + } + + inv[eslot] = NON_ITEM; + return (true); +} + +// We don't want monsters to pick up ammunition that cancels out with +// the launcher brand or that is identical to the launcher brand, +// the latter in hope of another monster wandering by who may want to +// use the ammo in question. +static bool _compatible_launcher_ammo_brands(item_def *launcher, + const item_def *ammo) +{ + // If the monster has no ammo then there's no compatibility problems + // to check. + if (ammo == NULL) + return (true); + + const int bow_brand = get_weapon_brand(*launcher); + const int ammo_brand = get_ammo_brand(*ammo); + + switch (ammo_brand) + { + case SPMSL_FLAME: + case SPMSL_FROST: + return (bow_brand != SPWPN_FLAME && bow_brand != SPWPN_FROST); + case SPMSL_CHAOS: + return (bow_brand != SPWPN_CHAOS); + default: + return (true); + } +} + +bool monsters::pickup_launcher(item_def &launch, int near) +{ + // Don't allow monsters to pick up launchers that would also + // refuse to pick up the matching ammo. + if (!_needs_ranged_attack(this)) + return (false); + + // Don't allow monsters to switch to another type of launcher + // as that would require them to also drop their ammunition + // and then try to find ammunition for their new launcher. + // However, they may switch to another launcher if they're + // out of ammo. (jpeg) + const int mdam_rating = mons_weapon_damage_rating(launch); + const missile_type mt = fires_ammo_type(launch); + int eslot = -1; + for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) + { + if (const item_def *elaunch = mslot_item(static_cast(i))) + { + if (!is_range_weapon(*elaunch)) + continue; + + return ((fires_ammo_type(*elaunch) == mt || !missiles()) + && (mons_weapon_damage_rating(*elaunch) < mdam_rating + || mons_weapon_damage_rating(*elaunch) == mdam_rating + && get_weapon_brand(*elaunch) == SPWPN_NORMAL + && get_weapon_brand(launch) != SPWPN_NORMAL + && _compatible_launcher_ammo_brands(&launch, + missiles())) + && drop_item(i, near) && pickup(launch, i, near)); + } + else + eslot = i; + } + + return (eslot == -1 ? false : pickup(launch, eslot, near)); +} + +static bool _is_signature_weapon(monsters *monster, const item_def &weapon) +{ + if (weapon.base_type != OBJ_WEAPONS) + return (false); + + if (monster->type == MONS_DAEVA) + return (weapon.sub_type == WPN_BLESSED_EUDEMON_BLADE); + + // We might allow Sigmund to pick up a better scythe if he finds one... + if (monster->type == MONS_SIGMUND) + return (weapon.sub_type == WPN_SCYTHE); + + if (is_unrandom_artefact(weapon)) + { + switch (weapon.special) + { + case UNRAND_ASMODEUS: + return (monster->type == MONS_ASMODEUS); + case UNRAND_DISPATER: + return (monster->type == MONS_DISPATER); + case UNRAND_CEREBOV: + return (monster->type == MONS_CEREBOV); + } + } + return (false); +} + +static int _ego_damage_bonus(item_def &item) +{ + switch (get_weapon_brand(item)) + { + case SPWPN_NORMAL: return 0; + case SPWPN_PROTECTION: return 1; + default: return 2; + case SPWPN_VORPAL: return 3; + } +} + +static bool _item_race_matches_monster(const item_def &item, monsters *mons) +{ + return (get_equip_race(item) == ISFLAG_ELVEN + && mons_genus(mons->type) == MONS_ELF + || get_equip_race(item) == ISFLAG_ORCISH + && mons_genus(mons->type) == MONS_ORC); +} + +bool monsters::pickup_melee_weapon(item_def &item, int near) +{ + // Throwable weapons may be picked up as though dual-wielding. + const bool dual_wielding = (mons_wields_two_weapons(this) + || is_throwable(this, item)); + if (dual_wielding) + { + // If we have either weapon slot free, pick up the weapon. + if (inv[MSLOT_WEAPON] == NON_ITEM) + return pickup(item, MSLOT_WEAPON, near); + + if (inv[MSLOT_ALT_WEAPON] == NON_ITEM) + return pickup(item, MSLOT_ALT_WEAPON, near); + } + + const int new_wpn_dam = mons_weapon_damage_rating(item) + + _ego_damage_bonus(item); + int eslot = -1; + item_def *weap; + + // Monsters have two weapon slots, one of which can be a ranged, and + // the other a melee weapon. (The exception being dual-wielders who can + // wield two melee weapons). The weapon in MSLOT_WEAPON is the one + // currently wielded (can be empty). + + for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) + { + weap = mslot_item(static_cast(i)); + + if (!weap) + { + // If no weapon in this slot, mark this one. + if (eslot == -1) + eslot = i; + } + else + { + if (is_range_weapon(*weap)) + continue; + + // Don't drop weapons specific to the monster. + if (_is_signature_weapon(this, *weap) && !dual_wielding) + return (false); + + // If we get here, the weapon is a melee weapon. + // If the new weapon is better than the current one and not cursed, + // replace it. Otherwise, give up. + const int old_wpn_dam = mons_weapon_damage_rating(*weap) + + _ego_damage_bonus(*weap); + + bool new_wpn_better = (new_wpn_dam > old_wpn_dam); + if (new_wpn_dam == old_wpn_dam) + { + // Use shopping value as a crude estimate of resistances etc. + // XXX: This is not really logical as many properties don't + // apply to monsters (e.g. levitation, blink, berserk). + // For simplicity, don't apply this check to secondary weapons + // for dual wielding monsters. + int oldval = item_value(*weap, true); + int newval = item_value(item, true); + + // Vastly prefer matching racial type. + if (_item_race_matches_monster(*weap, this)) + oldval *= 2; + if (_item_race_matches_monster(item, this)) + newval *= 2; + + if (newval > oldval) + new_wpn_better = true; + } + + if (new_wpn_better && !weap->cursed()) + { + if (!dual_wielding + || i == MSLOT_WEAPON + || old_wpn_dam + < mons_weapon_damage_rating(*mslot_item(MSLOT_WEAPON)) + + _ego_damage_bonus(*mslot_item(MSLOT_WEAPON))) + { + eslot = i; + if (!dual_wielding) + break; + } + } + else if (!dual_wielding) + { + // We've got a good melee weapon, that's enough. + return (false); + } + } + } + + // No slot found to place this item. + if (eslot == -1) + return (false); + + // Current item cannot be dropped. + if (inv[eslot] != NON_ITEM && !drop_item(eslot, near)) + return (false); + + return (pickup(item, eslot, near)); +} + +// Arbitrary damage adjustment for quantity of missiles. So sue me. +static int _q_adj_damage(int damage, int qty) +{ + return (damage * std::min(qty, 8)); +} + +bool monsters::pickup_throwable_weapon(item_def &item, int near) +{ + const mon_inv_type slot = item_to_mslot(item); + + // If it's a melee weapon then pickup_melee_weapon() already rejected + // it, even though it can also be thrown. + if (slot == MSLOT_WEAPON) + return (false); + + ASSERT(slot == MSLOT_MISSILE); + + // If occupied, don't pick up a throwable weapons if it would just + // stack with an existing one. (Upgrading is possible.) + if (mslot_item(slot) + && (mons_is_wandering(this) || mons_friendly(this) && foe == MHITYOU) + && pickup(item, slot, near, true)) + { + return (true); + } + + item_def *launch = NULL; + const int exist_missile = mons_pick_best_missile(this, &launch, true); + if (exist_missile == NON_ITEM + || (_q_adj_damage(mons_missile_damage(this, launch, + &mitm[exist_missile]), + mitm[exist_missile].quantity) + < _q_adj_damage(mons_thrown_weapon_damage(&item), item.quantity))) + { + if (inv[slot] != NON_ITEM && !drop_item(slot, near)) + return (false); + return pickup(item, slot, near); + } + return (false); +} + +bool monsters::wants_weapon(const item_def &weap) const +{ + if (!could_wield(weap)) + return (false); + + // Blademasters and master archers like their starting weapon and + // don't want another, thank you. + if (type == MONS_DEEP_ELF_BLADEMASTER + || type == MONS_DEEP_ELF_MASTER_ARCHER) + { + return (false); + } + + // Monsters capable of dual-wielding will always prefer two weapons + // to a single two-handed one, however strong. + if (mons_wields_two_weapons(this) + && hands_reqd(weap, body_size()) == HANDS_TWO) + { + return (false); + } + + // Nobody picks up giant clubs. Starting equipment is okay, of course. + if (weap.sub_type == WPN_GIANT_CLUB + || weap.sub_type == WPN_GIANT_SPIKED_CLUB) + { + return (false); + } + + return (true); +} + +bool monsters::wants_armour(const item_def &item) const +{ + // Monsters that are capable of dual wielding won't pick up shields. + // Neither will monsters that are already wielding a two-hander. + if (is_shield(item) + && (mons_wields_two_weapons(this) + || mslot_item(MSLOT_WEAPON) + && hands_reqd(*mslot_item(MSLOT_WEAPON), body_size()) + == HANDS_TWO)) + { + return (false); + } + + // Returns whether this armour is the monster's size. + return (check_armour_size(item, body_size())); +} + +bool monsters::pickup_armour(item_def &item, int near, bool force) +{ + ASSERT(item.base_type == OBJ_ARMOUR); + + if (!force && !wants_armour(item)) + return (false); + + equipment_type eq = EQ_NONE; + + // HACK to allow nagas/centaurs to wear bardings. (jpeg) + switch (item.sub_type) + { + case ARM_NAGA_BARDING: + if (::mons_species(this->type) == MONS_NAGA) + eq = EQ_BODY_ARMOUR; + break; + case ARM_CENTAUR_BARDING: + if (::mons_species(this->type) == MONS_CENTAUR + || ::mons_species(this->type) == MONS_YAKTAUR) + { + eq = EQ_BODY_ARMOUR; + } + break; + // And another hack or two... + case ARM_WIZARD_HAT: + if (this->type == MONS_GASTRONOK) + eq = EQ_BODY_ARMOUR; + break; + case ARM_CLOAK: + if (this->type == MONS_MAURICE) + eq = EQ_BODY_ARMOUR; + break; + default: + eq = get_armour_slot(item); + } + + // Bardings are only wearable by the appropriate monster. + if (eq == EQ_NONE) + return (false); + + // XXX: Monsters can only equip body armour and shields (as of 0.4). + if (!force && eq != EQ_BODY_ARMOUR && eq != EQ_SHIELD) + return (false); + + const mon_inv_type mslot = equip_slot_to_mslot(eq); + if (mslot == NUM_MONSTER_SLOTS) + return (false); + + int newAC = item.armour_rating(); + + // No armour yet -> get this one. + if (!mslot_item(mslot) && newAC > 0) + return pickup(item, mslot, near); + + // Very simplistic armour evaluation (AC comparison). + if (const item_def *existing_armour = slot_item(eq)) + { + if (!force) + { + int oldAC = existing_armour->armour_rating(); + if (oldAC > newAC) + return (false); + + if (oldAC == newAC) + { + // Use shopping value as a crude estimate of resistances etc. + // XXX: This is not really logical as many properties don't + // apply to monsters (e.g. levitation, blink, berserk). + int oldval = item_value(*existing_armour, true); + int newval = item_value(item, true); + + // Vastly prefer matching racial type. + if (_item_race_matches_monster(*existing_armour, this)) + oldval *= 2; + if (_item_race_matches_monster(item, this)) + newval *= 2; + + if (oldval >= newval) + return (false); + } + } + + if (!drop_item(mslot, near)) + return (false); + } + + return pickup(item, mslot, near); +} + +bool monsters::pickup_weapon(item_def &item, int near, bool force) +{ + if (!force && !wants_weapon(item)) + return (false); + + // Weapon pickup involves: + // - If we have no weapons, always pick this up. + // - If this is a melee weapon and we already have a melee weapon, pick + // it up if it is superior to the one we're carrying (and drop the + // one we have). + // - If it is a ranged weapon, and we already have a ranged weapon, + // pick it up if it is better than the one we have. + // - If it is a throwable weapon, and we're carrying no missiles (or our + // missiles are the same type), pick it up. + + if (is_range_weapon(item)) + return (pickup_launcher(item, near)); + + if (pickup_melee_weapon(item, near)) + return (true); + + return (can_use_missile(item) && pickup_throwable_weapon(item, near)); +} + +bool monsters::pickup_missile(item_def &item, int near, bool force) +{ + const item_def *miss = missiles(); + + if (!force) + { + if (item.sub_type == MI_THROWING_NET) + { + // Monster may not pick up trapping net. + if (mons_is_caught(this) && item_is_stationary(item)) + return (false); + } + else // None of these exceptions hold for throwing nets. + { + // Spellcasters should not waste time with ammunition. + // Neither summons nor hostile enchantments are counted for + // this purpose. + if (mons_has_ranged_spell(this, true, false)) + return (false); + + // Monsters in a fight will only pick up missiles if doing so + // is worthwhile. + if (!mons_is_wandering(this) + && (!mons_friendly(this) || foe != MHITYOU) + && (item.quantity < 5 || miss && miss->quantity >= 7)) + { + return (false); + } + } + } + + if (miss && items_stack(*miss, item)) + return (pickup(item, MSLOT_MISSILE, near)); + + if (!force && !can_use_missile(item)) + return (false); + + if (miss) + { + item_def *launch; + for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) + { + launch = mslot_item(static_cast(i)); + if (launch) + { + const int item_brand = get_ammo_brand(item); + // If this ammunition is better, drop the old ones. + // Don't upgrade to ammunition whose brand cancels the + // launcher brand or doesn't improve it further. + if (fires_ammo_type(*launch) == item.sub_type + && (fires_ammo_type(*launch) != miss->sub_type + || item.plus > miss->plus + && get_ammo_brand(*miss) == item_brand + || item.plus >= miss->plus + && get_ammo_brand(*miss) == SPMSL_NORMAL + && item_brand != SPMSL_NORMAL + &&_compatible_launcher_ammo_brands(launch, miss))) + { + if (!drop_item(MSLOT_MISSILE, near)) + return (false); + break; + } + } + } + + // Darts don't absolutely need a launcher - still allow upgrading. + if (item.sub_type == miss->sub_type + && item.sub_type == MI_DART + && (item.plus > miss->plus + || item.plus == miss->plus + && get_ammo_brand(*miss) == SPMSL_NORMAL + && get_ammo_brand(item) != SPMSL_NORMAL)) + { + if (!drop_item(MSLOT_MISSILE, near)) + return (false); + } + } + + return pickup(item, MSLOT_MISSILE, near); +} + +bool monsters::pickup_wand(item_def &item, int near) +{ + // Don't pick up empty wands. + if (item.plus == 0) + return (false); + + // Only low-HD monsters bother with wands. + if (hit_dice >= 14) + return (false); + + // Holy monsters and worshippers of good gods won't pick up evil + // wands. + if ((mons_is_holy(this) || is_good_god(god)) && is_evil_item(item)) + return (false); + + // If a monster already has a charged wand, don't bother. + // Otherwise, replace with a charged one. + if (item_def *wand = mslot_item(MSLOT_WAND)) + { + if (wand->plus > 0) + return (false); + + if (!drop_item(MSLOT_WAND, near)) + return (false); + } + + return (pickup(item, MSLOT_WAND, near)); +} + +bool monsters::pickup_scroll(item_def &item, int near) +{ + if (item.sub_type != SCR_TELEPORTATION + && item.sub_type != SCR_BLINKING + && item.sub_type != SCR_SUMMONING) + { + return (false); + } + + // Holy monsters and worshippers of good gods won't pick up evil + // scrolls. + if ((mons_is_holy(this) || is_good_god(god)) && is_evil_item(item)) + return (false); + + return (pickup(item, MSLOT_SCROLL, near)); +} + +bool monsters::pickup_potion(item_def &item, int near) +{ + // Only allow monsters to pick up potions if they can actually use + // them. + const potion_type ptype = static_cast(item.sub_type); + + if (!this->can_drink_potion(ptype)) + return (false); + + return (pickup(item, MSLOT_POTION, near)); +} + +bool monsters::pickup_gold(item_def &item, int near) +{ + return (pickup(item, MSLOT_GOLD, near)); +} + +bool monsters::pickup_misc(item_def &item, int near) +{ + // Never pick up runes. + if (item.sub_type == MISC_RUNE_OF_ZOT) + return (false); + + // Holy monsters and worshippers of good gods won't pick up evil + // miscellaneous items. + if ((mons_is_holy(this) || is_good_god(god)) && is_evil_item(item)) + return (false); + + return (pickup(item, MSLOT_MISCELLANY, near)); +} + +// Eaten items are handled elsewhere, in _handle_pickup() in monstuff.cc. +bool monsters::pickup_item(item_def &item, int near, bool force) +{ + // Equipping stuff can be forced when initially equipping monsters. + if (!force) + { + // If a monster isn't otherwise occupied (has a foe, is fleeing, etc.) + // it is considered wandering. + bool wandering = (mons_is_wandering(this) + || mons_friendly(this) && foe == MHITYOU); + const int itype = item.base_type; + + // Weak(ened) monsters won't stop to pick up things as long as they + // feel unsafe. + if (!wandering && (hit_points * 10 < max_hit_points || hit_points < 10) + && mon_enemies_around(this)) + { + return (false); + } + + if (mons_friendly(this)) + { + // Never pick up gold or misc. items, it'd only annoy the player. + if (itype == OBJ_MISCELLANY || itype == OBJ_GOLD) + return (false); + + // Depending on the friendly pickup toggle, your allies may not + // pick up anything, or only stuff dropped by (other) allies. + if (you.friendly_pickup == FRIENDLY_PICKUP_NONE + || you.friendly_pickup == FRIENDLY_PICKUP_FRIEND + && !testbits(item.flags, ISFLAG_DROPPED_BY_ALLY) + || you.friendly_pickup == FRIENDLY_PICKUP_PLAYER + && !(item.flags & (ISFLAG_DROPPED | ISFLAG_THROWN + | ISFLAG_DROPPED_BY_ALLY))) + { + return (false); + } + } + + if (!wandering) + { + // These are not important enough for pickup when + // seeking, fleeing etc. + if (itype == OBJ_ARMOUR || itype == OBJ_CORPSES + || itype == OBJ_MISCELLANY || itype == OBJ_GOLD) + { + return (false); + } + + if (itype == OBJ_WEAPONS || itype == OBJ_MISSILES) + { + // Fleeing monsters only pick up emergency equipment. + if (mons_is_fleeing(this)) + return (false); + + // While occupied, hostile monsters won't pick up items + // dropped or thrown by you. (You might have done that to + // distract them.) + if (!mons_friendly(this) + && (testbits(item.flags, ISFLAG_DROPPED) + || testbits(item.flags, ISFLAG_THROWN))) + { + return (false); + } + } + } + } + + switch (item.base_type) + { + // Pickup some stuff only if WANDERING. + case OBJ_ARMOUR: + return pickup_armour(item, near, force); + case OBJ_MISCELLANY: + return pickup_misc(item, near); + case OBJ_GOLD: + return pickup_gold(item, near); + // Fleeing monsters won't pick up these. + // Hostiles won't pick them up if they were ever dropped/thrown by you. + case OBJ_WEAPONS: + return pickup_weapon(item, near, force); + case OBJ_MISSILES: + return pickup_missile(item, near, force); + // Other types can always be picked up + // (barring other checks depending on subtype, of course). + case OBJ_WANDS: + return pickup_wand(item, near); + case OBJ_SCROLLS: + return pickup_scroll(item, near); + case OBJ_POTIONS: + return pickup_potion(item, near); + case OBJ_BOOKS: + if (force) + return pickup_misc(item, near); + // else fall through + default: + return (false); + } +} + +bool monsters::need_message(int &near) const +{ + return (near != -1 ? near + : (near = observable())); +} + +void monsters::swap_weapons(int near) +{ + item_def *weap = mslot_item(MSLOT_WEAPON); + item_def *alt = mslot_item(MSLOT_ALT_WEAPON); + + if (weap && !unequip(*weap, MSLOT_WEAPON, near)) + { + // Item was cursed. + return; + } + + swap_slots(MSLOT_WEAPON, MSLOT_ALT_WEAPON); + + if (alt) + equip(*alt, MSLOT_WEAPON, near); + + // Monsters can swap weapons really fast. :-) + if ((weap || alt) && speed_increment >= 2) + { + if (const monsterentry *entry = find_monsterentry()) + speed_increment -= div_rand_round(entry->energy_usage.attack, 5); + } +} + +void monsters::wield_melee_weapon(int near) +{ + const item_def *weap = mslot_item(MSLOT_WEAPON); + if (!weap || (!weap->cursed() && is_range_weapon(*weap))) + { + const item_def *alt = mslot_item(MSLOT_ALT_WEAPON); + + // Switch to the alternate weapon if it's not a ranged weapon, too, + // or switch away from our main weapon if it's a ranged weapon. + if (alt && !is_range_weapon(*alt) || weap && !alt) + swap_weapons(near); + } +} + +item_def *monsters::slot_item(equipment_type eq) +{ + return (mslot_item(equip_slot_to_mslot(eq))); +} + +item_def *monsters::mslot_item(mon_inv_type mslot) const +{ + const int mi = (mslot == NUM_MONSTER_SLOTS) ? NON_ITEM : inv[mslot]; + return (mi == NON_ITEM ? NULL : &mitm[mi]); +} + +item_def *monsters::shield() +{ + return (mslot_item(MSLOT_SHIELD)); +} + +bool monsters::is_named() const +{ + return (!mname.empty() || mons_is_unique(type)); +} + +bool monsters::has_base_name() const +{ + // Any non-ghost, non-Pandemonium demon that has an explicitly set + // name has a base name. + return (!mname.empty() && !ghost.get()); +} + +static const char *ugly_colour_names[] = { + "red", "brown", "green", "cyan", "purple", "white" +}; + +static std::string _ugly_thing_colour_name(const monsters *mon) +{ + const int colour_offset = ugly_thing_colour_offset(mon); + + if (colour_offset == -1) + return ("buggy"); + + return (ugly_colour_names[colour_offset]); +} + + +static std::string _str_monam(const monsters& mon, description_level_type desc, + bool force_seen) +{ + if (mon.type == MONS_NO_MONSTER) + return ("DEAD MONSTER"); + else if (invalid_monster_type(mon.type) && mon.type != MONS_PROGRAM_BUG) + return make_stringf("INVALID MONSTER (#%d)", mon.type); + + const bool arena_submerged = crawl_state.arena && !force_seen + && mon.submerged(); + + // Handle non-visible case first. + if (!force_seen && !you.can_see(&mon) && !arena_submerged) + { + switch (desc) + { + case DESC_CAP_THE: case DESC_CAP_A: + return ("It"); + case DESC_NOCAP_THE: case DESC_NOCAP_A: case DESC_PLAIN: + return ("it"); + default: + return ("it (buggy)"); + } + } + + // Assumed visible from now on. + + // Various special cases: + // non-gold mimics, dancing weapons, ghosts, Pan demons + if (mons_is_mimic(mon.type) && mon.type != MONS_GOLD_MIMIC) + { + item_def item; + get_mimic_item(&mon, item); + return (item.name(desc)); + } + + if (mon.type == MONS_DANCING_WEAPON && mon.inv[MSLOT_WEAPON] != NON_ITEM) + { + unsigned long ignore_flags = ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES; + bool use_inscrip = true; + + if (desc == DESC_BASENAME || desc == DESC_QUALNAME + || desc == DESC_DBNAME) + { + use_inscrip = false; + } + + const item_def& item = mitm[mon.inv[MSLOT_WEAPON]]; + return (item.name(desc, false, false, use_inscrip, false, + ignore_flags)); + } + + if (desc == DESC_DBNAME) + return (get_monster_data(mon.type)->name); + + if (mon.type == MONS_PLAYER_GHOST) + return (apostrophise(mon.mname) + " ghost"); + + // Some monsters might want the name of a different creature. + monster_type nametype = mon.type; + + // Tack on other prefixes. + switch (mon.type) + { + case MONS_ZOMBIE_SMALL: case MONS_ZOMBIE_LARGE: + case MONS_SKELETON_SMALL: case MONS_SKELETON_LARGE: + case MONS_SIMULACRUM_SMALL: case MONS_SIMULACRUM_LARGE: + case MONS_SPECTRAL_THING: + nametype = mon.base_monster; + break; + + default: + break; + } + + // If the monster has an explicit name, return that, handling it like + // a unique's name. Special handling for named hydras. + if (desc != DESC_BASENAME && !mon.mname.empty() + && mons_genus(nametype) != MONS_HYDRA) + { + return (mon.mname); + } + + std::string result; + + // Start building the name string. + + // Start with the prefix. + // (Uniques don't get this, because their names are proper nouns.) + if (!mons_is_unique(nametype) + && (mon.mname.empty() || mons_genus(nametype) == MONS_HYDRA)) + { + const bool use_your = mons_friendly(&mon); + switch (desc) + { + case DESC_CAP_THE: + result = (use_your ? "Your " : "The "); + break; + case DESC_NOCAP_THE: + result = (use_your ? "your " : "the "); + break; + case DESC_CAP_A: + if (mon.mname.empty()) + result = "A "; + else + result = "The "; + break; + case DESC_NOCAP_A: + if (mon.mname.empty()) + result = "a "; + else + result = "the "; + break; + case DESC_PLAIN: + default: + break; + } + } + + if (arena_submerged) + result += "submerged "; + + // Tack on other prefixes. + switch (mon.type) + { + case MONS_UGLY_THING: + case MONS_VERY_UGLY_THING: + result += _ugly_thing_colour_name(&mon) + " "; + break; + + case MONS_SPECTRAL_THING: + result += "spectral "; + break; + + case MONS_DRACONIAN_CALLER: + case MONS_DRACONIAN_MONK: + case MONS_DRACONIAN_ZEALOT: + case MONS_DRACONIAN_SHIFTER: + case MONS_DRACONIAN_ANNIHILATOR: + case MONS_DRACONIAN_KNIGHT: + case MONS_DRACONIAN_SCORCHER: + if (mon.base_monster != MONS_NO_MONSTER) // database search + result += draconian_colour_name(mon.base_monster) + " "; + break; + + default: + break; + } + + if (mon.mons_species() == MONS_SLIME_CREATURE && desc != DESC_DBNAME) + { + ASSERT(mon.number <= 5); + const char* cardinals[] = {"", "large ", "very large ", + "enormous ", "titanic "}; + result += cardinals[mon.number - 1]; + } + + // Done here to cover cases of undead versions of hydras. + if (mons_species(nametype) == MONS_HYDRA + && mon.number > 0 && desc != DESC_DBNAME) + { + if (nametype == MONS_LERNAEAN_HYDRA) + result += "the "; + + if (mon.number < 11) + { + const char* cardinals[] = {"one", "two", "three", "four", "five", + "six", "seven", "eight", "nine", "ten"}; + result += cardinals[mon.number - 1]; + } + else + result += make_stringf("%d", mon.number); + + result += "-headed "; + } + + if (!mon.mname.empty()) + result += mon.mname; + else if (nametype == MONS_LERNAEAN_HYDRA) + result += "Lernaean hydra"; + else + { + // Add the base name. + if (invalid_monster_type(nametype) && nametype != MONS_PROGRAM_BUG) + result += make_stringf("INVALID MONSTER (#%d)", nametype); + else + result += get_monster_data(nametype)->name; + } + + // Add suffixes. + switch (mon.type) + { + case MONS_ZOMBIE_SMALL: + case MONS_ZOMBIE_LARGE: + result += " zombie"; + break; + case MONS_SKELETON_SMALL: + case MONS_SKELETON_LARGE: + result += " skeleton"; + break; + case MONS_SIMULACRUM_SMALL: + case MONS_SIMULACRUM_LARGE: + result += " simulacrum"; + break; + default: + break; + } + + // Vowel fix: Change 'a orc' to 'an orc'. + if (result.length() >= 3 + && (result[0] == 'a' || result[0] == 'A') + && result[1] == ' ' + && is_vowel(result[2]) + // XXX: Hack + && !starts_with(&result[2], "one-")) + { + result.insert(1, "n"); + } + + if (mons_is_unique(mon.type) && starts_with(result, "the ")) + { + switch (desc) + { + case DESC_CAP_THE: + case DESC_CAP_A: + result = upcase_first(result); + break; + + default: + break; + } + } + + if ((mon.flags & MF_KNOWN_MIMIC) && mons_is_shapeshifter(&mon)) + { + // If momentarily in original form, don't display "shaped + // shifter". + if (mons_genus(mon.type) != MONS_SHAPESHIFTER) + result += " shaped shifter"; + } + + // All done. + return (result); +} + +std::string monsters::name(description_level_type desc, bool force_vis) const +{ + if (desc == DESC_NONE) + return (""); + + const bool possessive = + (desc == DESC_NOCAP_YOUR || desc == DESC_NOCAP_ITS); + + if (possessive) + desc = DESC_NOCAP_THE; + + std::string monnam; + if ((flags & MF_NAME_MASK) && (force_vis || you.can_see(this)) + || crawl_state.arena && mons_class_is_zombified(type)) + { + monnam = full_name(desc); + } + else + monnam = _str_monam(*this, desc, force_vis); + + return (possessive ? apostrophise(monnam) : monnam); +} + +std::string monsters::base_name(description_level_type desc, bool force_vis) + const +{ + if (desc == DESC_NONE) + return (""); + + if (ghost.get() || mons_is_unique(type)) + return (name(desc, force_vis)); + else + { + unwind_var tmname( + const_cast(this)->mname, ""); + return (name(desc, force_vis)); + } +} + +std::string monsters::full_name(description_level_type desc, + bool use_comma) const +{ + if (desc == DESC_NONE) + return (""); + + std::string title = _str_monam(*this, desc, true); + + const unsigned long flag = flags & MF_NAME_MASK; + + const int _type = mons_is_zombified(this) ? base_monster : type; + if (mons_genus(_type) == MONS_HYDRA && flag == 0) + return (title); + + if (has_base_name()) + { + if (flag == MF_NAME_SUFFIX) + { + title = base_name(desc, true); + title += " "; + title += mname; + } + else if (flag == MF_NAME_NO_THE) + { + title += " "; + title += base_name(DESC_PLAIN, true); + } + else if (flag == MF_NAME_REPLACE) + ; + else + { + if (use_comma) + title += ","; + title += " "; + title += base_name(DESC_NOCAP_THE, true); + } + } + return (title); +} + +std::string monsters::pronoun(pronoun_type pro, bool force_visible) const +{ + return (mons_pronoun(static_cast(type), pro, + force_visible || you.can_see(this))); +} + +std::string monsters::conj_verb(const std::string &verb) const +{ + if (!verb.empty() && verb[0] == '!') + return (verb.substr(1)); + + if (verb == "are") + return ("is"); + + if (ends_with(verb, "f") || ends_with(verb, "fe") + || ends_with(verb, "y")) + { + return (verb + "s"); + } + + return (pluralise(verb)); +} + +std::string monsters::hand_name(bool plural, bool *can_plural) const +{ + bool _can_plural; + if (can_plural == NULL) + can_plural = &_can_plural; + *can_plural = true; + + std::string str; + char ch = mons_char(type); + + const bool rand = (type == MONS_CHAOS_SPAWN); + + switch (get_mon_shape(this)) + { + case MON_SHAPE_CENTAUR: + case MON_SHAPE_NAGA: + // Defaults to "hand" + break; + case MON_SHAPE_HUMANOID: + case MON_SHAPE_HUMANOID_WINGED: + case MON_SHAPE_HUMANOID_TAILED: + case MON_SHAPE_HUMANOID_WINGED_TAILED: + if (ch == 'T' || ch == 'd' || ch == 'n' || mons_is_demon(type)) + str = "claw"; + break; + + case MON_SHAPE_QUADRUPED: + case MON_SHAPE_QUADRUPED_TAILLESS: + case MON_SHAPE_QUADRUPED_WINGED: + case MON_SHAPE_ARACHNID: + if (type == MONS_SCORPION || rand && one_chance_in(4)) + str = "pincer"; + else + { + str = "front "; + return (str + foot_name(plural, can_plural)); + } + break; + + case MON_SHAPE_BLOB: + case MON_SHAPE_SNAKE: + case MON_SHAPE_FISH: + return foot_name(plural, can_plural); + + case MON_SHAPE_BAT: + str = "wing"; + break; + + case MON_SHAPE_INSECT: + case MON_SHAPE_INSECT_WINGED: + case MON_SHAPE_CENTIPEDE: + str = "antenna"; + break; + + case MON_SHAPE_SNAIL: + str = "eye-stalk"; + break; + + case MON_SHAPE_PLANT: + str = "leaf"; + break; + + case MON_SHAPE_MISC: + if (ch == 'x' || ch == 'X' || rand) + { + str = "tentacle"; + break; + } + // Deliberate fallthrough. + case MON_SHAPE_FUNGUS: + str = "body"; + *can_plural = false; + break; + + case MON_SHAPE_ORB: + switch (type) + { + case MONS_GIANT_SPORE: + str = "rhizome"; + break; + + case MONS_GIANT_EYEBALL: + case MONS_EYE_OF_DRAINING: + case MONS_SHINING_EYE: + case MONS_EYE_OF_DEVASTATION: + *can_plural = false; + // Deliberate fallthrough. + case MONS_GREAT_ORB_OF_EYES: + str = "pupil"; + break; + + case MONS_GIANT_ORANGE_BRAIN: + default: + if (rand) + str = "rhizome"; + else + { + str = "body"; + *can_plural = false; + } + break; + } + } + + if (str.empty()) + { + // Reduce the chance of a random-shaped monster having hands. + if (rand && coinflip()) + return (hand_name(plural, can_plural)); + + str = "hand"; + } + + if (plural && *can_plural) + str = pluralise(str); + + return (str); +} + +std::string monsters::foot_name(bool plural, bool *can_plural) const +{ + bool _can_plural; + if (can_plural == NULL) + can_plural = &_can_plural; + *can_plural = true; + + std::string str; + char ch = mons_char(type); + + const bool rand = (type == MONS_CHAOS_SPAWN); + + switch (get_mon_shape(this)) + { + case MON_SHAPE_INSECT: + case MON_SHAPE_INSECT_WINGED: + case MON_SHAPE_ARACHNID: + case MON_SHAPE_CENTIPEDE: + str = "leg"; + break; + + case MON_SHAPE_HUMANOID: + case MON_SHAPE_HUMANOID_WINGED: + case MON_SHAPE_HUMANOID_TAILED: + case MON_SHAPE_HUMANOID_WINGED_TAILED: + if (type == MONS_MINOTAUR) + str = "hoof"; + else if (swimming() + && (type == MONS_MERFOLK || mons_genus(type) == MONS_MERMAID)) + { + str = "tail"; + *can_plural = false; + } + break; + + case MON_SHAPE_CENTAUR: + str = "hoof"; + break; + + case MON_SHAPE_QUADRUPED: + case MON_SHAPE_QUADRUPED_TAILLESS: + case MON_SHAPE_QUADRUPED_WINGED: + if (rand) + { + const char* feet[] = {"paw", "talon", "hoof"}; + str = RANDOM_ELEMENT(feet); + } + else if (ch == 'h') + str = "paw"; + else if (ch == 'l' || ch == 'D') + str = "talon"; + else if (type == MONS_YAK || type == MONS_DEATH_YAK) + str = "hoof"; + else if (ch == 'H') + { + if (type == MONS_MANTICORE || type == MONS_SPHINX) + str = "paw"; + else + str = "talon"; + } + break; + + case MON_SHAPE_BAT: + str = "claw"; + break; + + case MON_SHAPE_SNAKE: + case MON_SHAPE_FISH: + str = "tail"; + *can_plural = false; + break; + + case MON_SHAPE_PLANT: + str = "root"; + break; + + case MON_SHAPE_FUNGUS: + str = "stem"; + *can_plural = false; + break; + + case MON_SHAPE_BLOB: + str = "pseudopod"; + break; + + case MON_SHAPE_MISC: + if (ch == 'x' || ch == 'X' || rand) + { + str = "tentacle"; + break; + } + // Deliberate fallthrough. + case MON_SHAPE_SNAIL: + case MON_SHAPE_NAGA: + case MON_SHAPE_ORB: + str = "underside"; + *can_plural = false; + break; + } + + if (str.empty()) + { + // Reduce the chance of a random-shaped monster having feet. + if (rand && coinflip()) + return (foot_name(plural, can_plural)); + + return (plural ? "feet" : "foot"); + } + + if (plural && *can_plural) + str = pluralise(str); + + return (str); +} + +std::string monsters::arm_name(bool plural, bool *can_plural) const +{ + mon_body_shape shape = get_mon_shape(this); + + if (shape > MON_SHAPE_NAGA) + return hand_name(plural, can_plural); + + if (can_plural != NULL) + *can_plural = true; + + std::string str; + switch (mons_genus(type)) + { + case MONS_NAGA: + case MONS_DRACONIAN: str = "scaled arm"; break; + + case MONS_MUMMY: str = "bandaged wrapped arm"; break; + + case MONS_SKELETAL_WARRIOR: + case MONS_LICH: str = "bony arm"; break; + + default: str = "arm"; break; + } + + if (plural) + str = pluralise(str); + + return (str); +} + +monster_type monsters::id() const +{ + return (type); +} + +int monsters::mindex() const +{ + return (monster_index(this)); +} + +int monsters::get_experience_level() const +{ + return (hit_dice); +} + +void monsters::moveto(const coord_def& c) +{ + if (c != pos() && in_bounds(pos())) + mons_clear_trapping_net(this); + + position = c; +} + +bool monsters::fumbles_attack(bool verbose) +{ + if (floundering() && one_chance_in(4)) + { + if (verbose) + { + if (you.can_see(this)) + { + mprf("%s splashes around in the water.", + this->name(DESC_CAP_THE).c_str()); + } + else if (player_can_hear(pos())) + mpr("You hear a splashing noise.", MSGCH_SOUND); + } + + return (true); + } + + if (submerged()) + return (true); + + return (false); +} + +bool monsters::cannot_fight() const +{ + return (mons_class_flag(type, M_NO_EXP_GAIN) + || mons_is_statue(type)); +} + +void monsters::attacking(actor * /* other */) +{ +} + +void monsters::go_berserk(bool /* intentional */) +{ + if (!this->can_go_berserk()) + return; + + if (has_ench(ENCH_SLOW)) + { + del_ench(ENCH_SLOW, true); // Give no additional message. + simple_monster_message( + this, + make_stringf(" shakes off %s lethargy.", + pronoun(PRONOUN_NOCAP_POSSESSIVE).c_str()).c_str()); + } + del_ench(ENCH_HASTE, true); + del_ench(ENCH_FATIGUE, true); // Give no additional message. + + const int duration = 16 + random2avg(13, 2); + add_ench(mon_enchant(ENCH_BERSERK, 0, KC_OTHER, duration * 10)); + add_ench(mon_enchant(ENCH_HASTE, 0, KC_OTHER, duration * 10)); + add_ench(mon_enchant(ENCH_MIGHT, 0, KC_OTHER, duration * 10)); + if (simple_monster_message( this, " goes berserk!" )) + // Xom likes monsters going berserk. + xom_is_stimulated(mons_friendly(this) ? 32 : 128); +} + +void monsters::expose_to_element(beam_type flavour, int strength) +{ + switch (flavour) + { + case BEAM_COLD: + if (mons_class_flag(this->type, M_COLD_BLOOD) && this->res_cold() <= 0 && coinflip()) + slow_down(this, strength); + break; + default: + break; + } +} + +void monsters::banish(const std::string &) +{ + monster_die(this, KILL_RESET, NON_MONSTER); +} + +bool monsters::has_spell(spell_type spell) const +{ + for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) + if (spells[i] == spell) + return (true); + + return (false); +} + +bool monsters::has_attack_flavour(int flavour) const +{ + for (int i = 0; i < 4; ++i) + { + const int attk_flavour = mons_attack_spec(this, i).flavour; + if (attk_flavour == flavour) + return (true); + } + + return (false); +} + +bool monsters::has_damage_type(int dam_type) +{ + for (int i = 0; i < 4; ++i) + { + const int dmg_type = damage_type(i); + if (dmg_type == dam_type) + return (true); + } + + return (false); +} + +bool monsters::confused() const +{ + return (mons_is_confused(this)); +} + +bool monsters::confused_by_you() const +{ + if (mons_class_flag(type, M_CONFUSED)) + return false; + + const mon_enchant me = get_ench(ENCH_CONFUSION); + return (me.ench == ENCH_CONFUSION && me.who == KC_YOU); +} + +bool monsters::paralysed() const +{ + return (mons_is_paralysed(this)); +} + +bool monsters::cannot_act() const +{ + return (mons_cannot_act(this)); +} + +bool monsters::cannot_move() const +{ + return (mons_cannot_move(this)); +} + +bool monsters::asleep() const +{ + return (behaviour == BEH_SLEEP); +} + +bool monsters::backlit(bool check_haloed) const +{ + return (has_ench(ENCH_BACKLIGHT) + || ((check_haloed) ? haloed() : false)); +} + +bool monsters::haloed() const +{ + return (inside_halo(pos())); +} + +bool monsters::caught() const +{ + return (mons_is_caught(this)); +} + +int monsters::shield_bonus() const +{ + const item_def *shld = const_cast(this)->shield(); + if (shld) + { + // Note that 0 is not quite no-blocking. + if (incapacitated()) + return (0); + + int shld_c = property(*shld, PARM_AC) + shld->plus; + return (random2avg(shld_c + hit_dice * 2 / 3, 2)); + } + return (-100); +} + +int monsters::shield_block_penalty() const +{ + return (4 * shield_blocks * shield_blocks); +} + +void monsters::shield_block_succeeded() +{ + ++shield_blocks; +} + +int monsters::shield_bypass_ability(int) const +{ + return (15 + hit_dice * 2 / 3); +} + +int monsters::armour_class() const +{ + return (ac); +} + +int monsters::melee_evasion(const actor *act) const +{ + int evasion = ev; + if (paralysed() || asleep()) + evasion = 0; + else if (caught()) + evasion /= (body_size(PSIZE_BODY) + 2); + else if (confused()) + evasion /= 2; + return (evasion); +} + +void monsters::heal(int amount, bool max_too) +{ + hit_points += amount; + if (max_too) + max_hit_points += amount; + if (hit_points > max_hit_points) + hit_points = max_hit_points; +} + +mon_holy_type monsters::holiness() const +{ + if (testbits(flags, MF_HONORARY_UNDEAD)) + return (MH_UNDEAD); + + return (mons_class_holiness(type)); +} + +bool monsters::is_unholy() const +{ + const mon_holy_type holi = holiness(); + + return (holi == MH_UNDEAD || holi == MH_DEMONIC); +} + +int monsters::res_fire() const +{ + const mon_resist_def res = get_mons_resists(this); + + int u = std::min(res.fire + res.hellfire * 3, 3); + + if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) + { + u += scan_mon_inv_randarts(this, ARTP_FIRE); + + const int armour = inv[MSLOT_ARMOUR]; + const int shld = inv[MSLOT_SHIELD]; + + if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR) + { + // intrinsic armour abilities + switch (mitm[armour].sub_type) + { + case ARM_DRAGON_ARMOUR: u += 2; break; + case ARM_GOLD_DRAGON_ARMOUR: u += 1; break; + case ARM_ICE_DRAGON_ARMOUR: u -= 1; break; + default: break; + } + + // check ego resistance + const int ego = get_armour_ego_type(mitm[armour]); + if (ego == SPARM_FIRE_RESISTANCE || ego == SPARM_RESISTANCE) + u += 1; + } + + if (shld != NON_ITEM && mitm[shld].base_type == OBJ_ARMOUR) + { + // check ego resistance + const int ego = get_armour_ego_type(mitm[shld]); + if (ego == SPARM_FIRE_RESISTANCE || ego == SPARM_RESISTANCE) + u += 1; + } + } + + if (u < -3) + u = -3; + else if (u > 3) + u = 3; + + return (u); +} + +int monsters::res_steam() const +{ + int res = get_mons_resists(this).steam; + if (has_equipped(EQ_BODY_ARMOUR, ARM_STEAM_DRAGON_ARMOUR)) + res += 3; + return (res + res_fire() / 2); +} + +int monsters::res_cold() const +{ + int u = get_mons_resists(this).cold; + + if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) + { + u += scan_mon_inv_randarts(this, ARTP_COLD); + + const int armour = inv[MSLOT_ARMOUR]; + const int shld = inv[MSLOT_SHIELD]; + + if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR) + { + // intrinsic armour abilities + switch (mitm[armour].sub_type) + { + case ARM_ICE_DRAGON_ARMOUR: u += 2; break; + case ARM_GOLD_DRAGON_ARMOUR: u += 1; break; + case ARM_DRAGON_ARMOUR: u -= 1; break; + default: break; + } + + // check ego resistance + const int ego = get_armour_ego_type(mitm[armour]); + if (ego == SPARM_COLD_RESISTANCE || ego == SPARM_RESISTANCE) + u += 1; + } + + if (shld != NON_ITEM && mitm[shld].base_type == OBJ_ARMOUR) + { + // check ego resistance + const int ego = get_armour_ego_type(mitm[shld]); + if (ego == SPARM_COLD_RESISTANCE || ego == SPARM_RESISTANCE) + u += 1; + } + } + + if (u < -3) + u = -3; + else if (u > 3) + u = 3; + + return (u); +} + +int monsters::res_elec() const +{ + // This is a variable, not a player_xx() function, so can be above 1. + int u = 0; + + u += get_mons_resists(this).elec; + + // Don't bother checking equipment if the monster can't use it. + if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) + { + u += scan_mon_inv_randarts(this, ARTP_ELECTRICITY); + + // No ego armour, but storm dragon. + const int armour = inv[MSLOT_ARMOUR]; + if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR + && mitm[armour].sub_type == ARM_STORM_DRAGON_ARMOUR) + { + u += 1; + } + } + + // Monsters can legitimately get multiple levels of electricity resistance. + + return (u); +} + +int monsters::res_asphyx() const +{ + int res = get_mons_resists(this).asphyx; + const mon_holy_type holi = holiness(); + if (is_unholy() + || holi == MH_NONLIVING + || holi == MH_PLANT) + { + res += 1; + } + return (res); +} + +int monsters::res_poison() const +{ + int u = get_mons_resists(this).poison; + + if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) + { + u += scan_mon_inv_randarts( this, ARTP_POISON ); + + const int armour = this->inv[MSLOT_ARMOUR]; + const int shld = this->inv[MSLOT_SHIELD]; + + if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR) + { + // intrinsic armour abilities + switch (mitm[armour].sub_type) + { + case ARM_SWAMP_DRAGON_ARMOUR: u += 1; break; + case ARM_GOLD_DRAGON_ARMOUR: u += 1; break; + default: break; + } + + // ego armour resistance + if (get_armour_ego_type(mitm[armour]) == SPARM_POISON_RESISTANCE) + u += 1; + } + + if (shld != NON_ITEM && mitm[shld].base_type == OBJ_ARMOUR) + { + // ego armour resistance + if (get_armour_ego_type(mitm[shld]) == SPARM_POISON_RESISTANCE) + u += 1; + } + } + + // Monsters can legitimately get multiple levels of poison resistance. + + return (u); +} + +int monsters::res_sticky_flame() const +{ + int res = get_mons_resists(this).sticky_flame; + if (has_equipped(EQ_BODY_ARMOUR, ARM_MOTTLED_DRAGON_ARMOUR)) + res += 1; + return (res); +} + +int monsters::res_rotting() const +{ + int res = get_mons_resists(this).rotting; + if (holiness() != MH_NATURAL) + res += 1; + return (res); +} + +int monsters::res_holy_energy(const actor *attacker) const +{ + if (mons_is_evil(this)) + return (-1); + + if (is_unholy()) + return (-2); + + if (is_good_god(god) + || mons_is_holy(this) + || mons_neutral(this) + || is_unchivalric_attack(attacker, this) + || is_good_god(you.religion) && is_follower(this)) + { + return (1); + } + + return (0); +} + +int monsters::res_negative_energy() const +{ + if (holiness() != MH_NATURAL + || type == MONS_SHADOW_DRAGON) + { + return (3); + } + + int u = 0; + + if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) + { + u += scan_mon_inv_randarts(this, ARTP_NEGATIVE_ENERGY); + + const int armour = this->inv[MSLOT_ARMOUR]; + const int shld = this->inv[MSLOT_SHIELD]; + + if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR) + { + // check for ego resistance + if (get_armour_ego_type(mitm[armour]) == SPARM_POSITIVE_ENERGY) + u += 1; + } + + if (shld != NON_ITEM && mitm[shld].base_type == OBJ_ARMOUR) + { + // check for ego resistance + if (get_armour_ego_type(mitm[shld]) == SPARM_POSITIVE_ENERGY) + u += 1; + } + } + + if (u > 3) + u = 3; + + return (u); +} + +int monsters::res_torment() const +{ + const mon_holy_type holy = holiness(); + if (holy == MH_UNDEAD + || holy == MH_DEMONIC + || holy == MH_NONLIVING) + { + return (1); + } + + return (0); +} + +int monsters::res_acid() const +{ + return (get_mons_resists(this).acid); +} + +flight_type monsters::flight_mode() const +{ + return (mons_flies(this)); +} + +bool monsters::is_levitating() const +{ + // Checking class flags is not enough - see mons_flies. + return (flight_mode() == FL_LEVITATE); +} + +int monsters::mons_species() const +{ + return ::mons_species(type); +} + +void monsters::poison(actor *agent, int amount) +{ + if (amount <= 0) + return; + + // Scale poison down for monsters. + if (!(amount /= 2)) + amount = 1; + + poison_monster(this, agent ? agent->kill_alignment() : KC_OTHER, amount); +} + +int monsters::skill(skill_type sk, bool) const +{ + switch (sk) + { + case SK_NECROMANCY: + return (holiness() == MH_UNDEAD ? hit_dice / 2 : hit_dice / 3); + + default: + return (0); + } +} + +void monsters::blink(bool) +{ + monster_blink(this); +} + +void monsters::teleport(bool now, bool) +{ + monster_teleport(this, now, false); +} + +bool monsters::alive() const +{ + return (hit_points > 0 && type != MONS_NO_MONSTER); +} + +god_type monsters::deity() const +{ + return (god); +} + +bool monsters::drain_exp(actor *agent, bool quiet, int pow) +{ + if (x_chance_in_y(res_negative_energy(), 3)) + return (false); + + if (!quiet && you.can_see(this)) + mprf("%s is drained!", name(DESC_CAP_THE).c_str()); + + // If quiet, don't clean up the monster in order to credit properly. + hurt(agent, 2 + random2(pow), BEAM_NEG, !quiet); + + if (alive()) + { + if (x_chance_in_y(pow, 15)) + { + hit_dice--; + experience = 0; + } + + max_hit_points -= 2 + random2(pow); + hit_points = std::min(max_hit_points, hit_points); + } + + return (true); +} + +bool monsters::rot(actor *agent, int amount, int immediate, bool quiet) +{ + if (res_rotting() > 0 || amount <= 0) + return (false); + + if (!quiet && you.can_see(this)) + { + mprf("%s %s!", name(DESC_CAP_THE).c_str(), + amount > 0 ? "rots" : "looks less resilient"); + } + + // Apply immediate damage because we can't handle rotting for + // monsters yet. + if (immediate > 0) + { + // If quiet, don't clean up the monster in order to credit + // properly. + hurt(agent, immediate, BEAM_MISSILE, !quiet); + + if (alive()) + { + max_hit_points -= immediate * 2; + hit_points = std::min(max_hit_points, hit_points); + } + } + + add_ench(mon_enchant(ENCH_ROT, std::min(amount, 4), + agent->kill_alignment())); + + return (true); +} + +int monsters::hurt(const actor *agent, int amount, beam_type flavour, + bool cleanup_dead) +{ + if (hit_points > 0 && type != -1) + { + if (amount == INSTANT_DEATH) + amount = hit_points; + else if (hit_dice <= 0) + amount = hit_points; + else if (amount <= 0 && hit_points <= max_hit_points) + return (0); + + amount = std::min(amount, hit_points); + hit_points -= amount; + + if (hit_points > max_hit_points) + { + amount += hit_points - max_hit_points; + hit_points = max_hit_points; + } + + // Allow the victim to exhibit passive damage behaviour (royal + // jelly). + kill_category whose = (agent == NULL) ? KC_OTHER : + (agent->atype() == ACT_PLAYER) ? KC_YOU : + mons_friendly_real((monsters*)agent) ? KC_FRIENDLY : + KC_OTHER; + react_to_damage(amount, flavour, whose); + } + + if (cleanup_dead && (hit_points <= 0 || hit_dice <= 0) && type != -1) + { + if (agent == NULL) + monster_die(this, KILL_MISC, NON_MONSTER); + else if (agent->atype() == ACT_PLAYER) + monster_die(this, KILL_YOU, NON_MONSTER); + else + monster_die(this, KILL_MON, agent->mindex()); + } + + return (amount); +} + +void monsters::confuse(actor *atk, int strength) +{ + enchant_monster_with_flavour(this, atk, BEAM_CONFUSION, strength); +} + +void monsters::paralyse(actor *atk, int strength) +{ + enchant_monster_with_flavour(this, atk, BEAM_PARALYSIS, strength); +} + +void monsters::petrify(actor *atk, int strength) +{ + if (mons_is_insubstantial(type)) + return; + + enchant_monster_with_flavour(this, atk, BEAM_PETRIFY, strength); +} + +void monsters::slow_down(actor *atk, int strength) +{ + enchant_monster_with_flavour(this, atk, BEAM_SLOW, strength); +} + +void monsters::set_ghost(const ghost_demon &g, bool has_name) +{ + ghost.reset(new ghost_demon(g)); + + if (has_name) + mname = ghost->name; +} + +void monsters::pandemon_init() +{ + hit_dice = ghost->xl; + max_hit_points = ghost->max_hp; + hit_points = max_hit_points; + ac = ghost->ac; + ev = ghost->ev; + flags = MF_INTERESTING; + // Don't make greased-lightning Pandemonium demons in the dungeon + // max speed = 17). Demons in Pandemonium can be up to speed 24. + if (you.level_type == LEVEL_DUNGEON) + speed = (one_chance_in(3) ? 10 : 7 + roll_dice(2, 5)); + else + speed = (one_chance_in(3) ? 10 : 10 + roll_dice(2, 7)); + + speed_increment = 70; + + if (you.char_direction == GDT_ASCENDING && you.level_type == LEVEL_DUNGEON) + colour = LIGHTRED; + else + colour = ghost->colour; + + load_spells(MST_GHOST); +} + +void monsters::ghost_init() +{ + type = MONS_PLAYER_GHOST; + god = ghost->religion; + hit_dice = ghost->xl; + max_hit_points = ghost->max_hp; + hit_points = max_hit_points; + ac = ghost->ac; + ev = ghost->ev; + speed = ghost->speed; + speed_increment = 70; + attitude = ATT_HOSTILE; + behaviour = BEH_WANDER; + flags = MF_INTERESTING; + foe = MHITNOT; + foe_memory = 0; + colour = ghost->colour; + number = MONS_NO_MONSTER; + load_spells(MST_GHOST); + + inv.init(NON_ITEM); + enchantments.clear(); + ench_countdown = 0; + + find_place_to_live(); +} + +void monsters::uglything_init(bool only_mutate) +{ + // If we're mutating an ugly thing, leave its experience level, hit + // dice and maximum hit points as they are. + if (!only_mutate) + { + hit_dice = ghost->xl; + max_hit_points = ghost->max_hp; + } + + hit_points = max_hit_points; + ac = ghost->ac; + ev = ghost->ev; + speed = ghost->speed; + speed_increment = 70; + colour = ghost->colour; +} + +void monsters::uglything_mutate(unsigned char force_colour) +{ + ghost->init_ugly_thing(type == MONS_VERY_UGLY_THING, true, force_colour); + uglything_init(true); +} + +void monsters::uglything_upgrade() +{ + ghost->ugly_thing_to_very_ugly_thing(); + uglything_init(); +} + +bool monsters::check_set_valid_home(const coord_def &place, + coord_def &chosen, + int &nvalid) const +{ + if (!in_bounds(place)) + return (false); + + if (actor_at(place)) + return (false); + + if (!monster_habitable_grid(this, grd(place))) + return (false); + + if (one_chance_in(++nvalid)) + chosen = place; + + return (true); +} + +bool monsters::find_home_around(const coord_def &c, int radius) +{ + coord_def place(-1, -1); + int nvalid = 0; + for (int yi = -radius; yi <= radius; ++yi) + { + const coord_def c1(c.x - radius, c.y + yi); + const coord_def c2(c.x + radius, c.y + yi); + check_set_valid_home(c1, place, nvalid); + check_set_valid_home(c2, place, nvalid); + } + + for (int xi = -radius + 1; xi < radius; ++xi) + { + const coord_def c1(c.x + xi, c.y - radius); + const coord_def c2(c.x + xi, c.y + radius); + check_set_valid_home(c1, place, nvalid); + check_set_valid_home(c2, place, nvalid); + } + + if (nvalid) + { + moveto(place); + return (true); + } + + return (false); +} + +bool monsters::find_home_near_place(const coord_def &c) +{ + for (int radius = 1; radius < 7; ++radius) + if (find_home_around(c, radius)) + return (true); + + return (false); +} + +bool monsters::find_home_near_player() +{ + return (find_home_near_place(you.pos())); +} + +bool monsters::find_home_anywhere() +{ + coord_def place(-1, -1); + int nvalid = 0; + for (int tries = 0; tries < 600; ++tries) + { + if (check_set_valid_home(random_in_bounds(), place, nvalid)) + { + moveto(place); + return (true); + } + } + + return (false); +} + +bool monsters::find_place_to_live(bool near_player) +{ + if (near_player && find_home_near_player() + || find_home_anywhere()) + { + mgrd(pos()) = mindex(); + return (true); + } + + return (false); +} + +void monsters::destroy_inventory() +{ + for (int j = 0; j < NUM_MONSTER_SLOTS; j++) + { + if (inv[j] != NON_ITEM) + { + destroy_item( inv[j] ); + inv[j] = NON_ITEM; + } + } +} + +bool monsters::is_travelling() const +{ + return (!travel_path.empty()); +} + +bool monsters::is_patrolling() const +{ + return (!patrol_point.origin()); +} + +bool monsters::needs_transit() const +{ + return ((mons_is_unique(type) + || (flags & MF_BANISHED) + || you.level_type == LEVEL_DUNGEON + && hit_dice > 8 + random2(25) + && mons_can_use_stairs(this)) + && !mons_is_summoned(this)); +} + +void monsters::set_transit(const level_id &dest) +{ + add_monster_to_transit(dest, *this); +} + +void monsters::load_spells(mon_spellbook_type book) +{ + spells.init(SPELL_NO_SPELL); + if (book == MST_NO_SPELLS || book == MST_GHOST && !ghost.get()) + return; + +#if DEBUG_DIAGNOSTICS + mprf( MSGCH_DIAGNOSTICS, "%s: loading spellbook #%d", + name(DESC_PLAIN).c_str(), static_cast(book) ); +#endif + + if (book == MST_GHOST) + spells = ghost->spells; + else + { + for (unsigned int i = 0; i < ARRAYSZ(mspell_list); ++i) + { + if (mspell_list[i].type == book) + { + for (int j = 0; j < NUM_MONSTER_SPELL_SLOTS; ++j) + spells[j] = mspell_list[i].spells[j]; + break; + } + } + } +#if DEBUG_DIAGNOSTICS + // Only for ghosts, too spammy to use for all monsters. + if (book == MST_GHOST) + { + for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; i++) + { + mprf( MSGCH_DIAGNOSTICS, "Spell #%d: %d (%s)", + i, spells[i], spell_title(spells[i]) ); + } + } +#endif +} + +bool monsters::has_hydra_multi_attack() const +{ + return (mons_species() == MONS_HYDRA + || mons_is_zombified(this) && base_monster == MONS_HYDRA); +} + +bool monsters::has_multitargeting() const +{ + if (mons_class_wields_two_weapons(type)) + return (true); + + // Hacky little list for now. evk + return (type == MONS_HYDRA + || type == MONS_TENTACLED_MONSTROSITY + || type == MONS_ELECTRIC_GOLEM); +} + +bool monsters::has_ench(enchant_type ench) const +{ + return (enchantments.find(ench) != enchantments.end()); +} + +bool monsters::has_ench(enchant_type ench, enchant_type ench2) const +{ + if (ench2 == ENCH_NONE) + ench2 = ench; + + for (int i = ench; i <= ench2; ++i) + if (has_ench(static_cast(i))) + return (true); + + return (false); +} + +mon_enchant monsters::get_ench(enchant_type ench1, + enchant_type ench2) const +{ + if (ench2 == ENCH_NONE) + ench2 = ench1; + + for (int e = ench1; e <= ench2; ++e) + { + mon_enchant_list::const_iterator i = + enchantments.find(static_cast(e)); + + if (i != enchantments.end()) + return (i->second); + } + + return mon_enchant(); +} + +void monsters::update_ench(const mon_enchant &ench) +{ + if (ench.ench != ENCH_NONE) + { + mon_enchant_list::iterator i = enchantments.find(ench.ench); + if (i != enchantments.end()) + i->second = ench; + } +} + +bool monsters::add_ench(const mon_enchant &ench) +{ + // silliness + if (ench.ench == ENCH_NONE) + return (false); + + if (ench.ench == ENCH_FEAR + && (holiness() == MH_NONLIVING || has_ench(ENCH_BERSERK))) + { + return (false); + } + + mon_enchant_list::iterator i = enchantments.find(ench.ench); + bool new_enchantment = false; + mon_enchant *added = NULL; + if (i == enchantments.end()) + { + new_enchantment = true; + added = &(enchantments[ench.ench] = ench); + } + else + { + i->second += ench; + added = &i->second; + } + + // If the duration is not set, we must calculate it (depending on the + // enchantment). + if (!ench.duration) + added->set_duration(this, new_enchantment ? NULL : &ench); + + if (new_enchantment) + add_enchantment_effect(ench); + + return (true); +} + +void monsters::forget_random_spell() +{ + int which_spell = -1; + int count = 0; + for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) + if (spells[i] != SPELL_NO_SPELL && one_chance_in(++count)) + which_spell = i; + if (which_spell != -1) + spells[which_spell] = SPELL_NO_SPELL; +} + +void monsters::add_enchantment_effect(const mon_enchant &ench, bool quiet) +{ + // Check for slow/haste. + switch (ench.ench) + { + case ENCH_BERSERK: + // Inflate hp. + scale_hp(3, 2); + + if (has_ench(ENCH_SUBMERGED)) + del_ench(ENCH_SUBMERGED); + + if (mons_is_lurking(this)) + { + behaviour = BEH_WANDER; + behaviour_event(this, ME_EVAL); + } + break; + + case ENCH_HASTE: + if (speed >= 100) + speed = 100 + ((speed - 100) * 2); + else + speed *= 2; + + break; + + case ENCH_SLOW: + if (speed >= 100) + speed = 100 + ((speed - 100) / 2); + else + speed /= 2; + + break; + + case ENCH_SUBMERGED: + mons_clear_trapping_net(this); + + // Don't worry about invisibility. You should be able to see if + // something has submerged. + if (!quiet && mons_near(this)) + { + if (type == MONS_AIR_ELEMENTAL) + { + mprf("%s merges itself into the air.", + name(DESC_CAP_A, true).c_str()); + } + else if (type == MONS_TRAPDOOR_SPIDER) + { + mprf("%s hides itself under the floor.", + name(DESC_CAP_A, true).c_str()); + } + else if (seen_context == "surfaces" + || seen_context == "bursts forth" + || seen_context == "emerges") + { + // The monster surfaced and submerged in the same turn + // without doing anything else. + interrupt_activity(AI_SEE_MONSTER, + activity_interrupt_data(this, + "surfaced")); + } + else if (crawl_state.arena) + mprf("%s submerges.", name(DESC_CAP_A, true).c_str()); + } + + // Pacified monsters leave the level when they submerge. + if (mons_is_pacified(this)) + make_mons_leave_level(this); + break; + + case ENCH_CONFUSION: + if (type == MONS_TRAPDOOR_SPIDER && has_ench(ENCH_SUBMERGED)) + del_ench(ENCH_SUBMERGED); + + if (mons_is_lurking(this)) + { + behaviour = BEH_WANDER; + behaviour_event(this, ME_EVAL); + } + break; + + case ENCH_CHARM: + behaviour = BEH_SEEK; + target = you.pos(); + foe = MHITYOU; + + if (is_patrolling()) + { + // Enslaved monsters stop patrolling and forget their patrol + // point; they're supposed to follow you now. + patrol_point.reset(); + } + if (you.can_see(this)) + learned_something_new(TUT_MONSTER_FRIENDLY, pos()); + break; + + default: + break; + } +} + +static bool _prepare_del_ench(monsters* mon, const mon_enchant &me) +{ + if (me.ench != ENCH_SUBMERGED) + return (true); + + // Lurking monsters only unsubmerge when their foe is in sight if the foe + // is right next to them. + if (mons_is_lurking(mon)) + { + const actor* foe = mon->get_foe(); + if (foe != NULL && mon->can_see(foe) + && !adjacent(mon->pos(), foe->pos())) + { + return (false); + } + } + + int midx = mon->mindex(); + + if (mgrd(mon->pos()) == NON_MONSTER) + mgrd(mon->pos()) = midx; + + if (mon->pos() != you.pos() && midx == mgrd(mon->pos())) + return (true); + + if (midx != mgrd(mon->pos())) + { + monsters* other_mon = &menv[mgrd(mon->pos())]; + + if (other_mon->type == MONS_NO_MONSTER + || other_mon->type == MONS_PROGRAM_BUG) + { + mgrd(mon->pos()) = midx; + + mprf(MSGCH_ERROR, "mgrd(%d,%d) points to %s monster, even " + "though it contains submerged monster %s (see bug 2293518)", + mon->pos().x, mon->pos().y, + other_mon->type == MONS_NO_MONSTER ? "dead" : "buggy", + mon->name(DESC_PLAIN, true).c_str()); + + if (mon->pos() != you.pos()) + return (true); + } + else + mprf(MSGCH_ERROR, "%s tried to unsubmerge while on same square as " + "%s (see bug 2293518)", mon->name(DESC_CAP_THE, true).c_str(), + mon->name(DESC_NOCAP_A, true).c_str()); + } + + // Monster un-submerging while under player or another monster. Try to + // move to an adjacent square in which the monster could have been + // submerged and have it unsubmerge from there. + coord_def target_square; + int okay_squares = 0; + + for (adjacent_iterator ai; ai; ++ai) + if (mgrd(*ai) == NON_MONSTER && *ai != you.pos() + && monster_can_submerge(mon, grd(*ai)) + && one_chance_in(++okay_squares)) + { + target_square = *ai; + } + + if (okay_squares > 0) + { + mon->move_to_pos(target_square); + return (true); + } + + // No available adjacent squares from which the monster could also + // have unsubmerged. Can it just stay submerged where it is? + if (monster_can_submerge(mon, grd(mon->pos()))) + return (false); + + // The terrain changed and the monster can't remain submerged. + // Try to move to an adjacent square where it would be happy. + for (adjacent_iterator ai; ai; ++ai) + { + if (mgrd(*ai) == NON_MONSTER + && monster_habitable_grid(mon, grd(*ai)) + && !find_trap(*ai)) + { + if (one_chance_in(++okay_squares)) + target_square = *ai; + } + } + + if (okay_squares > 0) + mon->move_to_pos(target_square); + + return (true); +} + +bool monsters::del_ench(enchant_type ench, bool quiet, bool effect) +{ + mon_enchant_list::iterator i = enchantments.find(ench); + if (i == enchantments.end()) + return (false); + + const mon_enchant me = i->second; + const enchant_type et = i->first; + + if (!_prepare_del_ench(this, me)) + return (false); + + enchantments.erase(et); + if (effect) + remove_enchantment_effect(me, quiet); + return (true); +} + +void monsters::remove_enchantment_effect(const mon_enchant &me, bool quiet) +{ + switch (me.ench) + { + case ENCH_BERSERK: + scale_hp(2, 3); + break; + + case ENCH_HASTE: + if (speed >= 100) + speed = 100 + ((speed - 100) / 2); + else + speed /= 2; + if (!quiet) + simple_monster_message(this, " is no longer moving quickly."); + break; + + case ENCH_MIGHT: + if (!quiet) + simple_monster_message(this, " no longer looks unusually strong."); + break; + + case ENCH_SLOW: + if (!quiet) + simple_monster_message(this, " is no longer moving slowly."); + if (speed >= 100) + speed = 100 + ((speed - 100) * 2); + else + speed *= 2; + + break; + + case ENCH_PARALYSIS: + if (!quiet) + simple_monster_message(this, " is no longer paralysed."); + + behaviour_event(this, ME_EVAL); + break; + + case ENCH_NEUTRAL: + if (!quiet) + simple_monster_message(this, " is no longer neutral."); + + behaviour_event(this, ME_EVAL); + break; + + case ENCH_PETRIFIED: + if (!quiet) + simple_monster_message(this, " is no longer petrified."); + del_ench(ENCH_PETRIFYING); + + behaviour_event(this, ME_EVAL); + break; + + case ENCH_PETRIFYING: + if (!has_ench(ENCH_PETRIFIED)) + break; + + if (!quiet) + simple_monster_message(this, " stops moving altogether!"); + + behaviour_event(this, ME_EVAL); + break; + + case ENCH_FEAR: + if (holiness() == MH_NONLIVING || has_ench(ENCH_BERSERK)) + { + // This should only happen because of fleeing sanctuary + snprintf(info, INFO_SIZE, " stops retreating."); + } + else + { + snprintf(info, INFO_SIZE, " seems to regain %s courage.", + this->pronoun(PRONOUN_NOCAP_POSSESSIVE, true).c_str()); + } + + if (!quiet) + simple_monster_message(this, info); + + // Reevaluate behaviour. + behaviour_event(this, ME_EVAL); + break; + + case ENCH_CONFUSION: + if (!quiet) + simple_monster_message(this, " seems less confused."); + + // Reevaluate behaviour. + behaviour_event(this, ME_EVAL); + break; + + case ENCH_INVIS: + // Invisible monsters stay invisible. + if (mons_class_flag(type, M_INVIS)) + add_ench(mon_enchant(ENCH_INVIS)); + else if (mons_near(this) && !you.can_see_invisible() + && !has_ench(ENCH_SUBMERGED)) + { + if (!quiet) + { + mprf("%s appears from thin air!", + name(DESC_CAP_A, true).c_str()); + autotoggle_autopickup(false); + } + + handle_seen_interrupt(this); + } + break; + + case ENCH_CHARM: + if (!quiet) + simple_monster_message(this, " is no longer charmed."); + + if (you.can_see(this)) + { + // and fire activity interrupts + interrupt_activity(AI_SEE_MONSTER, + activity_interrupt_data(this, "uncharm")); + } + + if (is_patrolling()) + { + // Enslaved monsters stop patrolling and forget their patrol point, + // in case they were on order to wait. + patrol_point.reset(); + } + + // Reevaluate behaviour. + behaviour_event(this, ME_EVAL); + break; + + case ENCH_BACKLIGHT: + if (!quiet) + { + if (visible_to(&you)) + simple_monster_message(this, " stops glowing."); + else if (has_ench(ENCH_INVIS) && mons_near(this)) + { + mprf("%s stops glowing and disappears.", + name(DESC_CAP_THE, true).c_str()); + } + } + break; + + case ENCH_STICKY_FLAME: + if (!quiet) + simple_monster_message(this, " stops burning."); + break; + + case ENCH_POISON: + if (!quiet) + simple_monster_message(this, " looks more healthy."); + break; + + case ENCH_ROT: + if (!quiet) + simple_monster_message(this, " is no longer rotting."); + break; + + case ENCH_HELD: + { + int net = get_trapping_net(this->pos()); + if (net != NON_ITEM) + remove_item_stationary(mitm[net]); + + if (!quiet) + simple_monster_message(this, " breaks free."); + break; + } + case ENCH_ABJ: + case ENCH_SHORT_LIVED: + // Set duration to -1 so that monster_die() and any of its + // callees can tell that the monster ran out of time or was + // abjured. + add_ench(mon_enchant(ENCH_ABJ, 0, KC_OTHER, -1)); + + if (this->has_ench(ENCH_BERSERK)) + simple_monster_message(this, " is no longer berserk."); + + monster_die(this, quiet ? KILL_DISMISSED : KILL_RESET, NON_MONSTER); + break; + + case ENCH_SUBMERGED: + if (mons_is_wandering(this)) + { + behaviour = BEH_SEEK; + behaviour_event(this, ME_EVAL); + } + + if (you.pos() == this->pos()) + { + mprf(MSGCH_ERROR, "%s is on the same square as you!", + name(DESC_CAP_A).c_str()); + } + + if (you.can_see(this)) + { + if (!mons_is_safe(this) && is_run_delay(current_delay_action())) + { + // Already set somewhere else. + if (!seen_context.empty()) + return; + + if (type == MONS_AIR_ELEMENTAL) + seen_context = "thin air"; + else if (type == MONS_TRAPDOOR_SPIDER) + seen_context = "leaps out"; + else if (!monster_habitable_grid(this, DNGN_FLOOR)) + seen_context = "bursts forth"; + else + seen_context = "surfaces"; + } + else if (!quiet) + { + if (type == MONS_AIR_ELEMENTAL) + { + mprf("%s forms itself from the air!", + name(DESC_CAP_A, true).c_str() ); + } + else if (type == MONS_TRAPDOOR_SPIDER) + { + mprf("%s leaps out from its hiding place under the floor!", + name(DESC_CAP_A, true).c_str() ); + } + else if (crawl_state.arena) + mprf("%s surfaces.", name(DESC_CAP_A, true).c_str() ); + } + } + else if (mons_near(this) + && feat_compatible(grd(pos()), DNGN_DEEP_WATER)) + { + mpr("Something invisible bursts forth from the water."); + interrupt_activity(AI_FORCE_INTERRUPT); + } + break; + + case ENCH_SOUL_RIPE: + if (!quiet) + { + simple_monster_message(this, + "'s soul is no longer ripe for the taking."); + } + break; + + default: + break; + } +} + +bool monsters::lose_ench_levels(const mon_enchant &e, int lev) +{ + if (!lev) + return (false); + + if (e.degree <= lev) + { + del_ench(e.ench); + return (true); + } + else + { + mon_enchant newe(e); + newe.degree -= lev; + update_ench(newe); + return (false); + } +} + +bool monsters::lose_ench_duration(const mon_enchant &e, int dur) +{ + if (!dur) + return (false); + + if (e.duration <= dur) + { + del_ench(e.ench); + return (true); + } + else + { + mon_enchant newe(e); + newe.duration -= dur; + update_ench(newe); + return (false); + } +} + +//--------------------------------------------------------------- +// +// timeout_enchantments +// +// Update a monster's enchantments when the player returns +// to the level. +// +// Management for enchantments... problems with this are the oddities +// (monster dying from poison several thousands of turns later), and +// game balance. +// +// Consider: Poison/Sticky Flame a monster at range and leave, monster +// dies but can't leave level to get to player (implied game balance of +// the delayed damage is that the monster could be a danger before +// it dies). This could be fixed by keeping some monsters active +// off level and allowing them to take stairs (a very serious change). +// +// Compare this to the current abuse where the player gets +// effectively extended duration of these effects (although only +// the actual effects only occur on level, the player can leave +// and heal up without having the effect disappear). +// +// This is a simple compromise between the two... the enchantments +// go away, but the effects don't happen off level. -- bwr +// +//--------------------------------------------------------------- +void monsters::timeout_enchantments(int levels) +{ + if (enchantments.empty()) + return; + + const mon_enchant_list ec = enchantments; + for (mon_enchant_list::const_iterator i = ec.begin(); + i != ec.end(); ++i) + { + switch (i->first) + { + case ENCH_POISON: case ENCH_ROT: case ENCH_BACKLIGHT: + case ENCH_STICKY_FLAME: case ENCH_ABJ: case ENCH_SHORT_LIVED: + case ENCH_SLOW: case ENCH_HASTE: case ENCH_MIGHT: case ENCH_FEAR: + case ENCH_INVIS: case ENCH_CHARM: case ENCH_SLEEP_WARY: + case ENCH_SICK: case ENCH_SLEEPY: case ENCH_PARALYSIS: + case ENCH_PETRIFYING: case ENCH_PETRIFIED: + case ENCH_BATTLE_FRENZY: case ENCH_NEUTRAL: + case ENCH_LOWERED_MR: case ENCH_SOUL_RIPE: + lose_ench_levels(i->second, levels); + break; + + case ENCH_BERSERK: + del_ench(i->first); + del_ench(ENCH_HASTE, true); + del_ench(ENCH_MIGHT, true); + break; + + case ENCH_FATIGUE: + del_ench(i->first); + del_ench(ENCH_SLOW); + break; + + case ENCH_TP: + del_ench(i->first); + teleport(true); + break; + + case ENCH_CONFUSION: + if (!mons_class_flag(type, M_CONFUSED)) + del_ench(i->first); + blink(); + break; + + case ENCH_HELD: + del_ench(i->first); + break; + + case ENCH_SLOWLY_DYING: + { + const int actdur = speed_to_duration(speed) * levels; + if (lose_ench_duration(i->first, actdur)) + monster_die(this, KILL_MISC, NON_MONSTER, true); + break; + } + default: + break; + } + + if (!alive()) + break; + } +} + +std::string monsters::describe_enchantments() const +{ + std::ostringstream oss; + for (mon_enchant_list::const_iterator i = enchantments.begin(); + i != enchantments.end(); ++i) + { + if (i != enchantments.begin()) + oss << ", "; + oss << std::string(i->second); + } + return (oss.str()); +} + +// Used to adjust time durations in calc_duration() for monster speed. +static inline int _mod_speed( int val, int speed ) +{ + if (!speed) + speed = 10; + const int modded = (speed ? (val * 10) / speed : val); + return (modded? modded : 1); +} + +bool monsters::decay_enchantment(const mon_enchant &me, bool decay_degree) +{ + // Faster monsters can wiggle out of the net more quickly. + const int spd = (me.ench == ENCH_HELD) ? speed : + 10; + const int actdur = speed_to_duration(spd); + if (lose_ench_duration(me, actdur)) + return (true); + + if (!decay_degree) + return (false); + + // Decay degree so that higher degrees decay faster than lower + // degrees, and a degree of 1 does not decay (it expires when the + // duration runs out). + const int level = me.degree; + if (level <= 1) + return (false); + + const int decay_factor = level * (level + 1) / 2; + if (me.duration < me.maxduration * (decay_factor - 1) / decay_factor) + { + mon_enchant newme = me; + --newme.degree; + newme.maxduration = newme.duration; + + if (newme.degree <= 0) + { + del_ench(me.ench); + return (true); + } + else + update_ench(newme); + } + return (false); +} + +void monsters::apply_enchantment(const mon_enchant &me) +{ + const int spd = 10; + switch (me.ench) + { + case ENCH_BERSERK: + if (decay_enchantment(me)) + { + simple_monster_message(this, " is no longer berserk."); + del_ench(ENCH_HASTE, true); + del_ench(ENCH_MIGHT, true); + const int duration = random_range(70, 130); + add_ench(mon_enchant(ENCH_FATIGUE, 0, KC_OTHER, duration)); + add_ench(mon_enchant(ENCH_SLOW, 0, KC_OTHER, duration)); + } + break; + + case ENCH_FATIGUE: + if (decay_enchantment(me)) + { + simple_monster_message(this, " looks more energetic."); + del_ench(ENCH_SLOW, true); + } + break; + + case ENCH_SLOW: + case ENCH_HASTE: + case ENCH_MIGHT: + case ENCH_FEAR: + case ENCH_PARALYSIS: + case ENCH_NEUTRAL: + case ENCH_PETRIFYING: + case ENCH_PETRIFIED: + case ENCH_SICK: + case ENCH_BACKLIGHT: + case ENCH_ABJ: + case ENCH_CHARM: + case ENCH_SLEEP_WARY: + case ENCH_LOWERED_MR: + case ENCH_SOUL_RIPE: + decay_enchantment(me); + break; + + case ENCH_BATTLE_FRENZY: + decay_enchantment(me, false); + break; + + case ENCH_AQUATIC_LAND: + // Aquatic monsters lose hit points every turn they spend on dry land. + ASSERT(mons_habitat(this) == HT_WATER + && !feat_is_watery( grd(pos()) )); + + // Zombies don't take damage from flopping about on land. + if (mons_is_zombified(this)) + break; + + // We don't have a reasonable agent to give. + // Don't clean up the monster in order to credit properly. + hurt(NULL, 1 + random2(5), BEAM_NONE, false); + + // Credit the kill. + if (hit_points < 1) + { + monster_die(this, me.killer(), me.kill_agent()); + break; + } + break; + + case ENCH_HELD: + { + if (mons_is_stationary(this) || mons_cannot_act(this) + || asleep()) + { + break; + } + + int net = get_trapping_net(this->pos(), true); + + if (net == NON_ITEM) // Really shouldn't happen! + { + del_ench(ENCH_HELD); + break; + } + + // Handled in handle_pickup(). + if (mons_eats_items(this)) + break; + + // The enchantment doubles as the durability of a net + // the more corroded it gets, the more easily it will break. + const int hold = mitm[net].plus; // This will usually be negative. + const int mon_size = body_size(PSIZE_BODY); + + // Smaller monsters can escape more quickly. + if (mon_size < random2(SIZE_BIG) // BIG = 5 + && !has_ench(ENCH_BERSERK) && type != MONS_DANCING_WEAPON) + { + if (mons_near(this) && !visible_to(&you)) + mpr("Something wriggles in the net."); + else + simple_monster_message(this, " struggles to escape the net."); + + // Confused monsters have trouble finding the exit. + if (has_ench(ENCH_CONFUSION) && !one_chance_in(5)) + break; + + decay_enchantment(me, 2*(NUM_SIZE_LEVELS - mon_size) - hold); + + // Frayed nets are easier to escape. + if (mon_size <= -(hold-1)/2) + decay_enchantment(me, (NUM_SIZE_LEVELS - mon_size)); + } + else // Large (and above) monsters always thrash the net and destroy it + { // e.g. ogre, large zombie (large); centaur, naga, hydra (big). + + if (mons_near(this) && !visible_to(&you)) + mpr("Something wriggles in the net."); + else + simple_monster_message(this, " struggles against the net."); + + // Confused monsters more likely to struggle without result. + if (has_ench(ENCH_CONFUSION) && one_chance_in(3)) + break; + + // Nets get destroyed more quickly for larger monsters + // and if already strongly frayed. + int damage = 0; + + // tiny: 1/6, little: 2/5, small: 3/4, medium and above: always + if (x_chance_in_y(mon_size + 1, SIZE_GIANT - mon_size)) + damage++; + + // Handled specially to make up for its small size. + if (type == MONS_DANCING_WEAPON) + { + damage += one_chance_in(3); + + if (can_cut_meat(mitm[inv[MSLOT_WEAPON]])) + damage++; + } + + + // Extra damage for large (50%) and big (always). + if (mon_size == SIZE_BIG || mon_size == SIZE_LARGE && coinflip()) + damage++; + + // overall damage per struggle: + // tiny -> 1/6 + // little -> 2/5 + // small -> 3/4 + // medium -> 1 + // large -> 1,5 + // big -> 2 + + // extra damage if already damaged + if (random2(body_size(PSIZE_BODY) - hold + 1) >= 4) + damage++; + + // Berserking doubles damage dealt. + if (has_ench(ENCH_BERSERK)) + damage *= 2; + + // Faster monsters can damage the net more often per + // time period. + if (speed != 0) + damage = div_rand_round(damage * speed, spd); + + mitm[net].plus -= damage; + + if (mitm[net].plus < -7) + { + if (mons_near(this)) + { + if (visible_to(&you)) + { + mprf("The net rips apart, and %s comes free!", + name(DESC_NOCAP_THE).c_str()); + } + else + { + mpr("All of a sudden the net rips apart!"); + } + } + destroy_item(net); + + del_ench(ENCH_HELD, true); + } + + } + break; + } + case ENCH_CONFUSION: + if (!mons_class_flag(type, M_CONFUSED)) + decay_enchantment(me); + break; + + case ENCH_INVIS: + if (!mons_class_flag(type, M_INVIS)) + decay_enchantment(me); + break; + + case ENCH_SUBMERGED: + { + // Not even air elementals unsubmerge into clouds. + if (env.cgrid(pos()) != EMPTY_CLOUD) + break; + + // Air elementals are a special case, as their submerging in air + // isn't up to choice. - bwr + if (type == MONS_AIR_ELEMENTAL) + { + heal_monster( this, 1, one_chance_in(5) ); + + if (one_chance_in(5)) + del_ench(ENCH_SUBMERGED); + + break; + } + + // Now we handle the others: + const dungeon_feature_type grid = grd(pos()); + + // Badly injured monsters prefer to stay submerged... + // electric eels and lava snakes have ranged attacks + // and are more likely to surface. -- bwr + if (!monster_can_submerge(this, grid)) + del_ench(ENCH_SUBMERGED); // forced to surface + else if (hit_points <= max_hit_points / 2) + break; + else if (type == MONS_TRAPDOOR_SPIDER) + { + // This should probably never happen. + if (!mons_is_lurking(this)) + del_ench(ENCH_SUBMERGED); + break; + } + else if (((type == MONS_ELECTRIC_EEL || type == MONS_LAVA_SNAKE || type == MONS_KRAKEN) + && (one_chance_in(50) || (mons_near(this) + && hit_points == max_hit_points + && !one_chance_in(10)))) + || one_chance_in(200) + || (mons_near(this) + && hit_points == max_hit_points + && !one_chance_in(5))) + { + del_ench(ENCH_SUBMERGED); + } + break; + } + case ENCH_POISON: + { + const int poisonval = me.degree; + int dam = (poisonval >= 4) ? 1 : 0; + + if (coinflip()) + dam += roll_dice(1, poisonval + 1); + + if (res_poison() < 0) + dam += roll_dice(2, poisonval) - 1; + + if (dam > 0) + { + // We don't have a reasonable agent to give. + // Don't clean up the monster in order to credit properly. + hurt(NULL, dam, BEAM_POISON, false); + +#if DEBUG_DIAGNOSTICS + // For debugging, we don't have this silent. + simple_monster_message( this, " takes poison damage.", + MSGCH_DIAGNOSTICS ); + mprf(MSGCH_DIAGNOSTICS, "poison damage: %d", dam ); +#endif + + // Credit the kill. + if (hit_points < 1) + { + monster_die(this, me.killer(), me.kill_agent()); + break; + } + } + + decay_enchantment(me, true); + break; + } + case ENCH_ROT: + { + if (hit_points > 1 && one_chance_in(3)) + { + hurt(NULL, 1); // nonlethal so we don't care about agent + if (hit_points < max_hit_points && coinflip()) + --max_hit_points; + } + + decay_enchantment(me, true); + break; + } + + // Assumption: monsters::res_fire has already been checked. + case ENCH_STICKY_FLAME: + { + if (feat_is_watery(grd(pos()))) + { + if (mons_near(this) && visible_to(&you)) + mprf("The flames covering %s go out.", + this->name(DESC_NOCAP_THE, false).c_str()); + del_ench(ENCH_STICKY_FLAME); + break; + } + int dam = resist_adjust_damage(this, BEAM_FIRE, res_fire(), + roll_dice(2, 4) - 1); + + if (dam > 0) + { + simple_monster_message(this, " burns!"); + // We don't have a reasonable agent to give. + // Don't clean up the monster in order to credit properly. + hurt(NULL, dam, BEAM_NAPALM, false); + +#if DEBUG_DIAGNOSTICS + mprf( MSGCH_DIAGNOSTICS, "sticky flame damage: %d", dam ); +#endif + + // Credit the kill. + if (hit_points < 1) + { + monster_die(this, me.killer(), me.kill_agent()); + break; + } + } + + decay_enchantment(me, true); + break; + } + + case ENCH_SHORT_LIVED: + // This should only be used for ball lightning -- bwr + if (decay_enchantment(me)) + hit_points = -1; + break; + + case ENCH_SLOWLY_DYING: + // If you are no longer dying, you must be dead. + if (decay_enchantment(me)) + { + if (::see_cell(this->position)) + { + mprf("A nearby %s withers and dies.", + this->name(DESC_PLAIN, false).c_str()); + } + + monster_die(this, KILL_MISC, NON_MONSTER, true); + } + break; + + case ENCH_SPORE_PRODUCTION: + // Very low chance of actually making a spore on each turn. + if(one_chance_in(5000)) + { + int idx[] = {0, 1, 2, 3, 4, 5, 6, 7}; + std::random_shuffle(idx, idx + 8); + + for (unsigned i = 0; i < 8; ++i) + { + coord_def adjacent = this->pos() + Compass[idx[i]]; + if (mons_class_can_pass(MONS_GIANT_SPORE, env.grid(adjacent)) + && !actor_at(adjacent)) + { + beh_type created_behavior = BEH_HOSTILE; + + if (this->attitude == ATT_FRIENDLY) + created_behavior = BEH_FRIENDLY; + + int rc = create_monster(mgen_data(MONS_GIANT_SPORE, + created_behavior, + 0, + 0, + adjacent, + MHITNOT, + MG_FORCE_PLACE)); + + if (rc != -1) + { + env.mons[rc].behaviour = BEH_WANDER; + + if (::see_cell(adjacent) && ::see_cell(pos())) + mpr("A nearby fungus spawns a giant spore."); + } + break; + } + } + } + break; + + case ENCH_GLOWING_SHAPESHIFTER: // This ench never runs out! + // Number of actions is fine for shapeshifters. Don't change + // shape while taking the stairs because monster_polymorph() has + // an assert about it. -cao + if (!(this->flags & MF_TAKING_STAIRS) && !asleep() + && (type == MONS_GLOWING_SHAPESHIFTER + || one_chance_in(4))) + { + monster_polymorph(this, RANDOM_MONSTER); + } + break; + + case ENCH_SHAPESHIFTER: // This ench never runs out! + if (!(this->flags & MF_TAKING_STAIRS) && !asleep() + && (type == MONS_SHAPESHIFTER + || x_chance_in_y(1000 / (15 * hit_dice / 5), 1000))) + { + monster_polymorph(this, RANDOM_MONSTER); + } + break; + + case ENCH_TP: + if (decay_enchantment(me, true)) + monster_teleport(this, true); + break; + + case ENCH_SLEEPY: + del_ench(ENCH_SLEEPY); + break; + + case ENCH_EAT_ITEMS: + break; + + default: + break; + } +} + +void monsters::mark_summoned(int longevity, bool mark_items, int summon_type) +{ + add_ench( mon_enchant(ENCH_ABJ, longevity) ); + if (summon_type != 0) + add_ench( mon_enchant(ENCH_SUMMON, summon_type, KC_OTHER, INT_MAX) ); + + if (mark_items) + { + for (int i = 0; i < NUM_MONSTER_SLOTS; ++i) + { + const int item = inv[i]; + if (item != NON_ITEM) + mitm[item].flags |= ISFLAG_SUMMONED; + } + } +} + +bool monsters::is_summoned(int* duration, int* summon_type) const +{ + return mons_is_summoned(this, duration, summon_type); +} + +void monsters::apply_enchantments() +{ + if (enchantments.empty()) + return; + + // The ordering in enchant_type makes sure that "super-enchantments" + // like berserk time out before their parts. + const mon_enchant_list ec = enchantments; + for (mon_enchant_list::const_iterator i = ec.begin(); i != ec.end(); ++i) + { + apply_enchantment(i->second); + if (!alive()) + break; + } +} + +void monsters::scale_hp(int num, int den) +{ + hit_points = hit_points * num / den; + max_hit_points = max_hit_points * num / den; + + if (hit_points < 1) + hit_points = 1; + if (max_hit_points < 1) + max_hit_points = 1; + if (hit_points > max_hit_points) + hit_points = max_hit_points; +} + +kill_category monsters::kill_alignment() const +{ + return (mons_friendly_real(this) ? KC_FRIENDLY : KC_OTHER); +} + +bool monsters::sicken(int amount) +{ + if (res_rotting() || (amount /= 2) < 1) + return (false); + + if (!has_ench(ENCH_SICK) && you.can_see(this)) + { + // Yes, could be confused with poisoning. + mprf("%s looks sick.", name(DESC_CAP_THE).c_str()); + } + + add_ench(mon_enchant(ENCH_SICK, 0, KC_OTHER, amount * 10)); + + return (true); +} + +// Recalculate movement speed. +void monsters::fix_speed() +{ + speed = mons_real_base_speed(type); + + if (has_ench(ENCH_HASTE)) + speed *= 2; + else if (has_ench(ENCH_SLOW)) + speed /= 2; +} + +// Check speed and speed_increment sanity. +void monsters::check_speed() +{ + // FIXME: If speed is borked, recalculate. Need to figure out how + // speed is getting borked. + if (speed < 0 || speed > 130) + { +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, + "Bad speed: %s, spd: %d, spi: %d, hd: %d, ench: %s", + name(DESC_PLAIN).c_str(), + speed, speed_increment, hit_dice, + describe_enchantments().c_str()); +#endif + + fix_speed(); + +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "Fixed speed for %s to %d", + name(DESC_PLAIN).c_str(), speed); +#endif + } + + if (speed_increment < 0) + speed_increment = 0; + + if (speed_increment > 200) + { +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, + "Clamping speed increment on %s: %d", + name(DESC_PLAIN).c_str(), speed_increment); +#endif + speed_increment = 140; + } +} + +actor *monsters::get_foe() const +{ + if (foe == MHITNOT) + return (NULL); + else if (foe == MHITYOU) + return (mons_friendly(this) ? NULL : &you); + + // Must be a monster! + monsters *my_foe = &menv[foe]; + return (my_foe->alive()? my_foe : NULL); +} + +int monsters::foe_distance() const +{ + const actor *afoe = get_foe(); + return (afoe ? pos().distance_from(afoe->pos()) + : INFINITE_DISTANCE); +} + +bool monsters::can_go_berserk() const +{ + if (holiness() != MH_NATURAL || type == MONS_KRAKEN_TENTACLE) + return (false); + + if (has_ench(ENCH_FATIGUE) || has_ench(ENCH_BERSERK)) + return (false); + + // If we have no melee attack, going berserk is pointless. + const mon_attack_def attk = mons_attack_spec(this, 0); + if (attk.type == AT_NONE || attk.damage == 0) + return (false); + + return (true); +} + +bool monsters::needs_berserk(bool check_spells) const +{ + if (!can_go_berserk()) + return (false); + + if (has_ench(ENCH_HASTE) || has_ench(ENCH_TP)) + return (false); + + if (foe_distance() > 3) + return (false); + + if (check_spells) + { + for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) + { + const int spell = spells[i]; + if (spell != SPELL_NO_SPELL && spell != SPELL_BERSERKER_RAGE) + return (false); + } + } + + return (true); +} + +bool monsters::can_see_invisible() const +{ + if (mons_is_ghost_demon(this->type)) + return (this->ghost->see_invis); + else if (mons_class_flag(this->type, M_SEE_INVIS)) + return (true); + else if (scan_mon_inv_randarts(this, ARTP_EYESIGHT) > 0) + return (true); + return (false); +} + +bool monsters::invisible() const +{ + return (has_ench(ENCH_INVIS) && !backlit()); +} + +bool monsters::visible_to(const actor *looker) const +{ + bool vis = !invisible() || looker->can_see_invisible(); + return (vis && (this == looker || !has_ench(ENCH_SUBMERGED))); +} + +bool monsters::mon_see_cell(const coord_def& p, bool reach) const +{ + if (distance(pos(), p) > LOS_RADIUS * LOS_RADIUS + 1) + return (false); + + dungeon_feature_type max_disallowed = DNGN_MAXOPAQUE; + if (reach) + max_disallowed = DNGN_MAX_NONREACH; + + // XXX: Ignoring clouds for now. + return (!num_feats_between(pos(), p, DNGN_UNSEEN, max_disallowed, + true, true)); +} + +bool monsters::see_cell(const coord_def &c) const +{ + // XXX: using env.show since that's been filled anywa. + if (c == you.pos()) + return (you.see_cell(pos())); + + // TODO: Proper monster LOS. + return (mon_see_cell(c)); +} + +bool monsters::can_see(const actor *targ) const +{ + return (targ->visible_to(this) && see_cell(targ->pos())); +} + +bool monsters::near_foe() const +{ + const actor *afoe = get_foe(); + return (afoe && see_cell(afoe->pos())); +} + +bool monsters::can_mutate() const +{ + return (holiness() == MH_NATURAL || holiness() == MH_PLANT); +} + +bool monsters::can_safely_mutate() const +{ + return (can_mutate()); +} + +bool monsters::can_bleed() const +{ + return (mons_has_blood(type)); +} + +bool monsters::mutate() +{ + if (!can_mutate()) + return (false); + + // Polymorphing a (very) ugly thing will mutate it into a different + // (very) ugly thing. + if (type == MONS_UGLY_THING || type == MONS_VERY_UGLY_THING) + { + ugly_thing_mutate(this); + return (true); + } + + // Polymorphing a shapeshifter will make it revert to its original + // form. + if (this->has_ench(ENCH_GLOWING_SHAPESHIFTER)) + return (monster_polymorph(this, MONS_GLOWING_SHAPESHIFTER)); + if (this->has_ench(ENCH_SHAPESHIFTER)) + return (monster_polymorph(this, MONS_SHAPESHIFTER)); + + return (monster_polymorph(this, RANDOM_MONSTER)); +} + +bool monsters::is_icy() const +{ + return (mons_is_icy(type)); +} + +static bool _mons_is_fiery(int mc) +{ + return (mc == MONS_FIRE_VORTEX + || mc == MONS_FIRE_ELEMENTAL + || mc == MONS_FLAMING_CORPSE + || mc == MONS_EFREET + || mc == MONS_AZRAEL + || mc == MONS_LAVA_WORM + || mc == MONS_LAVA_FISH + || mc == MONS_LAVA_SNAKE + || mc == MONS_SALAMANDER + || mc == MONS_MOLTEN_GARGOYLE + || mc == MONS_ORB_OF_FIRE); +} + +bool monsters::is_fiery() const +{ + return (_mons_is_fiery(type)); +} + +bool monsters::has_action_energy() const +{ + return (speed_increment >= 80); +} + +void monsters::check_redraw(const coord_def &old) const +{ + const bool see_new = ::see_cell(pos()); + const bool see_old = ::see_cell(old); + if ((see_new || see_old) && !view_update()) + { + if (see_new) + view_update_at(pos()); + if (see_old) + view_update_at(old); + update_screen(); + } +} + +void monsters::apply_location_effects(const coord_def &oldpos) +{ + if (oldpos != pos()) + dungeon_events.fire_position_event(DET_MONSTER_MOVED, pos()); + + if (alive() && has_ench(ENCH_AQUATIC_LAND)) + { + if (!feat_is_watery( grd(pos()) )) + simple_monster_message(this, " flops around on dry land!"); + else if (!feat_is_watery( grd(oldpos) )) + { + simple_monster_message(this, " dives back into the water!"); + del_ench(ENCH_AQUATIC_LAND); + } + } + + // Monsters stepping on traps: + trap_def* ptrap = find_trap(pos()); + if (ptrap) + ptrap->trigger(*this); + + if (alive()) + mons_check_pool(this, pos()); + + if (alive() && has_ench(ENCH_SUBMERGED) + && (!monster_can_submerge(this, grd(pos())) + || type == MONS_TRAPDOOR_SPIDER)) + { + del_ench(ENCH_SUBMERGED); + + if (type == MONS_TRAPDOOR_SPIDER) + behaviour_event(this, ME_EVAL); + } + + unsigned long &prop = env.map(pos()).property; + + if (prop & FPROP_BLOODY) + { + monster_type genus = mons_genus(type); + + if (genus == MONS_JELLY || genus == MONS_GIANT_SLUG) + { + prop &= ~FPROP_BLOODY; + if (see_cell(pos()) && !visible_to(&you)) + { + std::string desc = + feature_description(pos(), false, DESC_NOCAP_THE, false); + mprf("The bloodstain on %s disappears!", desc.c_str()); + } + } + } +} + +bool monsters::move_to_pos(const coord_def &newpos) +{ + if (actor_at(newpos)) + return (false); + + // Clear old cell pointer. + mgrd(pos()) = NON_MONSTER; + + // Set monster x,y to new value. + moveto(newpos); + + // Set new monster grid pointer to this monster. + mgrd(newpos) = mindex(); + + return (true); +} + +// Returns true if the trap should be revealed to the player. +bool monsters::do_shaft() +{ + if (!is_valid_shaft_level()) + return (false); + + // Handle instances of do_shaft() being invoked magically when + // the monster isn't standing over a shaft. + if (get_trap_type(this->pos()) != TRAP_SHAFT) + { + switch (grd(pos())) + { + case DNGN_FLOOR: + case DNGN_OPEN_DOOR: + case DNGN_TRAP_MECHANICAL: + case DNGN_TRAP_MAGICAL: + case DNGN_TRAP_NATURAL: + case DNGN_UNDISCOVERED_TRAP: + case DNGN_ENTER_SHOP: + break; + + default: + return (false); + } + + if (airborne() || total_weight() == 0) + { + if (mons_near(this)) + { + if (visible_to(&you)) + { + mprf("A shaft briefly opens up underneath %s!", + name(DESC_NOCAP_THE).c_str()); + } + else + mpr("A shaft briefly opens up in the floor!"); + } + + handle_items_on_shaft(this->pos(), false); + return (false); + } + } + + level_id lev = shaft_dest(); + + if (lev == level_id::current()) + return (false); + + // If a pacified monster is leaving the level via a shaft trap, and + // has reached its goal, handle it here. + if (!mons_is_pacified(this)) + set_transit(lev); + + const bool reveal = + simple_monster_message(this, " falls through a shaft!"); + + handle_items_on_shaft(this->pos(), false); + + // Monster is no longer on this level. + destroy_inventory(); + monster_cleanup(this); + + return (reveal); +} + +void monsters::put_to_sleep(int) +{ + if (has_ench(ENCH_BERSERK)) + return; + + behaviour = BEH_SLEEP; + add_ench(ENCH_SLEEPY); + add_ench(ENCH_SLEEP_WARY); +} + +void monsters::check_awaken(int) +{ + // XXX +} + +const monsterentry *monsters::find_monsterentry() const +{ + return (type == MONS_NO_MONSTER || type == MONS_PROGRAM_BUG) ? NULL + : get_monster_data(type); +} + +int monsters::action_energy(energy_use_type et) const +{ + if (const monsterentry *me = find_monsterentry()) + { + const mon_energy_usage &mu = me->energy_usage; + switch (et) + { + case EUT_MOVE: return mu.move; + case EUT_SWIM: + // [ds] Amphibious monsters get a significant speed boost + // when swimming, as discussed with dpeg. We do not + // distinguish between amphibians that favour land + // (HT_AMPHIBIOUS_LAND, such as hydras) and those that + // favour water (HT_AMPHIBIOUS_WATER, such as merfolk), but + // that's something we can think about. + if (mons_amphibious(this)) + return div_rand_round(mu.swim * 7, 10); + else + return mu.swim; + case EUT_MISSILE: return mu.missile; + case EUT_ITEM: return mu.item; + case EUT_SPECIAL: return mu.special; + case EUT_SPELL: return mu.spell; + case EUT_ATTACK: return mu.attack; + case EUT_PICKUP: return mu.pickup_percent; + } + } + return 10; +} + +void monsters::lose_energy(energy_use_type et, int div, int mult) +{ + int energy_loss = div_round_up(mult * action_energy(et), div); + if (has_ench(ENCH_PETRIFYING)) + { + energy_loss *= 3; + energy_loss /= 2; + } + + speed_increment -= energy_loss; +} + +bool monsters::can_drink_potion(potion_type ptype) const +{ + if (mons_class_is_stationary(this->type)) + return (false); + + if (mons_itemuse(this) < MONUSE_STARTING_EQUIPMENT) + return (false); + + // These monsters cannot drink. + if (mons_is_skeletal(type) || mons_is_insubstantial(type) + || mons_species() == MONS_LICH || mons_genus(type) == MONS_MUMMY) + { + return (false); + } + + switch (ptype) + { + case POT_HEALING: + case POT_HEAL_WOUNDS: + return (holiness() != MH_NONLIVING + && holiness() != MH_PLANT); + case POT_BLOOD: + case POT_BLOOD_COAGULATED: + return (mons_species() == MONS_VAMPIRE); + case POT_SPEED: + case POT_MIGHT: + case POT_BERSERK_RAGE: + case POT_INVISIBILITY: + // If there are any item using monsters that are permanently + // invisible, this might have to be restricted. + return (true); + default: + break; + } + + return (false); +} + +bool monsters::should_drink_potion(potion_type ptype) const +{ + switch (ptype) + { + case POT_HEALING: + return (hit_points <= max_hit_points / 2) + || has_ench(ENCH_POISON) + || has_ench(ENCH_SICK) + || has_ench(ENCH_CONFUSION) + || has_ench(ENCH_ROT); + case POT_HEAL_WOUNDS: + return (hit_points <= max_hit_points / 2); + case POT_BLOOD: + case POT_BLOOD_COAGULATED: + return (hit_points <= max_hit_points / 2); + case POT_SPEED: + return (!has_ench(ENCH_HASTE)); + case POT_MIGHT: + return (!has_ench(ENCH_MIGHT) && foe_distance() <= 2); + case POT_BERSERK_RAGE: + // this implies !has_ench(ENCH_BERSERK_RAGE) + return (!has_ench(ENCH_MIGHT) && !has_ench(ENCH_HASTE) + && needs_berserk()); + case POT_INVISIBILITY: + // We're being nice: friendlies won't go invisible if the player + // won't be able to see them. + return (!has_ench(ENCH_INVIS) + && (you.can_see_invisible(false) || !mons_friendly(this))); + default: + break; + } + + return (false); +} + +// Return the ID status gained. +item_type_id_state_type monsters::drink_potion_effect(potion_type ptype) +{ + simple_monster_message(this, " drinks a potion."); + + item_type_id_state_type ident = ID_MON_TRIED_TYPE; + + switch (ptype) + { + case POT_HEALING: + { + heal(5 + random2(7)); + simple_monster_message(this, " is healed!"); + + const enchant_type cured_enchants[] = { + ENCH_POISON, ENCH_SICK, ENCH_CONFUSION, ENCH_ROT + }; + + // We can differentiate healing and heal wounds (and blood, + // for vampires) by seeing if any status ailments are cured. + for (unsigned int i = 0; i < ARRAYSZ(cured_enchants); ++i) + if (del_ench(cured_enchants[i])) + ident = ID_KNOWN_TYPE; + } + break; + + case POT_HEAL_WOUNDS: + heal(10 + random2avg(28, 3)); + simple_monster_message(this, " is healed!"); + break; + + case POT_BLOOD: + case POT_BLOOD_COAGULATED: + if (mons_species() == MONS_VAMPIRE) + { + heal(10 + random2avg(28, 3)); + simple_monster_message(this, " is healed!"); + } + break; + + case POT_SPEED: + if (enchant_monster_with_flavour(this, this, BEAM_HASTE)) + ident = ID_KNOWN_TYPE; + break; + + case POT_INVISIBILITY: + if (enchant_monster_with_flavour(this, this, BEAM_INVISIBILITY)) + ident = ID_KNOWN_TYPE; + break; + + case POT_MIGHT: + if (enchant_monster_with_flavour(this, this, BEAM_MIGHT)) + ident = ID_KNOWN_TYPE; + break; + + case POT_BERSERK_RAGE: + if (enchant_monster_with_flavour(this, this, BEAM_BERSERK)) + ident = ID_KNOWN_TYPE; + break; + + default: + break; + } + + return (ident); +} + +void monsters::react_to_damage(int damage, beam_type flavour, kill_category whose) +{ + if (!alive()) + return; + + // The royal jelly objects to taking damage and will SULK. :-) + if (type == MONS_ROYAL_JELLY && flavour != BEAM_TORMENT_DAMAGE + && damage > 8 && x_chance_in_y(damage, 50)) + { + mon_acting mact(this); + + const int tospawn = + 1 + random2avg(1 + std::min((damage - 8) / 8, 5), 2); +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "Trying to spawn %d jellies.", tospawn); +#endif + const beh_type beha = SAME_ATTITUDE(this); + int spawned = 0; + for (int i = 0; i < tospawn; ++i) + { + const monster_type jelly = royal_jelly_ejectable_monster(); + coord_def jpos = find_newmons_square_contiguous(jelly, pos()); + if (!in_bounds(jpos)) + continue; + + const int nmons = mons_place( + mgen_data(jelly, beha, 0, 0, + jpos, foe, 0, god)); + + if (nmons != -1 && nmons != NON_MONSTER) + { + // Don't allow milking the royal jelly. + menv[nmons].flags |= MF_CREATED_FRIENDLY; + spawned++; + } + } + + const bool needs_message = spawned && mons_near(this) + && visible_to(&you); + + if (needs_message) + { + const std::string monnam = name(DESC_CAP_THE); + mprf("%s shudders%s.", monnam.c_str(), + spawned >= 5 ? " alarmingly" : + spawned >= 3 ? " violently" : + spawned > 1 ? " vigorously" : ""); + + if (spawned == 1) + mprf("%s spits out another jelly.", monnam.c_str()); + else + { + mprf("%s spits out %s more jellies.", + monnam.c_str(), + number_in_words(spawned).c_str()); + } + } + } + else if (type == MONS_KRAKEN_TENTACLE && flavour != BEAM_TORMENT_DAMAGE) + { + if (!invalid_monster_index(number) + && menv[number].type == MONS_KRAKEN) + { + menv[number].hurt(&you, damage, flavour); + } + } + else if (type == MONS_BUSH && flavour == BEAM_FIRE + && damage>8 && x_chance_in_y(damage, 20)) + { + place_cloud(CLOUD_FIRE, pos(), 20+random2(15), whose, 5); + } +} + +///////////////////////////////////////////////////////////////////////// +// mon_enchant + +static const char *enchant_names[] = +{ + "none", "slow", "haste", "might", "fear", "conf", "inv", "pois", "bers", + "rot", "summon", "abj", "backlit", "charm", "fire", + "gloshifter", "shifter", "tp", "wary", "submerged", + "short-lived", "paralysis", "sick", "sleep", "fatigue", "held", + "blood-lust", "neutral", "petrifying", "petrified", "magic-vulnerable", + "soul-ripe", "decay", "hungry", "flopping", "spore-producing", + "downtrodden", "bug" +}; + +static const char *_mons_enchantment_name(enchant_type ench) +{ + COMPILE_CHECK(ARRAYSZ(enchant_names) == NUM_ENCHANTMENTS+1, c1); + + if (ench > NUM_ENCHANTMENTS) + ench = NUM_ENCHANTMENTS; + + return (enchant_names[ench]); +} + +mon_enchant::mon_enchant(enchant_type e, int deg, kill_category whose, + int dur) + : ench(e), degree(deg), duration(dur), maxduration(0), who(whose) +{ +} + +mon_enchant::operator std::string () const +{ + return make_stringf("%s (%d:%d%s)", + _mons_enchantment_name(ench), + degree, + duration, + kill_category_desc(who)); +} + +const char *mon_enchant::kill_category_desc(kill_category k) const +{ + return (k == KC_YOU? " you" : + k == KC_FRIENDLY? " pet" : ""); +} + +void mon_enchant::merge_killer(kill_category k) +{ + who = who < k? who : k; +} + +void mon_enchant::cap_degree() +{ + // Sickness is not capped. + if (ench == ENCH_SICK) + return; + + // Hard cap to simulate old enum behaviour, we should really throw this + // out entirely. + const int max = ench == ENCH_ABJ? 6 : 4; + if (degree > max) + degree = max; +} + +mon_enchant &mon_enchant::operator += (const mon_enchant &other) +{ + if (ench == other.ench) + { + degree += other.degree; + cap_degree(); + duration += other.duration; + merge_killer(other.who); + } + return (*this); +} + +mon_enchant mon_enchant::operator + (const mon_enchant &other) const +{ + mon_enchant tmp(*this); + tmp += other; + return (tmp); +} + +killer_type mon_enchant::killer() const +{ + return (who == KC_YOU ? KILL_YOU : + who == KC_FRIENDLY ? KILL_MON + : KILL_MISC); +} + +int mon_enchant::kill_agent() const +{ + return (who == KC_FRIENDLY? ANON_FRIENDLY_MONSTER : 0); +} + +int mon_enchant::modded_speed(const monsters *mons, int hdplus) const +{ + return (_mod_speed(mons->hit_dice + hdplus, mons->speed)); +} + +int mon_enchant::calc_duration(const monsters *mons, + const mon_enchant *added) const +{ + int cturn = 0; + + const int newdegree = added ? added->degree : degree; + const int deg = newdegree ? newdegree : 1; + + // Beneficial enchantments (like Haste) should not be throttled by + // monster HD via modded_speed(). Use mod_speed instead! + switch (ench) + { + case ENCH_HASTE: + case ENCH_MIGHT: + case ENCH_INVIS: + cturn = 1000 / _mod_speed(25, mons->speed); + break; + case ENCH_SLOW: + cturn = 250 / (1 + modded_speed(mons, 10)); + break; + case ENCH_FEAR: + cturn = 150 / (1 + modded_speed(mons, 5)); + break; + case ENCH_PARALYSIS: + cturn = std::max(90 / modded_speed(mons, 5), 3); + break; + case ENCH_PETRIFIED: + cturn = std::max(8, 150 / (1 + modded_speed(mons, 5))); + break; + case ENCH_PETRIFYING: + cturn = 50 / _mod_speed(10, mons->speed); + break; + case ENCH_CONFUSION: + cturn = std::max(100 / modded_speed(mons, 5), 3); + break; + case ENCH_HELD: + cturn = 120 / _mod_speed(25, mons->speed); + break; + case ENCH_POISON: + cturn = 1000 * deg / _mod_speed(125, mons->speed); + break; + case ENCH_STICKY_FLAME: + cturn = 1000 * deg / _mod_speed(200, mons->speed); + break; + case ENCH_ROT: + if (deg > 1) + cturn = 1000 * (deg - 1) / _mod_speed(333, mons->speed); + cturn += 1000 / _mod_speed(250, mons->speed); + break; + case ENCH_BACKLIGHT: + if (deg > 1) + cturn = 1000 * (deg - 1) / _mod_speed(200, mons->speed); + cturn += 1000 / _mod_speed(100, mons->speed); + break; + case ENCH_SHORT_LIVED: + cturn = 1000 / _mod_speed(200, mons->speed); + break; + case ENCH_SLOWLY_DYING: + // This may be a little too direct but the randomization at the end + // of this function is excessive for toadstools. -cao + return (2 * FRESHEST_CORPSE + random2(10)) + * speed_to_duration(mons->speed) * mons->speed / 10; + case ENCH_ABJ: + if (deg >= 6) + cturn = 1000 / _mod_speed(10, mons->speed); + if (deg >= 5) + cturn += 1000 / _mod_speed(20, mons->speed); + cturn += 1000 * std::min(4, deg) / _mod_speed(100, mons->speed); + break; + case ENCH_CHARM: + cturn = 500 / modded_speed(mons, 10); + break; + case ENCH_TP: + cturn = 1000 * deg / _mod_speed(1000, mons->speed); + break; + case ENCH_SLEEP_WARY: + cturn = 1000 / _mod_speed(50, mons->speed); + break; + default: + break; + } + + cturn = std::max(2, cturn); + + int raw_duration = (cturn * speed_to_duration(mons->speed)); + raw_duration = std::max(15, fuzz_value(raw_duration, 60, 40)); + + return (raw_duration); +} + +// Calculate the effective duration (in terms of normal player time - 10 +// duration units being one normal player action) of this enchantment. +void mon_enchant::set_duration(const monsters *mons, const mon_enchant *added) +{ + if (duration && !added) + return; + + if (added && added->duration) + duration += added->duration; + else + duration += calc_duration(mons, added); + + if (duration > maxduration) + maxduration = duration; +} + + -- cgit v1.2.3-54-g00ecf