/**
 * @file
 * @brief Monster information that may be passed to the user.
 *
 * Used to fill the monster pane and to pass monster info to Lua.
**/

#include "AppHdr.h"

#include "mon-info.h"

#include "act-iter.h"
#include "areas.h"
#include "artefact.h"
#include "coord.h"
#include "coordit.h"
#include "env.h"
#include "fight.h"
#include "ghost.h"
#include "itemname.h"
#include "itemprop.h"
#include "libutil.h"
#include "los.h"
#include "message.h"
#include "misc.h"
#include "mon-book.h"
#include "mon-chimera.h"
#include "mon-util.h"
#include "monster.h"
#include "options.h"
#include "religion.h"
#include "skills2.h"
#include "spl-summoning.h"
#include "state.h"
#include "stringutil.h"
#include "terrain.h"
#include "traps.h"

#include <algorithm>
#include <sstream>

static monster_info_flags ench_to_mb(const monster& mons, enchant_type ench)
{
    // Suppress silly-looking combinations, even if they're
    // internally valid.
    if (mons.paralysed() && (ench == ENCH_SLOW || ench == ENCH_HASTE
                      || ench == ENCH_SWIFT
                      || ench == ENCH_PETRIFIED
                      || ench == ENCH_PETRIFYING))
    {
        return NUM_MB_FLAGS;
    }

    if (ench == ENCH_PETRIFIED && mons.has_ench(ENCH_PETRIFYING))
        return NUM_MB_FLAGS;

    // Don't claim that naturally 'confused' monsters are especially bewildered
    if (ench == ENCH_CONFUSION && mons_class_flag(mons.type, M_CONFUSED))
        return NUM_MB_FLAGS;

    switch (ench)
    {
    case ENCH_BERSERK:
        return MB_BERSERK;
    case ENCH_POISON:
        return MB_POISONED;
    case ENCH_SICK:
        return MB_SICK;
    case ENCH_ROT:
        return MB_ROTTING;
    case ENCH_CORONA:
    case ENCH_SILVER_CORONA:
        return MB_GLOWING;
    case ENCH_SLOW:
        return MB_SLOWED;
    case ENCH_INSANE:
        return MB_INSANE;
    case ENCH_BATTLE_FRENZY:
        return MB_FRENZIED;
    case ENCH_ROUSED:
        return MB_ROUSED;
    case ENCH_HASTE:
        return MB_HASTED;
    case ENCH_MIGHT:
        return MB_STRONG;
    case ENCH_CONFUSION:
        return MB_CONFUSED;
    case ENCH_INVIS:
    {
        you.seen_invis = true;
        return MB_INVISIBLE;
    }
    case ENCH_CHARM:
        return MB_CHARMED;
    case ENCH_STICKY_FLAME:
        return MB_BURNING;
    case ENCH_HELD:
        return get_trapping_net(mons.pos(), true) == NON_ITEM
               ? MB_WEBBED : MB_CAUGHT;
    case ENCH_PETRIFIED:
        return MB_PETRIFIED;
    case ENCH_PETRIFYING:
        return MB_PETRIFYING;
    case ENCH_LOWERED_MR:
        return MB_VULN_MAGIC;
    case ENCH_SWIFT:
        return MB_SWIFT;
    case ENCH_SILENCE:
        return MB_SILENCING;
    case ENCH_PARALYSIS:
        return MB_PARALYSED;
    case ENCH_SOUL_RIPE:
        return MB_POSSESSABLE;
    case ENCH_REGENERATION:
        return MB_REGENERATION;
    case ENCH_RAISED_MR:
        return MB_RAISED_MR;
    case ENCH_MIRROR_DAMAGE:
        return MB_MIRROR_DAMAGE;
    case ENCH_FEAR_INSPIRING:
        return MB_FEAR_INSPIRING;
    case ENCH_WITHDRAWN:
        return MB_WITHDRAWN;
    case ENCH_BLEED:
        return MB_BLEEDING;
    case ENCH_DAZED:
        return MB_DAZED;
    case ENCH_MUTE:
        return MB_MUTE;
    case ENCH_BLIND:
        return MB_BLIND;
    case ENCH_DUMB:
        return MB_DUMB;
    case ENCH_MAD:
        return MB_MAD;
    case ENCH_INNER_FLAME:
        return MB_INNER_FLAME;
    case ENCH_BREATH_WEAPON:
        return MB_BREATH_WEAPON;
    case ENCH_DEATHS_DOOR:
        return MB_DEATHS_DOOR;
    case ENCH_ROLLING:
        return MB_ROLLING;
    case ENCH_STONESKIN:
        return MB_STONESKIN;
    case ENCH_OZOCUBUS_ARMOUR:
        return MB_OZOCUBUS_ARMOUR;
    case ENCH_WRETCHED:
        return MB_WRETCHED;
    case ENCH_SCREAMED:
        return MB_SCREAMED;
    case ENCH_WORD_OF_RECALL:
        return MB_WORD_OF_RECALL;
    case ENCH_INJURY_BOND:
        return MB_INJURY_BOND;
    case ENCH_WATER_HOLD:
        if (mons.res_water_drowning())
            return MB_WATER_HOLD;
        else
            return MB_WATER_HOLD_DROWN;
    case ENCH_FLAYED:
        return MB_FLAYED;
    case ENCH_WEAK:
        return MB_WEAK;
    case ENCH_DIMENSION_ANCHOR:
        return MB_DIMENSION_ANCHOR;
    case ENCH_CONTROL_WINDS:
        return MB_CONTROL_WINDS;
    case ENCH_TOXIC_RADIANCE:
        return MB_TOXIC_RADIANCE;
    case ENCH_GRASPING_ROOTS:
        return MB_GRASPING_ROOTS;
    case ENCH_FIRE_VULN:
        return MB_FIRE_VULN;
    case ENCH_TORNADO:
        return MB_TORNADO;
    case ENCH_TORNADO_COOLDOWN:
        return MB_TORNADO_COOLDOWN;
    case ENCH_BARBS:
        return MB_BARBS;
    case ENCH_POISON_VULN:
        return MB_POISON_VULN;
    case ENCH_ICEMAIL:
        return MB_ICEMAIL;
    case ENCH_AGILE:
        return MB_AGILE;
    case ENCH_FROZEN:
        return MB_FROZEN;
    case ENCH_BLACK_MARK:
        return MB_BLACK_MARK;
    case ENCH_SAP_MAGIC:
        return MB_SAP_MAGIC;
    case ENCH_SHROUD:
        return MB_SHROUD;
    case ENCH_CORROSION:
        return MB_CORROSION;
    case ENCH_DRAINED:
        {
            const bool heavily_drained = mons.get_ench(ench).degree
                                         >= mons.get_experience_level() / 2;
            return heavily_drained ? MB_HEAVILY_DRAINED : MB_LIGHTLY_DRAINED;
        }
    default:
        return NUM_MB_FLAGS;
    }
}

static bool _blocked_ray(const coord_def &where,
                         dungeon_feature_type* feat = NULL)
{
    if (exists_ray(you.pos(), where, opc_solid_see)
        || !exists_ray(you.pos(), where, opc_default))
    {
        return false;
    }
    if (feat == NULL)
        return true;
    *feat = ray_blocker(you.pos(), where);
    return true;
}

static bool _is_public_key(string key)
{
    if (key == "helpless"
     || key == "wand_known"
     || key == "feat_type"
     || key == "glyph"
     || key == "dbname"
     || key == "monster_tile"
     || key == "tile_num"
     || key == "tile_idx"
     || key == "chimera_part_2"
     || key == "chimera_part_3"
     || key == "chimera_batty"
     || key == "chimera_wings"
     || key == "chimera_legs"
     || key == "custom_spells")
    {
        return true;
    }

    return false;
}

static int quantise(int value, int stepsize)
{
    return value + stepsize - value % stepsize;
}

// Returns true if using a directional tentacle tile would leak
// information the player doesn't have about a tentacle segment's
// current position.
static bool _tentacle_pos_unknown(const monster *tentacle,
                                  const coord_def orig_pos)
{
    // We can see the segment, no guessing necessary.
    if (!tentacle->submerged())
        return false;

    const coord_def t_pos = tentacle->pos();

    // Checks whether there are any positions adjacent to the
    // original tentacle that might also contain the segment.
    for (adjacent_iterator ai(orig_pos); ai; ++ai)
    {
        if (*ai == t_pos)
            continue;

        if (!in_bounds(*ai))
            continue;

        if (you.pos() == *ai)
            continue;

        // If there's an adjacent deep water tile, the segment
        // might be there instead.
        if (grd(*ai) == DNGN_DEEP_WATER)
        {
            const monster *mon = monster_at(*ai);
            if (mon && you.can_see(mon))
            {
                // Could originate from the kraken.
                if (mon->type == MONS_KRAKEN)
                    return true;

                // Otherwise, we know the segment can't be there.
                continue;
            }
            return true;
        }

        if (grd(*ai) == DNGN_SHALLOW_WATER)
        {
            const monster *mon = monster_at(*ai);

            // We know there's no segment there.
            if (!mon)
                continue;

            // Disturbance in shallow water -> might be a tentacle.
            if (mon->type == MONS_KRAKEN || mon->submerged())
                return true;
        }
    }

    // Using a directional tile leaks no information.
    return false;
}

static void _translate_tentacle_ref(monster_info& mi, const monster* m,
                                    const string &key)
{
    if (!m->props.exists(key))
        return;

    const int h_idx = m->props[key].get_int();
    monster *other = NULL; // previous or next segment
    if (h_idx != -1)
    {
        ASSERT(!invalid_monster_index(h_idx));
        other = &menv[h_idx];
        coord_def h_pos = other->pos();
        // If the tentacle and the other segment are no longer adjacent
        // (distortion etc.), just treat them as not connected.
        if (adjacent(m->pos(), h_pos)
            && other->type != MONS_KRAKEN
            && other->type != MONS_ZOMBIE
            && other->type != MONS_SPECTRAL_THING
            && other->type != MONS_SIMULACRUM
            && !_tentacle_pos_unknown(other, m->pos()))
        {
            mi.props[key] = h_pos - m->pos();
        }
    }
}

monster_info::monster_info(monster_type p_type, monster_type p_base_type)
{
    mb.reset();
    attitude = ATT_HOSTILE;
    pos = coord_def(0, 0);

    type = p_type;
    base_type = p_base_type;

    draco_type = mons_genus(type) == MONS_DRACONIAN  ? MONS_DRACONIAN :
                 mons_genus(type) == MONS_DEMONSPAWN ? MONS_DEMONSPAWN
                                                     : type;

    number = 0;
    colour = mons_class_colour(type);

    holi = mons_class_holiness(type);

    mintel = mons_class_intel(type);

    mresists = get_mons_class_resists(type);

    mitemuse = mons_class_itemuse(type);

    mbase_speed = mons_class_base_speed(type);
    menergy = mons_class_energy(type);

    fly = max(mons_class_flies(type), mons_class_flies(base_type));

    if (mons_class_wields_two_weapons(type)
        || mons_class_wields_two_weapons(base_type))
    {
        mb.set(MB_TWO_WEAPONS);
    }

    if (!mons_class_can_regenerate(type)
        || !mons_class_can_regenerate(base_type))
    {
        mb.set(MB_NO_REGEN);
    }

    threat = MTHRT_UNDEF;

    dam = MDAM_OKAY;

    fire_blocker = DNGN_UNSEEN;

    u.ghost.acting_part = MONS_0;
    if (mons_is_pghost(type))
    {
        u.ghost.species = SP_HUMAN;
        u.ghost.job = JOB_WANDERER;
        u.ghost.religion = GOD_NO_GOD;
        u.ghost.best_skill = SK_FIGHTING;
        u.ghost.best_skill_rank = 2;
        u.ghost.xl_rank = 3;
        u.ghost.ac = 5;
        u.ghost.damage = 5;
    }

    if (base_type == MONS_NO_MONSTER)
        base_type = type;

    if (mons_is_unique(type))
    {
        if (type == MONS_LERNAEAN_HYDRA
            || type == MONS_ROYAL_JELLY
            || mons_species(type) == MONS_SERPENT_OF_HELL)
        {
            mb.set(MB_NAME_THE);
        }
        else
        {
            mb.set(MB_NAME_UNQUALIFIED);
            mb.set(MB_NAME_THE);
        }
    }

    for (int i = 0; i < MAX_NUM_ATTACKS; ++i)
    {
        attack[i] = get_monster_data(type)->attack[i];
        attack[i].damage = 0;
    }

    props.clear();

    client_id = 0;
}

monster_info::monster_info(const monster* m, int milev)
{
    mb.reset();
    attitude = ATT_HOSTILE;
    pos = m->pos();

    attitude = mons_attitude(m);

    bool nomsg_wounds = false;

    type = m->type;
    threat = mons_threat_level(m);

    props.clear();
    if (!m->props.empty())
    {
        CrawlHashTable::hash_map_type::const_iterator i = m->props.begin();
        for (; i != m->props.end(); ++i)
            if (_is_public_key(i->first))
                props[i->first] = i->second;
    }

    // Translate references to tentacles into just their locations
    if (mons_is_tentacle_or_tentacle_segment(type))
    {
        _translate_tentacle_ref(*this, m, "inwards");
        _translate_tentacle_ref(*this, m, "outwards");
    }

    draco_type =
        (mons_genus(type) == MONS_DRACONIAN
        || mons_genus(type) == MONS_DEMONSPAWN)
            ? ::draco_or_demonspawn_subspecies(m)
            : type;

    if (!mons_can_display_wounds(m)
        || !mons_class_can_display_wounds(type))
    {
        nomsg_wounds = true;
    }

    base_type = m->base_monster;
    if (base_type == MONS_NO_MONSTER)
        base_type = type;

    // these use number for internal information
    if (type == MONS_SIXFIRHY
        || type == MONS_JIANGSHI
        || type == MONS_KRAKEN_TENTACLE
        || type == MONS_KRAKEN_TENTACLE_SEGMENT
        || type == MONS_ELDRITCH_TENTACLE_SEGMENT
        || type == MONS_STARSPAWN_TENTACLE
        || type == MONS_STARSPAWN_TENTACLE_SEGMENT)
    {
        number = 0;
    }
    else
        number = m->number;
    colour = m->colour;

    int stype = 0;
    if (m->is_summoned(0, &stype)
        && !m->has_ench(ENCH_PHANTOM_MIRROR))
    {
        mb.set(MB_SUMMONED);
        if (stype > 0 && stype < NUM_SPELLS
            && summons_are_capped(static_cast<spell_type>(stype)))
        {
            mb.set(MB_SUMMONED_NO_STAIRS);
        }
    }
    else if (m->is_perm_summoned())
        mb.set(MB_PERM_SUMMON);

    if (m->has_ench(ENCH_SUMMON_CAPPED))
        mb.set(MB_SUMMONED_CAPPED);

    if (mons_is_unique(type))
    {
        if (type == MONS_LERNAEAN_HYDRA
            || type == MONS_ROYAL_JELLY
            || mons_species(type) == MONS_SERPENT_OF_HELL)
        {
            mb.set(MB_NAME_THE);
        }
        else
        {
            mb.set(MB_NAME_UNQUALIFIED);
            mb.set(MB_NAME_THE);
        }
    }

    mname = m->mname;

    const uint64_t name_flags = m->flags & MF_NAME_MASK;

    if (name_flags == MF_NAME_SUFFIX)
        mb.set(MB_NAME_SUFFIX);
    else if (name_flags == MF_NAME_ADJECTIVE)
        mb.set(MB_NAME_ADJECTIVE);
    else if (name_flags == MF_NAME_REPLACE)
        mb.set(MB_NAME_REPLACE);

    const bool need_name_desc =
        name_flags == MF_NAME_SUFFIX
            || name_flags == MF_NAME_ADJECTIVE
            || (m->flags & MF_NAME_DEFINITE);

    if (!mname.empty()
        && !(m->flags & MF_NAME_DESCRIPTOR)
        && !need_name_desc)
    {
        mb.set(MB_NAME_UNQUALIFIED);
        mb.set(MB_NAME_THE);
    }
    else if (m->flags & MF_NAME_DEFINITE)
        mb.set(MB_NAME_THE);
    if (m->flags & MF_NAME_ZOMBIE)
        mb.set(MB_NAME_ZOMBIE);
    if (m->flags & MF_NAME_SPECIES)
        mb.set(MB_NO_NAME_TAG);

    // Chimera acting head needed for name
    u.ghost.acting_part = MONS_0;
    if (mons_class_is_chimeric(type))
    {
        ASSERT(m->ghost.get());
        ghost_demon& ghost = *m->ghost;
        u.ghost.acting_part = ghost.acting_part;
    }

    if (milev <= MILEV_NAME)
    {
        if (type == MONS_DANCING_WEAPON
            && m->inv[MSLOT_WEAPON] != NON_ITEM)
        {
            inv[MSLOT_WEAPON].reset(
                new item_def(get_item_info(mitm[m->inv[MSLOT_WEAPON]])));
        }
        if (mons_is_item_mimic(type))
        {
            ASSERT(m->inv[MSLOT_MISCELLANY] != NON_ITEM);
            inv[MSLOT_MISCELLANY].reset(
                new item_def(get_item_info(mitm[m->inv[MSLOT_MISCELLANY]])));
        }
        return;
    }

    holi = m->holiness();

    mintel = mons_intel(m);
    mresists = get_mons_resists(m);
    mitemuse = mons_itemuse(m);
    mbase_speed = mons_base_speed(m);
    menergy = mons_class_energy(m->type);
    fly = mons_flies(m);

    if (mons_wields_two_weapons(m))
        mb.set(MB_TWO_WEAPONS);
    if (!mons_can_regenerate(m))
        mb.set(MB_NO_REGEN);
    if (m->haloed() && !m->umbraed())
        mb.set(MB_HALOED);
    if (!m->haloed() && m->umbraed())
        mb.set(MB_UMBRAED);
    if (mons_looks_stabbable(m))
        mb.set(MB_STABBABLE);
    if (mons_looks_distracted(m))
        mb.set(MB_DISTRACTED);
    if (m->liquefied_ground())
        mb.set(MB_SLOW_MOVEMENT);
    if (m->is_wall_clinging())
        mb.set(MB_CLINGING);

    dam = mons_get_damage_level(m);

    // If no messages about wounds, don't display damage level either.
    if (nomsg_wounds)
        dam = MDAM_OKAY;

    if (!mons_class_flag(m->type, M_NO_EXP_GAIN)) // Firewood, butterflies, etc.
    {
        if (m->asleep())
        {
            if (!m->can_hibernate(true))
                mb.set(MB_DORMANT);
            else
                mb.set(MB_SLEEPING);
        }
        // Applies to both friendlies and hostiles
        else if (mons_is_fleeing(m))
            mb.set(MB_FLEEING);
        else if (mons_is_wandering(m) && !mons_is_batty(m))
        {
            if (m->is_stationary())
                mb.set(MB_UNAWARE);
            else
                mb.set(MB_WANDERING);
        }
        // TODO: is this ever needed?
        else if (m->foe == MHITNOT && !mons_is_batty(m)
                 && m->attitude == ATT_HOSTILE)
        {
            mb.set(MB_UNAWARE);
        }
    }

    for (mon_enchant_list::const_iterator e = m->enchantments.begin();
         e != m->enchantments.end(); ++e)
    {
        monster_info_flags flag = ench_to_mb(*m, e->first);
        if (flag != NUM_MB_FLAGS)
            mb.set(flag);
    }

    // fake enchantment (permanent)
    if (mons_class_flag(type, M_DEFLECT_MISSILES))
        mb.set(MB_DEFLECT_MSL);

    if (type == MONS_SILENT_SPECTRE)
        mb.set(MB_SILENCING);

    if (you.beheld_by(m))
        mb.set(MB_MESMERIZING);

    // Evilness of attacking
    switch (attitude)
    {
    case ATT_NEUTRAL:
    case ATT_HOSTILE:
        if (you_worship(GOD_SHINING_ONE)
            && !tso_unchivalric_attack_safe_monster(m)
            && find_stab_type(&you, m) != STAB_NO_STAB)
        {
            mb.set(MB_EVIL_ATTACK);
        }
        break;
    default:
        break;
    }

    if (testbits(m->flags, MF_ENSLAVED_SOUL))
        mb.set(MB_ENSLAVED);

    if (m->is_shapeshifter() && (m->flags & MF_KNOWN_SHIFTER))
        mb.set(MB_SHAPESHIFTER);

    if (m->known_chaos())
        mb.set(MB_CHAOTIC);

    if (m->submerged())
        mb.set(MB_SUBMERGED);

    if (testbits(m->flags, MF_SPECTRALISED))
        mb.set(MB_SPECTRALISED);

    if (mons_is_pghost(type))
    {
        ASSERT(m->ghost.get());
        ghost_demon& ghost = *m->ghost;
        u.ghost.species = ghost.species;
        if (species_genus(u.ghost.species) == GENPC_DRACONIAN && ghost.xl < 7)
            u.ghost.species = SP_BASE_DRACONIAN;
        u.ghost.job = ghost.job;
        u.ghost.religion = ghost.religion;
        u.ghost.best_skill = ghost.best_skill;
        u.ghost.best_skill_rank = get_skill_rank(ghost.best_skill_level);
        u.ghost.xl_rank = ghost_level_to_rank(ghost.xl);
        u.ghost.ac = quantise(ghost.ac, 5);
        u.ghost.damage = quantise(ghost.damage, 5);
        u.ghost.can_sinv = ghost.see_invis;
    }

    // book loading for player ghost and vault monsters
    spells.init(SPELL_NO_SPELL);
    if (m->props.exists("custom_spells") || mons_is_pghost(type))
    {
        for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i)
            spells[i] = m->spells[i];
        // XXX handle here special cases for sources of magic (magic, divine, other)
        if (m->is_priest())
            this->props["priest"] = true;
        else if (m->is_actual_spellcaster())
            this->props["actual_spellcaster"] = true;
    }

    for (int i = 0; i < MAX_NUM_ATTACKS; ++i)
    {
        attack[i] = mons_attack_spec(m, i, true);
        attack[i].damage = 0;
    }

    for (unsigned i = 0; i <= MSLOT_LAST_VISIBLE_SLOT; ++i)
    {
        bool ok;
        if (m->inv[i] == NON_ITEM)
            ok = false;
        else if (i == MSLOT_MISCELLANY)
            ok = mons_is_mimic(type);
        else if (attitude == ATT_FRIENDLY)
            ok = true;
        else if (i == MSLOT_WAND)
            ok = props.exists("wand_known") && props["wand_known"];
        else if (m->props.exists("ash_id") && item_type_known(mitm[m->inv[i]]))
            ok = true;
        else if (i == MSLOT_ALT_WEAPON)
            ok = wields_two_weapons();
        else if (i == MSLOT_MISSILE)
            ok = false;
        else
            ok = true;
        if (ok)
            inv[i].reset(new item_def(get_item_info(mitm[m->inv[i]])));
    }

    fire_blocker = DNGN_UNSEEN;
    if (!crawl_state.arena_suspended
        && m->pos() != you.pos())
    {
        _blocked_ray(m->pos(), &fire_blocker);
    }

    if (m->props.exists("quote"))
        quote = m->props["quote"].get_string();

    if (m->props.exists("description"))
        description = m->props["description"].get_string();

    // init names of constrictor and constrictees
    constrictor_name = "";
    constricting_name.clear();

    // name of what this monster is constricted by, if any
    if (m->is_constricted())
    {
        actor * const constrictor = actor_by_mid(m->constricted_by);
        if (constrictor)
        {
            constrictor_name = (m->held == HELD_MONSTER ? "held by "
                                                        : "constricted by ")
                               + constrictor->name(DESC_A, true);
        }
    }

    // names of what this monster is constricting, if any
    if (m->constricting)
    {
        actor::constricting_t::const_iterator i;
        for (i = m->constricting->begin(); i != m->constricting->end(); ++i)
        {
            actor* const constrictee = actor_by_mid(i->first);

            if (constrictee)
            {
                constricting_name.push_back(
                                (m->constriction_damage() ? "constricting "
                                                          : "holding ")
                                + constrictee->name(DESC_A, true));
            }
        }
    }

    if (mons_has_known_ranged_attack(m))
        mb.set(MB_RANGED_ATTACK);

    // this must be last because it provides this structure to Lua code
    if (milev > MILEV_SKIP_SAFE)
    {
        if (mons_is_safe(m))
            mb.set(MB_SAFE);
        else
            mb.set(MB_UNSAFE);
        if (mons_is_firewood(m))
            mb.set(MB_FIREWOOD);
    }

    client_id = m->get_client_id();
}

string monster_info::db_name() const
{
    if (type == MONS_DANCING_WEAPON && inv[MSLOT_WEAPON].get())
    {
        iflags_t ignore_flags = ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES;
        bool     use_inscrip  = false;
        return inv[MSLOT_WEAPON]->name(DESC_DBNAME, false, false, use_inscrip, false,
                         ignore_flags);
    }

    if (type == MONS_SENSED)
        return get_monster_data(base_type)->name;

    return get_monster_data(type)->name;
}

string monster_info::_core_name() const
{
    monster_type nametype = type;

    switch (type)
    {
    case MONS_ZOMBIE:
    case MONS_SKELETON:
    case MONS_SIMULACRUM:
#if TAG_MAJOR_VERSION == 34
    case MONS_ZOMBIE_SMALL:     case MONS_ZOMBIE_LARGE:
    case MONS_SKELETON_SMALL:   case MONS_SKELETON_LARGE:
    case MONS_SIMULACRUM_SMALL: case MONS_SIMULACRUM_LARGE:
#endif
    case MONS_SPECTRAL_THING:   case MONS_PILLAR_OF_SALT:
    case MONS_BLOCK_OF_ICE:     case MONS_CHIMERA:
    case MONS_SENSED:
        nametype = base_type;
        break;

    default:
        break;
    }

    string s;

    if (is(MB_NAME_REPLACE))
        s = mname;
    else if (nametype == MONS_LERNAEAN_HYDRA)
        s = "Lernaean hydra"; // TODO: put this into mon-data.h
    else if (nametype == MONS_ROYAL_JELLY)
        s = "royal jelly";
    else if (mons_species(nametype) == MONS_SERPENT_OF_HELL)
        s = "Serpent of Hell";
    else if (mons_is_mimic(nametype))
        s = mimic_name();
    else if (invalid_monster_type(nametype) && nametype != MONS_PROGRAM_BUG)
        s = "INVALID MONSTER";
    else
    {
        const char* slime_sizes[] = {"buggy ", "", "large ", "very large ",
                                               "enormous ", "titanic "};
        s = get_monster_data(nametype)->name;

        switch (type)
        {
        case MONS_SLIME_CREATURE:
            ASSERT(number <= 5);
            s = slime_sizes[number] + s;
            break;
        case MONS_UGLY_THING:
        case MONS_VERY_UGLY_THING:
            s = ugly_thing_colour_name(colour) + " " + s;
            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 (base_type != MONS_NO_MONSTER)
                s = draconian_colour_name(base_type) + " " + s;
            break;

        case MONS_BLOOD_SAINT:
        case MONS_CHAOS_CHAMPION:
        case MONS_WARMONGER:
        case MONS_CORRUPTER:
        case MONS_BLACK_SUN:
            if (base_type != MONS_NO_MONSTER)
                s = demonspawn_base_name(base_type) + " " + s;
            break;

        case MONS_DANCING_WEAPON:
        case MONS_SPECTRAL_WEAPON:
            if (inv[MSLOT_WEAPON].get())
            {
                iflags_t ignore_flags = ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES;
                bool     use_inscrip  = true;
                const item_def& item = *inv[MSLOT_WEAPON];
                s = type==MONS_SPECTRAL_WEAPON ? "spectral " : "";
                s += (item.name(DESC_PLAIN, false, false, use_inscrip, false,
                                ignore_flags));
            }
            break;

        case MONS_PLAYER_GHOST:
            s = apostrophise(mname) + " ghost";
            break;
        case MONS_PLAYER_ILLUSION:
            s = apostrophise(mname) + " illusion";
            break;
        case MONS_PANDEMONIUM_LORD:
            s = mname;
            break;
        default:
            break;
        }
    }

    if (is(MB_NAME_SUFFIX))
        s += " " + mname;
    else if (is(MB_NAME_ADJECTIVE))
        s = mname + " " + s;

    return s;
}

string monster_info::_apply_adjusted_description(description_level_type desc,
                                                 const string& s) const
{
    if (desc == DESC_ITS)
        desc = DESC_THE;

    if (is(MB_NAME_THE) && desc == DESC_A)
        desc = DESC_THE;

    if (attitude == ATT_FRIENDLY && desc == DESC_THE)
        desc = DESC_YOUR;

    return apply_description(desc, s);
}

string monster_info::common_name(description_level_type desc) const
{
    const string core = _core_name();
    const bool nocore = mons_class_is_zombified(type)
                        && mons_is_unique(base_type)
                        && base_type == mons_species(base_type)
                        || mons_class_is_chimeric(type);

    ostringstream ss;

    if (props.exists("helpless"))
        ss << "helpless ";

    if (is(MB_SUBMERGED))
        ss << "submerged ";

    if (type == MONS_SPECTRAL_THING && !is(MB_NAME_ZOMBIE) && !nocore)
        ss << "spectral ";

    if (type == MONS_SENSED && !mons_is_sensed(base_type))
        ss << "sensed ";

    if (type == MONS_BALLISTOMYCETE)
        ss << (number ? "active " : "");

    if ((mons_genus(type) == MONS_HYDRA || mons_genus(base_type) == MONS_HYDRA)
        && number > 0)
    {
        if (number < 11)
        {
            const char* cardinals[] = {"one", "two", "three", "four", "five",
                                       "six", "seven", "eight", "nine", "ten"};
            ss << cardinals[number - 1];
        }
        else
            ss << make_stringf("%d", number);

        ss << "-headed ";
    }

    if (mons_class_is_chimeric(type))
    {
        ss << "chimera";
        monsterentry *me = NULL;
        if (u.ghost.acting_part != MONS_0
            && (me = get_monster_data(u.ghost.acting_part)))
        {
            // Specify an acting head
            ss << "'s " << me->name << " head";
        }
        else
            // Suffix parts in brackets
            // XXX: Should have a desc level that disables this
            ss << " (" << core << chimera_part_names() << ")";
    }

    if (!nocore)
        ss << core;

    // Add suffixes.
    switch (type)
    {
    case MONS_ZOMBIE:
#if TAG_MAJOR_VERSION == 34
    case MONS_ZOMBIE_SMALL:
    case MONS_ZOMBIE_LARGE:
#endif
        if (!is(MB_NAME_ZOMBIE))
            ss << (nocore ? "" : " ") << "zombie";
        break;
    case MONS_SKELETON:
#if TAG_MAJOR_VERSION == 34
    case MONS_SKELETON_SMALL:
    case MONS_SKELETON_LARGE:
#endif
        if (!is(MB_NAME_ZOMBIE))
            ss << (nocore ? "" : " ") << "skeleton";
        break;
    case MONS_SIMULACRUM:
#if TAG_MAJOR_VERSION == 34
    case MONS_SIMULACRUM_SMALL:
    case MONS_SIMULACRUM_LARGE:
#endif
        if (!is(MB_NAME_ZOMBIE))
            ss << (nocore ? "" : " ") << "simulacrum";
        break;
    case MONS_SPECTRAL_THING:
        if (nocore)
            ss << "spectre";
        break;
    case MONS_PILLAR_OF_SALT:
        ss << (nocore ? "" : " ") << "shaped pillar of salt";
        break;
    case MONS_BLOCK_OF_ICE:
        ss << (nocore ? "" : " ") << "shaped block of ice";
        break;
    default:
        break;
    }

    if (is(MB_SHAPESHIFTER))
    {
        // If momentarily in original form, don't display "shaped
        // shifter".
        if (mons_genus(type) != MONS_SHAPESHIFTER)
            ss << " shaped shifter";
    }

    string s;
    // only respect unqualified if nothing was added ("Sigmund" or "The spectral Sigmund")
    if (!is(MB_NAME_UNQUALIFIED) || has_proper_name() || ss.str() != core)
        s = _apply_adjusted_description(desc, ss.str());
    else
        s = ss.str();

    if (desc == DESC_ITS)
        s = apostrophise(s);

    return s;
}

dungeon_feature_type monster_info::get_mimic_feature() const
{
    if (!props.exists("feat_type"))
        return DNGN_UNSEEN;
    return static_cast<dungeon_feature_type>(props["feat_type"].get_short());
}

const item_def* monster_info::get_mimic_item() const
{
    ASSERT(mons_is_item_mimic(type));

    return inv[MSLOT_MISCELLANY].get();
}

string monster_info::mimic_name() const
{
    string s;
    if (type == MONS_INEPT_ITEM_MIMIC || type == MONS_INEPT_FEATURE_MIMIC)
        s = "inept ";
    if (type == MONS_RAVENOUS_ITEM_MIMIC || type == MONS_RAVENOUS_FEATURE_MIMIC)
        s = "ravenous ";
#if TAG_MAJOR_VERSION == 34
    if (type == MONS_MONSTROUS_ITEM_MIMIC)
        s = "monstrous ";
#endif

    if (props.exists("feat_type"))
        s += feat_type_name(get_mimic_feature());
    else if (item_def* item = inv[MSLOT_MISCELLANY].get())
    {
        if (item->base_type == OBJ_GOLD)
            s += "pile of gold";
        else if (item->base_type == OBJ_MISCELLANY
                 && item->sub_type == MISC_RUNE_OF_ZOT)
        {
            s += "rune";
        }
        else if (item->base_type == OBJ_ORBS)
            s += "orb";
        else
            s += item->name(DESC_DBNAME);
    }

    if (!s.empty())
        s += " ";

    return s + "mimic";
}

bool monster_info::has_proper_name() const
{
    return !mname.empty() && !mons_is_ghost_demon(type)
            && !is(MB_NAME_REPLACE) && !is(MB_NAME_ADJECTIVE) && !is(MB_NAME_SUFFIX);
}

string monster_info::proper_name(description_level_type desc) const
{
    if (has_proper_name())
    {
        if (desc == DESC_ITS)
            return apostrophise(mname);
        else
            return mname;
    }
    else
        return common_name(desc);
}

string monster_info::full_name(description_level_type desc, bool use_comma) const
{
    if (desc == DESC_NONE)
        return "";

    if (has_proper_name())
    {
        string s = mname + (use_comma ? ", the " : " the ") + common_name();
        if (desc == DESC_ITS)
            s = apostrophise(s);
        return s;
    }
    else
        return common_name(desc);
}

// Needed because gcc 4.3 sort does not like comparison functions that take
// more than 2 arguments.
bool monster_info::less_than_wrapper(const monster_info& m1,
                                     const monster_info& m2)
{
    return monster_info::less_than(m1, m2, true);
}

// Sort monsters by (in that order):    attitude, difficulty, type, brand
bool monster_info::less_than(const monster_info& m1, const monster_info& m2,
                             bool zombified, bool fullname)
{
    if (m1.attitude < m2.attitude)
        return true;
    else if (m1.attitude > m2.attitude)
        return false;

    // Force plain but different coloured draconians to be treated like the
    // same sub-type.
    if (!zombified
        && mons_is_base_draconian(m1.type)
        && mons_is_base_draconian(m2.type))
    {
        return false;
    }

    // Treat base demonspawn identically, as with draconians.
    if (!zombified && m1.type >= MONS_FIRST_BASE_DEMONSPAWN
        && m1.type <= MONS_LAST_BASE_DEMONSPAWN
        && m2.type >= MONS_FIRST_BASE_DEMONSPAWN
        && m2.type <= MONS_LAST_BASE_DEMONSPAWN)
    {
        return false;
    }

    int diff_delta = mons_avg_hp(m1.type) - mons_avg_hp(m2.type);

    // By descending difficulty
    if (diff_delta > 0)
        return true;
    else if (diff_delta < 0)
        return false;

    // Force mimics of different types to be treated like the same one.
    if (mons_is_mimic(m1.type) && mons_is_mimic(m2.type))
        return false;

    if (m1.type < m2.type)
        return true;
    else if (m1.type > m2.type)
        return false;

    // Never distinguish between dancing weapons.
    // The above checks guarantee that *both* monsters are of this type.
    if (m1.type == MONS_DANCING_WEAPON)
        return false;

    if (m1.type == MONS_SLIME_CREATURE)
        return m1.number > m2.number;

    if (m1.type == MONS_BALLISTOMYCETE)
        return m1.number > 0 && m2.number <= 0;

    // Shifters after real monsters of the same type.
    if (m1.is(MB_SHAPESHIFTER) != m2.is(MB_SHAPESHIFTER))
        return m2.is(MB_SHAPESHIFTER);

    if (zombified)
    {
        if (mons_class_is_zombified(m1.type))
        {
            // Because of the type checks above, if one of the two is zombified, so
            // is the other, and of the same type.
            if (m1.base_type < m2.base_type)
                return true;
            else if (m1.base_type > m2.base_type)
                return false;
        }

        if (m1.type == MONS_CHIMERA)
        {
            for (int part = 1; part <= 3; part++)
            {
                const monster_type p1 = get_chimera_part(&m1, part);
                const monster_type p2 = get_chimera_part(&m2, part);

                if (p1 < p2)
                    return true;
                else if (p1 > p2)
                    return false;
            }
        }

        // Both monsters are hydras or hydra zombies, sort by number of heads.
        if (mons_genus(m1.type) == MONS_HYDRA || mons_genus(m1.base_type) == MONS_HYDRA)
        {
            if (m1.number > m2.number)
                return true;
            else if (m1.number < m2.number)
                return false;
        }
    }

    if (fullname || mons_is_pghost(m1.type))
        return m1.mname < m2.mname;

#if 0 // for now, sort mb together.
    // By descending mb, so no mb sorts to the end
    if (m1.mb > m2.mb)
        return true;
    else if (m1.mb < m2.mb)
        return false;
#endif

    return false;
}

static string _verbose_info0(const monster_info& mi)
{
    if (mi.is(MB_BERSERK))
        return "berserk";
    if (mi.is(MB_INSANE))
        return "insane";
    if (mi.is(MB_FRENZIED))
        return "frenzied";
    if (mi.is(MB_ROUSED))
        return "roused";
    if (mi.is(MB_INNER_FLAME))
        return "inner flame";
    if (mi.is(MB_DUMB))
        return "dumb";
    if (mi.is(MB_PARALYSED))
        return "paralysed";
    if (mi.is(MB_CAUGHT))
        return "caught";
    if (mi.is(MB_WEBBED))
        return "webbed";
    if (mi.is(MB_PETRIFIED))
        return "petrified";
    if (mi.is(MB_PETRIFYING))
        return "petrifying";
    if (mi.is(MB_MAD))
        return "mad";
    if (mi.is(MB_CONFUSED))
        return "confused";
    if (mi.is(MB_FLEEING))
        return "fleeing";
    if (mi.is(MB_DORMANT))
        return "dormant";
    if (mi.is(MB_SLEEPING))
        return "sleeping";
    if (mi.is(MB_UNAWARE))
        return "unaware";
    if (mi.is(MB_WITHDRAWN))
        return "withdrawn";
    if (mi.is(MB_DAZED))
        return "dazed";
    if (mi.is(MB_MUTE))
        return "mute";
    if (mi.is(MB_BLIND))
        return "blind";
    // avoid jelly (wandering) (fellow slime)
    if (mi.is(MB_WANDERING) && mi.attitude != ATT_STRICT_NEUTRAL)
        return "wandering";
    if (mi.is(MB_BURNING))
        return "burning";
    if (mi.is(MB_ROTTING))
        return "rotting";
    if (mi.is(MB_BLEEDING))
        return "bleeding";
    if (mi.is(MB_INVISIBLE))
        return "invisible";

    return "";
}

static string _verbose_info(const monster_info& mi)
{
    string inf = _verbose_info0(mi);
    if (!inf.empty())
        inf = " (" + inf + ")";
    return inf;
}

string monster_info::pluralised_name(bool fullname) const
{
    // Don't pluralise uniques, ever.  Multiple copies of the same unique
    // are unlikely in the dungeon currently, but quite common in the
    // arena.  This prevens "4 Gra", etc. {due}
    // Unless it's Mara, who summons illusions of himself.
    if (mons_is_unique(type) && type != MONS_MARA)
        return common_name();
    // Specialcase mimics, so they don't get described as piles of gold
    // when that would be inappropriate. (HACK)
    else if (mons_is_mimic(type))
        return "mimics";
    else if (mons_genus(type) == MONS_DRACONIAN)
        return pluralise(mons_type_name(MONS_DRACONIAN, DESC_PLAIN));
    else if (mons_genus(type) == MONS_DEMONSPAWN)
        return pluralise(mons_type_name(MONS_DEMONSPAWN, DESC_PLAIN));
    else if (type == MONS_UGLY_THING || type == MONS_VERY_UGLY_THING
             || type == MONS_DANCING_WEAPON || !fullname)
    {
        return pluralise(mons_type_name(type, DESC_PLAIN));
    }
    else
        return pluralise(common_name());
}

enum _monster_list_colour_type
{
    _MLC_FRIENDLY, _MLC_NEUTRAL, _MLC_GOOD_NEUTRAL, _MLC_STRICT_NEUTRAL,
    _MLC_TRIVIAL, _MLC_EASY, _MLC_TOUGH, _MLC_NASTY,
    _NUM_MLC
};

static const char * const _monster_list_colour_names[_NUM_MLC] =
{
    "friendly", "neutral", "good_neutral", "strict_neutral",
    "trivial", "easy", "tough", "nasty"
};

static int _monster_list_colours[_NUM_MLC] =
{
    GREEN, BROWN, BROWN, BROWN,
    DARKGREY, LIGHTGREY, YELLOW, LIGHTRED,
};

bool set_monster_list_colour(string key, int colour)
{
    for (int i = 0; i < _NUM_MLC; ++i)
    {
        if (key == _monster_list_colour_names[i])
        {
            _monster_list_colours[i] = colour;
            return true;
        }
    }
    return false;
}

void clear_monster_list_colours()
{
    for (int i = 0; i < _NUM_MLC; ++i)
        _monster_list_colours[i] = -1;
}

void monster_info::to_string(int count, string& desc, int& desc_colour,
                             bool fullname, const char *adj) const
{
    ostringstream out;
    _monster_list_colour_type colour_type = _NUM_MLC;

    string full = count == 1 ? full_name() : pluralised_name(fullname);

    if (adj && starts_with(full, "the "))
        full.erase(0, 4);

    // TODO: this should be done in a much cleaner way, with code to
    // merge multiple monster_infos into a single common structure
    if (count != 1)
        out << count << " ";
    if (adj)
        out << adj << " ";
    out << full;

#ifdef DEBUG_DIAGNOSTICS
    out << " av" << mons_avg_hp(type);
#endif

    if (count == 1)
       out << _verbose_info(*this);

    // Friendliness
    switch (attitude)
    {
    case ATT_FRIENDLY:
        //out << " (friendly)";
        colour_type = _MLC_FRIENDLY;
        break;
    case ATT_GOOD_NEUTRAL:
        //out << " (neutral)";
        colour_type = _MLC_GOOD_NEUTRAL;
        break;
    case ATT_NEUTRAL:
        //out << " (neutral)";
        colour_type = _MLC_NEUTRAL;
        break;
    case ATT_STRICT_NEUTRAL:
         out << " (fellow slime)";
         colour_type = _MLC_STRICT_NEUTRAL;
         break;
    case ATT_HOSTILE:
        // out << " (hostile)";
        switch (threat)
        {
        case MTHRT_TRIVIAL: colour_type = _MLC_TRIVIAL; break;
        case MTHRT_EASY:    colour_type = _MLC_EASY;    break;
        case MTHRT_TOUGH:   colour_type = _MLC_TOUGH;   break;
        case MTHRT_NASTY:   colour_type = _MLC_NASTY;   break;
        default:;
        }
        break;
    }

    if (count == 1 && is(MB_EVIL_ATTACK))
        desc_colour = Options.evil_colour;
    else if (colour_type < _NUM_MLC)
        desc_colour = _monster_list_colours[colour_type];

    // We still need something, or we'd get the last entry's colour.
    if (desc_colour < 0)
        desc_colour = LIGHTGREY;

    desc = out.str();
}

vector<string> monster_info::attributes() const
{
    vector<string> v;

    if (is(MB_BERSERK))
        v.push_back("berserk");
    if (is(MB_HASTED) || is(MB_BERSERK))
    {
        if (!is(MB_SLOWED))
            v.push_back("fast");
        else
            v.push_back("fast+slow");
    }
    else if (is(MB_SLOWED))
        v.push_back("slow");
    if (is(MB_STRONG) || is(MB_BERSERK))
        v.push_back("unusually strong");

    if (is(MB_POISONED))
        v.push_back("poisoned");
    if (is(MB_SICK))
        v.push_back("sick");
    if (is(MB_ROTTING))
        v.push_back("rotting away"); //jmf: "covered in sores"?
    if (is(MB_GLOWING))
        v.push_back("softly glowing");
    if (is(MB_INSANE))
        v.push_back("frenzied and insane");
    if (is(MB_FRENZIED))
        v.push_back("consumed by blood-lust");
    if (is(MB_ROUSED))
        v.push_back("inspired to greatness");
    if (is(MB_CONFUSED))
        v.push_back("bewildered and confused");
    if (is(MB_INVISIBLE))
        v.push_back("slightly transparent");
    if (is(MB_CHARMED))
        v.push_back("in your thrall");
    if (is(MB_BURNING))
        v.push_back("covered in liquid flames");
    if (is(MB_CAUGHT))
        v.push_back("entangled in a net");
    if (is(MB_WEBBED))
        v.push_back("entangled in a web");
    if (is(MB_PETRIFIED))
        v.push_back("petrified");
    if (is(MB_PETRIFYING))
        v.push_back("slowly petrifying");
    if (is(MB_VULN_MAGIC))
        v.push_back("susceptible to magic");
    if (is(MB_SWIFT))
        v.push_back("covering ground quickly");
    if (is(MB_SILENCING))
        v.push_back("radiating silence");
    if (is(MB_PARALYSED))
        v.push_back("paralysed");
    if (is(MB_BLEEDING))
        v.push_back("bleeding");
    if (is(MB_DEFLECT_MSL))
        v.push_back("deflecting missiles");
    if (is(MB_FEAR_INSPIRING))
        v.push_back("inspiring fear");
    if (is(MB_BREATH_WEAPON))
    {
        v.push_back(string("catching ")
                    + pronoun(PRONOUN_POSSESSIVE) + " breath");
    }
    if (is(MB_WITHDRAWN))
    {
        v.push_back("regenerating health quickly");
        v.push_back(string("protected by ")
                    + pronoun(PRONOUN_POSSESSIVE) + " shell");
    }
    if (is(MB_DAZED))
        v.push_back("dazed");
    if (is(MB_MUTE))
        v.push_back("permanently mute");
    if (is(MB_BLIND))
        v.push_back("blind");
    if (is(MB_DUMB))
        v.push_back("stupefied");
    if (is(MB_MAD))
        v.push_back("lost in madness");
    if (is(MB_DEATHS_DOOR))
        v.push_back("standing in death's doorway");
    if (is(MB_REGENERATION))
        v.push_back("regenerating");
    if (is(MB_ROLLING))
        v.push_back("rolling");
    if (is(MB_STONESKIN))
        v.push_back("stone skin");
    if (is(MB_OZOCUBUS_ARMOUR))
        v.push_back("covered in an icy film");
    if (is(MB_WRETCHED))
        v.push_back("misshapen and mutated");
    if (is(MB_WORD_OF_RECALL))
        v.push_back("chanting recall");
    if (is(MB_INJURY_BOND))
        v.push_back("sheltered from injuries");
    if (is(MB_WATER_HOLD))
        v.push_back("engulfed in water");
    if (is(MB_WATER_HOLD_DROWN))
    {
        v.push_back("engulfed in water");
        v.push_back("unable to breathe");
    }
    if (is(MB_FLAYED))
        v.push_back("covered in terrible wounds");
    if (is(MB_WEAK))
        v.push_back("weak");
    if (is(MB_DIMENSION_ANCHOR))
        v.push_back("unable to translocate");
    if (is(MB_CONTROL_WINDS))
        v.push_back("controlling the winds");
    if (is(MB_TOXIC_RADIANCE))
        v.push_back("radiating toxic energy");
    if (is(MB_GRASPING_ROOTS))
        v.push_back("movement impaired by roots");
    if (is(MB_FIRE_VULN))
        v.push_back("more vulnerable to fire");
    if (is(MB_TORNADO))
        v.push_back("surrounded by raging winds");
    if (is(MB_TORNADO_COOLDOWN))
        v.push_back("surrounded by restless winds");
    if (is(MB_BARBS))
        v.push_back("skewered by manticore barbs");
    if (is(MB_POISON_VULN))
        v.push_back("more vulnerable to poison");
    if (is(MB_ICEMAIL))
        v.push_back("surrounded by an icy envelope");
    if (is(MB_AGILE))
        v.push_back("unusually agile");
    if (is(MB_FROZEN))
        v.push_back("encased in ice");
    if (is(MB_BLACK_MARK))
        v.push_back("absorbing vital energies");
    if (is(MB_SAP_MAGIC))
        v.push_back("magic-sapped");
    if (is(MB_SHROUD))
        v.push_back("shrouded");
    if (is(MB_CORROSION))
        v.push_back("covered in acid");
    if (is(MB_SPECTRALISED))
        v.push_back("ghostly");
    if (is(MB_SLOW_MOVEMENT))
        v.push_back("covering ground slowly");
    if (is(MB_LIGHTLY_DRAINED))
        v.push_back("lightly drained");
    if (is(MB_HEAVILY_DRAINED))
        v.push_back("heavily drained");
    return v;
}

string monster_info::wounds_description_sentence() const
{
    const string wounds = wounds_description();
    if (wounds.empty())
        return "";
    else
        return string(pronoun(PRONOUN_SUBJECTIVE)) + " is " + wounds + ".";
}

string monster_info::wounds_description(bool use_colour) const
{
    if (dam == MDAM_OKAY)
        return "";

    string desc = get_damage_level_string(holi, dam);
    if (use_colour)
    {
        const int col = channel_to_colour(MSGCH_MONSTER_DAMAGE, dam);
        desc = colour_string(desc, col);
    }
    return desc;
}

string monster_info::constriction_description() const
{
    string cinfo = "";
    bool bymsg = false;

    if (constrictor_name != "")
    {
        cinfo += constrictor_name;
        bymsg = true;
    }

    string constricting = comma_separated_line(constricting_name.begin(),
                                               constricting_name.end());

    if (constricting != "")
    {
        if (bymsg)
            cinfo += ", ";
        cinfo += constricting;
    }
    return cinfo;
}

int monster_info::randarts(artefact_prop_type ra_prop) const
{
    int ret = 0;

    if (itemuse() >= MONUSE_STARTING_EQUIPMENT)
    {
        item_def* weapon = inv[MSLOT_WEAPON].get();
        item_def* second = inv[MSLOT_ALT_WEAPON].get(); // Two-headed ogres, etc.
        item_def* armour = inv[MSLOT_ARMOUR].get();
        item_def* shield = inv[MSLOT_SHIELD].get();
        item_def* ring   = inv[MSLOT_JEWELLERY].get();

        if (weapon && weapon->base_type == OBJ_WEAPONS && is_artefact(*weapon))
            ret += artefact_wpn_property(*weapon, ra_prop);

        if (second && second->base_type == OBJ_WEAPONS && is_artefact(*second))
            ret += artefact_wpn_property(*second, ra_prop);

        if (armour && armour->base_type == OBJ_ARMOUR && is_artefact(*armour))
            ret += artefact_wpn_property(*armour, ra_prop);

        if (shield && shield->base_type == OBJ_ARMOUR && is_artefact(*shield))
            ret += artefact_wpn_property(*shield, ra_prop);

        if (ring && ring->base_type == OBJ_JEWELLERY && is_artefact(*ring))
            ret += artefact_wpn_property(*ring, ra_prop);
    }

    return ret;
}

int monster_info::res_magic() const
{
    int mr = (get_monster_data(type))->resist_magic;
    if (mr == MAG_IMMUNE)
        return MAG_IMMUNE;

    // Negative values get multiplied with monster hit dice.
    if (mr < 0)
        mr = mons_class_hit_dice(type) * (-mr) * 4 / 3;

    // Randarts
    mr += 40 * randarts(ARTP_MAGIC);

    // ego armour resistance
    if (inv[MSLOT_ARMOUR].get()
        && get_armour_ego_type(*inv[MSLOT_ARMOUR]) == SPARM_MAGIC_RESISTANCE)
    {
        mr += 40;
    }

    if (inv[MSLOT_SHIELD].get()
        && get_armour_ego_type(*inv[MSLOT_SHIELD]) == SPARM_MAGIC_RESISTANCE)
    {
        mr += 40;
    }

    item_def *jewellery = inv[MSLOT_JEWELLERY].get();

    if (jewellery && jewellery->base_type == OBJ_JEWELLERY
        && jewellery->sub_type == RING_PROTECTION_FROM_MAGIC)
    {
        mr += 40;
    }

    if (is(MB_VULN_MAGIC))
        mr /= 2;

    return mr;
}

bool monster_info::wields_two_weapons() const
{
    return is(MB_TWO_WEAPONS);
}

bool monster_info::can_regenerate() const
{
    return !is(MB_NO_REGEN);
}

reach_type monster_info::reach_range() const
{
    const monsterentry *e = get_monster_data(mons_class_is_zombified(type)
                                             ? base_type : type);
    ASSERT(e);

    reach_type range = e->attack[0].flavour == AF_REACH
                       || e->attack[0].type == AT_REACH_STING
                          ? REACH_TWO : REACH_NONE;

    const item_def *weapon = inv[MSLOT_WEAPON].get();
    if (weapon)
        range = max(range, weapon_reach(*weapon));

    return range;
}

size_type monster_info::body_size() const
{
    // Using base_type to get the right size for zombies, skeletons and such.
    // For normal monsters, base_type is set to type in the constructor.
    const monsterentry *e = get_monster_data(base_type);
    size_type ret = (e ? e->size : SIZE_MEDIUM);

    // Slime creature size is increased by the number merged.
    if (type == MONS_SLIME_CREATURE)
    {
        if (number == 2)
            ret = SIZE_MEDIUM;
        else if (number == 3)
            ret = SIZE_LARGE;
        else if (number == 4)
            ret = SIZE_BIG;
        else if (number == 5)
            ret = SIZE_GIANT;
    }
    else if (mons_is_item_mimic(type))
    {
        const int mass = item_mass(*inv[MSLOT_MISCELLANY].get());
        if (mass < 50)
            ret = SIZE_TINY;
        else if (mass < 100)
            ret = SIZE_LITTLE;
        else if (mass < 200)
            ret = SIZE_SMALL;
        else
            ret = SIZE_MEDIUM;
    }

    return ret;
}

bool monster_info::cannot_move() const
{
    return is(MB_PARALYSED) || is(MB_PETRIFIED);
}

bool monster_info::airborne() const
{
    return fly == FL_LEVITATE || (fly == FL_WINGED && !cannot_move());
}

bool monster_info::ground_level() const
{
    return !airborne() && !is(MB_CLINGING);
}

// Only checks for spells from preset monster spellbooks.
// Use monster.h's has_spells for knowing a monster has spells
bool monster_info::has_spells() const
{
    const vector<mon_spellbook_type> books = get_spellbooks(*this);

    const size_t num_books = books.size();

    // Random pan lords don't display their spells.
    if (num_books == 0 || books[0] == MST_NO_SPELLS
        || type == MONS_PANDEMONIUM_LORD)
    {
        return false;
    }

    // Ghosts have a special book but may not have any spells anyways.
    if (books[0] == MST_GHOST)
    {
        for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i)
            if (this->spells[i] != SPELL_NO_SPELL)
                return true;
        return false;
    }

    return true;
}

void get_monster_info(vector<monster_info>& mons)
{
    vector<monster* > visible;
    if (crawl_state.game_is_arena())
    {
        for (monster_iterator mi; mi; ++mi)
            visible.push_back(*mi);
    }
    else
        visible = get_nearby_monsters();

    for (unsigned int i = 0; i < visible.size(); i++)
    {
        if (!mons_class_flag(visible[i]->type, M_NO_EXP_GAIN)
            || visible[i]->is_child_tentacle()
            || visible[i]->type == MONS_BALLISTOMYCETE
                && visible[i]->number > 0)
        {
            mons.push_back(monster_info(visible[i]));
        }
    }
    sort(mons.begin(), mons.end(), monster_info::less_than_wrapper);
}
