/*
 * Licensed under BSD license.  See LICENCE.TXT  
 *
 * Produced by:	Jeff Lait
 *
 *      	7DRL Development
 *
 * NAME:        mob.cpp ( Live Once Library, C++ )
 *
 * COMMENTS:
 */

#include "mob.h"

#include "map.h"
#include "msg.h"
#include "text.h"
#include "item.h"
#include "event.h"
#include "display.h"
#include "engine.h"
#include "thesaurus.h"
#include "level.h"
#include "config.h"

#include <stdio.h>

#include <iostream>
using namespace std;

// Disable wall sliding as sloppy motion only makes sense in tiled
// environment.
#define ENABLE_WALLSLIDE (glbConfig->myWallSlide)


//
// MOB Implementation
//

MOB::MOB()
{
    myFleeCount = 0;
    myBoredom = 0;
    myYellHystersis = 0;
    myAIState = 0;
    myHP = 0;
    myMaxHP = 1;
    myMP = 0;
    myMaxMP = 1;
    // Enter the dungeon full!
    myFood = getMaxFood() - 1;

    myOrigBody.reset();

    myIsSwallowed = false;
    myNumDeaths = 0;
    myUID = INVALID_UID;

    mySkipNextTurn = false;
    myDelayMob = false;
    myDelayMobIdx = -1;
    myCollisionTarget = 0;
    mySearchPower = 0;
    myAngryWithAvatar = false;
    mySeenAvatar = false;

    YELL_NAMES	yell;
    FOREACH_YELL(yell)
    {
	myHeardYell[yell] = false;
	myHeardYellSameRoom[yell] = false;
    }
    SPELL_NAMES spell;
    FOREACH_SPELL(spell)
    {
	myKnownSpells[spell] = 0;
    }
    mySawMurder = false;
    mySawMeanMurder = false;
    mySawVictory = false;
    myAvatarHasRanged = false;
    myWaiting = false;
    myCowardice = 0;
    myKills = 0;
    myHasWon = false;
    myHasTriedSuicide = false;
    myRangeTimeout = 0;
    mySpellTimeout = 0;

    myLastDir = 0;
    myLastDirCount = 0;

    mySeed = 0;

    myLevel = 1;
    myExp = 0;
    myExtraLives = 0;

    myDecoupledFromBody = true;
}

MOB::~MOB()
{
    int		i;

    if (myPos.map() && myPos.map()->avatar() == this)
	myPos.map()->setAvatar(0);

    myPos.removeMob(this);

    for (i = 0; i < myInventory.entries(); i++)
	delete myInventory(i);
}

#define MAX_TAINT		200

bool
MOB::isAvatarDef(MOB_NAMES mob)
{
    return defn(mob).isavatarform;
}

bool
MOB::isAvatarDef() const
{
    if (isAvatarDef(getDefinition()))
	return true;
    if (myOrigBody)
	return myOrigBody->isAvatarDef();
    return false;
}

void
MOB::initSystem()
{
}

void
MOB::saveGlobal(ostream &os)
{
}

void
MOB::loadGlobal(istream &is)
{
}

static u8
mob_findsymbol(const char *name)
{
    // Find letter before space.
    if (!name || !*name)
	return '!';
    const char *idx = &name[strlen(name)-1];

    // Eat trailing spaces (which there shouldn't be any!)
    while (idx > name)
    {
	if (*idx == ' ')
	    idx--;
	else
	    break;
    }

    while (idx > name)
    {
	if (*idx == ' ')
	    return *(idx+1);
	idx--;
    }
    return *name;
}

static ELEMENT_NAMES
mob_randelementresist()
{
    // Intentionally skip physical as that is too unbalanced both ways.
    ELEMENT_NAMES	elem = (ELEMENT_NAMES) rand_range(ELEMENT_PHYSICAL+1, NUM_ELEMENTS-1);

    // Light needs to be pretty rare as it is very odd.
    if (elem == ELEMENT_LIGHT && rand_chance(30))
	return mob_randelementresist();

    return elem;
}

static ELEMENT_NAMES
mob_randelementattack()
{
    // We already gate to bias to physical in the caller!
    // So only roll for non-physical
    ELEMENT_NAMES	elem = (ELEMENT_NAMES) rand_range(ELEMENT_PHYSICAL+1, NUM_ELEMENTS-1);

    // Light needs to be pretty rare as it is very odd.
    if (elem == ELEMENT_LIGHT && rand_chance(50))
	return mob_randelementattack();

    return elem;
}

static ATTR_NAMES
mob_randattr()
{
    static const ATTR_NAMES attrnames[20] =
    {
	ATTR_GOLD,
	ATTR_YELLOW,
	ATTR_PINK,
	ATTR_PURPLE,
	ATTR_NORMAL,
	ATTR_LIGHTBLACK,
	ATTR_WHITE,
	ATTR_ORANGE,
	ATTR_LIGHTBROWN,
	ATTR_BROWN,
	ATTR_RED,
	ATTR_DKRED,
	ATTR_GREEN,
	ATTR_DKGREEN,
	ATTR_BLUE,
	ATTR_LIGHTBLUE,
	ATTR_TEAL,
	ATTR_CYAN,
	ATTR_FIRE,
	ATTR_DKCYAN
    };

    return attrnames[rand_choice(20)];
}

static ATTR_NAMES
mob_randattrfromname(const char *name)
{
    if (strstr(name, "white") || strstr(name, "arctic") || strstr(name, "snowy"))
	return ATTR_WHITE;
    if (strstr(name, "brown"))
	return ATTR_BROWN;
    if (strstr(name, "red"))
	return ATTR_RED;
    if (strstr(name, "black"))
	return ATTR_LIGHTBLACK;
    if (strstr(name, "gold"))
	return ATTR_GOLD;
    if (strstr(name, "blue"))
	return ATTR_BLUE;
    if (strstr(name, "purple"))
	return ATTR_PURPLE;
    if (strstr(name, "green") || strstr(name, "malachite") || strstr(name, "emerald"))
	return ATTR_GREEN;
    if (strstr(name, "teal"))	//YES, I know!
	return ATTR_TEAL;
    if (strstr(name, "yellow"))
	return ATTR_YELLOW;
    if (strstr(name, "grey"))
	return ATTR_GREY;
    if (strstr(name, "silver"))
	return ATTR_LTGREY;
    if (strstr(name, "olive"))
	return ATTR_DKGREEN;

    return mob_randattr();
}

void
mob_adddescr(MOB_DEF *def, const char *descr)
{
    BUF		buf;

    // Don't double add!
    if (strstr(def->descr, descr))
	return;

    buf.sprintf("%s%s  ", def->descr, descr);
    def->descr = glb_harden(buf);
}


MOB_NAMES glbBossName = MOB_BAEZLBUB;

MOB *
MOB::create(const MAP *map, MOB_NAMES def, bool allowitems)
{
    MOB		*mob;

    mob = new MOB();

    mob->myDefinition = def;

    mob->myMaxHP = defn(def).max_hp.roll();
    mob->myHP = mob->myMaxHP;
    mob->myMaxMP = defn(def).max_mp.roll();
    mob->myMP = mob->myMaxMP;

    mob->mySeed = rand_int();

    // We don't currently support this!
    // J_ASSERT(!mob->defn().swallows);

    // Determine a starting set...
    if (allowitems)
    {
	int choice = 0;

	STARTITEMS_NAMES startitems = GAMEDEF::mobdef(def)->startitems;
	STARTITEMS_NAMES beststart = STARTITEMS_NONE;
	while (startitems != STARTITEMS_NONE)
	{
	    int rarity = glb_startitemsdefs[startitems].rarity;
	    if (rand_choice(choice + rarity) < rarity)
		beststart = startitems;
	    choice += rarity;
	    startitems = glb_startitemsdefs[startitems].next;
	}

	if (beststart != STARTITEMS_NONE)
	{
	    ITEMLIST_NAMES itemlist = glb_startitemsdefs[beststart].itemlist;

	    while (itemlist != ITEMLIST_NONE)
	    {
		if (rand_chance(glb_itemlistdefs[itemlist].chance))
		{
		    int		count = glb_itemlistdefs[itemlist].count.roll();
		    while (count --> 0)
		    {
			ITEM *item = nullptr;
			ITEM_NAMES itemname = glb_itemlistdefs[itemlist].item;
			if (itemname != ITEM_NONE)
			    item = ITEM::create(map, itemname, 1);
			ITEMCLASS_NAMES itemclass = glb_itemlistdefs[itemlist].itemclass;
			if (itemclass != ITEMCLASS_NONE && !item)
			    item = ITEM::createItemClass(map, itemclass, 1);

			if (item)
			{
			    item->markStartingItem(true);
			    mob->addItem(item);
			}
		    }
		}

		itemlist = glb_itemlistdefs[itemlist].next;
	    }
	}

	// DEBUG
	if (0)
	{
	    ITEM *item = ITEM::create(map, 
		    ITEM::lookupWand(map, WAND_POLY),
		    1);
	    mob->addItem(item);
	}
    }

    return mob;
}

MOB *
MOB::copy() const
{
    MOB		*mob;

    mob = new MOB();
    
    *mob = *this;

    // Copy inventory one item at a time.
    // We are careful to maintain the same list order here so restoring
    // won't shuffle things unexpectadly
    mob->myInventory.clear();
    for (int i = 0; i < myInventory.entries(); i++)
    {
	mob->myInventory.append(myInventory(i)->copy());
    }
    
    return mob;
}

void
MOB::setPos(POS pos)
{
    pos.moveMob(this, myPos);
    myPos = pos;
}


void
MOB::setMap(MAP *map)
{
    myPos.setMap(map);
    myTarget.setMap(map);
    myHome.setMap(map);
    myMeditatePos.setMap(map);
    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (myInventory(i))
	    myInventory(i)->setMap(map);
    }
}

void
MOB::stashInto(MOB *dst)
{
    dst->myInventory = myInventory;
    myInventory.clear();

    dst->myDefinition = myDefinition;
    dst->myName = myName;
    dst->myNumDeaths = myNumDeaths;

    SPELL_NAMES spell;
    FOREACH_SPELL(spell)
    {
	dst->myKnownSpells[spell] = myKnownSpells[spell];
    }

    dst->myHP = myHP;
    dst->myMaxHP = myMaxHP;
    dst->myMP = myMP;
    dst->myMaxMP = myMaxMP;
    dst->myFood = myFood;
    // In stasis so we don't need to unique!
    dst->myOrigBody = myOrigBody;


    dst->mySeed = mySeed;
    

    dst->myExp = myExp;
    dst->myLevel = myLevel;
    dst->myExtraLives = myExtraLives;
}

void
MOB::clearAllPos()
{
    myPos = POS();
    myTarget = POS();
    myHome = POS();
    myMeditatePos = POS();
}

MOB *
MOB::createNPC(const MAP *map, int difficulty)
{
    MOB_NAMES		basetype = MOB::chooseNPCType(difficulty);

    MOB			*base = MOB::create(map, basetype, /* items */ true);

    return base;
}

MOB_NAMES
MOB::chooseAnyNPCType()
{
    int		i;
    MOB_NAMES	mob = MOB_NONE;
    int		choice = 0;

    // Given the letter choice, choose a mob that matches it appropriately.
    choice = 0;
    for (i = 0; i < GAMEDEF::getNumMob(); i++)
    {
	// depth 0 isn't generated.
	if (!defn(i).depth)
	    continue;
	int		rarity = defn(i).rarity;
	if (rand_choice(choice + rarity) < rarity)
	    mob = (MOB_NAMES) i;
	choice += rarity;
    }

    return mob;
}

MOB_NAMES
MOB::chooseAnyNPCTypeWithCorpse()
{
    int		i;
    MOB_NAMES	mob = MOB_NONE;
    int		choice = 0;

    // Given the letter choice, choose a mob that matches it appropriately.
    choice = 0;
    for (i = 0; i < GAMEDEF::getNumMob(); i++)
    {
	// depth 0 isn't generated.
	if (!defn(i).depth)
	    continue;
	// No ghosts!
	if (!defn(i).corpsechance)
	    continue;
	int		rarity = defn(i).rarity;
	if (rand_choice(choice + rarity) < rarity)
	    mob = (MOB_NAMES) i;
	choice += rarity;
    }

    return mob;
}
MOB_NAMES
MOB::chooseNPCType(int difficulty)
{
    int		i;
    MOB_NAMES	mob = MOB_NONE;
    int		choice = 0;
    int		matchingdepth = 0;

    // Given the letter choice, choose a mob that matches it appropriately.
    choice = 0;
    for (i = 0; i < GAMEDEF::getNumMob(); i++)
    {
	// depth 0 isn't generated.
	if (!defn(i).depth)
	    continue;
	// Use the baseletter to bias the creation.
	if (defn(i).depth <= difficulty)
	{
	    // Usually a uniform chance for every new level works,
	    // But that is a very slow burn to introduce new monsters,
	    // so bias to your own depth.
	    int		rarity = defn(i).rarity;
	    int		scale = 15 - (difficulty - defn(i).depth);
	    if (scale < 1) scale = 1;
	    rarity *= scale;
	    if (rand_choice(choice + rarity) < rarity)
		mob = (MOB_NAMES) i;
	    choice += rarity;
	}
    }

    // Testing..
    // mob = MOB_KOBOLD;
    // mob = MOB_KOBOLD_THIEF;
    // mob = MOB_PURPLEWORM;
    // mob = MOB_CAVESPIDER;

    return mob;
}

MOB *
MOB::createAvatar(const MAP *map)
{
    MOB *m = MOB::create(map, MOB_AVATAR, true);

    for (int i = 0; i < 0; i++)
    {
	// ITEM *item = ITEM::createRandom(map, 5);
	ITEM *item = ITEM::create(map, ITEM::chooseRandomBalanced(map, 1));
	if (item)
	    m->addItem(item);
    }

    ITEM_NAMES item = ITEM_COIN;
    if (item != ITEM_NONE)
    {
	ITEM *i = ITEM::create(map, item, 1);
	if (i)
	    m->addItem(i);
    }

    // Equip what is sensible...
    m->avatarStartup();

    // DEBUG:
    // Add spells
    // m->memorizeSpell(SPELL_TELEPORT);
    // m->memorizeSpell(SPELL_TSUNAMI);
    // m->memorizeSpell(SPELL_DISINTEGRATE);
    // m->memorizeSpell(SPELL_LIGHTNINGBOLT);
    // m->memorizeSpell(SPELL_MAGICMISSILE);
    // m->memorizeSpell(SPELL_FORCEBOLT);

    return m;
}

void
MOB::avatarStartup()
{
    bool		donewield=false, donearmour=false, donebow=false;
    for (auto && item : inventory())
    {
	if (!donewield && item->isWeapon())
	{
	    item->setWielded(true);
            item->markBonusKnown();
	    donewield = true;
	}
	if (!donearmour && item->isArmour())
	{
	    item->setEquipped(true);
            item->markBonusKnown();
	    donearmour = true;
	}
	if (!donebow && item->isRanged())
	{
	    item->setEquipped(true);
            item->markBonusKnown();
	    donebow = true;
	}
        item->markMagicClassKnown(true);
    }
}

void
MOB::getLook(u8 &symbol, ATTR_NAMES &attr) const
{
    symbol = defn().symbol;
    attr = (ATTR_NAMES) defn().attr;

    if (!alive())
    {
	// Dead creatures override their attribute to blood
	attr = ATTR_RED;
    }
}

BUF
MOB::getName() const
{
    BUF		result;

    if (isAvatar())
	result.reference("you");
    else if (myName.isstring())
	result = myName;
    else
    {
	result = thesaurus_expand(mySeed, getDefinition());
    }

    if (hasItem(ITEM_ASLEEP) && !isAvatar())
    {
	BUF	tmp;
	tmp.sprintf("sleeping %s", result.buffer());
	result = tmp;
    }

    return result;
}

BUF
MOB::getRawName() const
{
    BUF		result;
    result.reference(defn().name);
    return result;
}

BUF
MOB::getArticleName() const
{
    BUF		name = getName();
    BUF		result;

    result.sprintf("%s%s", gram_getarticle(name.buffer()), name.buffer());

    return result;
}


void
MOB::setupTextVariables() const
{
    BUF		tmp;

    tmp.sprintf("%s%s", gram_getarticle(getName()), getName().buffer());
    text_registervariable("A_MOBNAME", tmp);
    text_registervariable("MOBNAME", getName());
    text_registervariable("MOBNAMES", gram_makepossessive(getName()));
}

MOB_NAMES
MOB::getBossName()
{
    return glbBossName;
}

BUF
MOB::buildSpellDescr(SPELL_NAMES spell)
{
    BUF		stats, descr;

    SPELL_DEF		*def = GAMEDEF::spelldef(spell);

    stats.sprintf("Spell: %s (%s)\n", def->name, def->verb);
    descr.strcat(stats);
    if (def->timeout)
    {
	stats.sprintf("Timeout: %d  ", def->timeout);
	descr.strcat(stats);
    }
    if (def->mana || def->reqfullmana)
    {
	if (def->reqfullmana)
	    stats.strcpy("Mana: All  ");
	else
	    stats.sprintf("Mana: %d  ", def->mana);
	descr.strcat(stats);
    }
    if (def->radius > 1)
    {
	stats.sprintf("Radius: %d  ", def->radius);
	descr.strcat(stats);
    }

    if (def->piercing)
    {
	descr.strcat("Piercing ");
    }

    if (def->blast)
    {
	stats.sprintf("Close Blast\n");
	descr.strcat(stats);
    }
    else if (def->cone)
    {
	stats.sprintf("Cone\n");
	descr.strcat(stats);
    }
    else if (def->range > 1)
    {
	stats.sprintf("Range: %d\n", def->range);
	descr.strcat(stats);
    }
    else
    {
	descr.strcat("\n");
    }

    if (!def->needsdir)
	descr.strcat("Targets Self\n");

    stats = buildEffectDescr((EFFECT_NAMES) def->effect);
    if (stats.isstring())
    {
	descr.strcat("Effect: ");
	descr.strcat(stats);
	descr.strcat("\n");
    }

    return descr;
}

BUF
MOB::buildAttackDescr(ATTACK_NAMES attack)
{
    BUF		stats, descr;

    ATTACK_DEF		*def = GAMEDEF::attackdef(attack);
    stats.sprintf("Attack: %s (%s)\n", def->noun, def->verb);
    descr.strcat(stats);
    stats.sprintf("   Damage: %s\n", def->damage.format().buffer());
    descr.strcat(stats);
    if (def->chancebonus)
    {
	stats.sprintf("   Bonus to Hit: %d\n", def->chancebonus);
	descr.strcat(stats);
    }
    if (def->element != ELEMENT_PHYSICAL)
    {
	stats.sprintf("   Element: %s\n", GAMEDEF::elementdef(def->element)->name);
	descr.strcat(stats);
    }
    stats = buildEffectDescr((EFFECT_NAMES) def->effect);
    if (stats.isstring())
    {
	descr.strcat("   Effect: ");
	descr.strcat(stats);
	descr.append('\n');
    }

    return descr;
}

BUF
MOB::buildSpecialDescr(MOB_NAMES mob)
{
    BUF		descr, stats;

    descr.strcat(defn(mob).descr);

    if (defn(mob).resistance != ELEMENT_NONE)
    {
	stats.sprintf("They are unaffected by %s.  ", GAMEDEF::elementdef(defn(mob).resistance)->name);
	descr.strcat(stats);
    }
    if (defn(mob).vulnerability != ELEMENT_NONE)
    {
	stats.sprintf("They are particularly sensitive to %s.  ", GAMEDEF::elementdef(defn(mob).vulnerability)->name);
	descr.strcat(stats);
    }

    if (defn(mob).isslow)
    {
	descr.strcat("They move with a slow plodding pace.  ");
    }
    if (defn(mob).isfast)
    {
	descr.strcat("They move with shocking speed.  ");
    }
    if (defn(mob).isvampire)
    {
	descr.strcat("They drink the blood of their foes, gaining energy therefrom.  ");
    }
    if (defn(mob).isregenerate)
    {
	descr.strcat("They heal wounds with astonishing speed.  ");
    }
    if (defn(mob).candig)
    {
	descr.strcat("They can dig through solid rock.  ");
    }
    if (defn(mob).swallows)
    {
	descr.strcat("They can swallow their foes whole.  ");
    }
    if (defn(mob).breeder)
    {
	descr.strcat("The rate of their reproduction leaves rabbits embarrassed.  ");
    }
    if (defn(mob).eggtype != MOB_NONE)
    {
	descr.strcat("They can lay eggs.  ");
    }
    if (defn(mob).isthief)
    {
	descr.strcat("They steal interesting objects.  ");
    }
    if (defn(mob).canleap)
    {
	descr.strcat("They can leap long distances.  ");
    }
    if (defn(mob).corrosiveblood)
    {
	descr.strcat("Their corrosive blood will damage weapons that strike them.  ");
    }
    if (defn(mob).passwall)
    {
	descr.strcat("They move through walls.  ");
    }
    if (defn(mob).sightrange < 5)
    {
	descr.strcat("They are known to be short-sighted.  ");
    }

    return descr;
}

void
MOB::describe() const
{
    BUF		descr;

    // Always in current tense as you are dead, not were dead.
    formatAndReport("%S <be> %o.", healthStatusDescr());

    BUF		be_buf = gram_conjugate("be", getPerson(), !alive());
    const char *be = be_buf.buffer();
    BUF		have_buf = gram_conjugate("have", getPerson(), !alive());
    const char *have = have_buf.buffer();

    descr.sprintf("%%S %s %%o.", be);
    formatAndReport(descr, foodStatusDescr());
    descr.clear();

    if (isSwallowed())
    {
	formatAndReport("%S <be> swallowed.");
    }

    if (myInventory.entries())
    {
	int		any = 0;
	for (auto && item : inventory())
	{
	    if (!item->isFlag())
		continue;
	    BUF		 name = item->getName();
	    if (any)
		descr.strcat(", ");
	    else
		descr.appendSprintf("%%S %s ", be);
	    any++;
	    descr.strcat(name);
	}
	if (any)
	    descr.strcat(".  ");

	// Describe anything held or worn...
	for (auto && item : inventory())
	{
	    if (item->isFlag())
		continue;
	    if (item->isEquipped() || item->isWielded())
	    {
		BUF		 name = item->getArticleName();
		descr.strcat("%S ");
		if (item->isWielded())
		    descr.appendSprintf("%s wielding ", be);
		else if (item->isRanged())
		    descr.appendSprintf("%s readied ", have);
		else
		    descr.appendSprintf("%s wearing ", be);
		descr.strcat(name);
		descr.strcat(".  ");
		any++;
	    }
	}
	if (any)
	    descr.strcat("\n");
    }

    if (isAvatar())
    {
	BUF buf;
	buf.sprintf("%%S %s level %d with %d out of %d experience.\n",
		be, getLevel(), getExp(), LEVEL::expFromLevel(getLevel()+1));
	descr.strcat(buf);
	int		story = GAMEDEF::rules().bosslevel - pos().depth() + 1;
	story = pos().depth();

	buf.sprintf("%%S %s on floor %d.\n",
		    be, story);
	descr.strcat(buf);
    }
    formatAndReport(descr);
}

BUF
MOB::getLongDescription() const
{
    BUF		descr, detail;
    BUF		stats, buf;
    const char 	*Pronoun = "They";
    const char  *possessive = "their";
    const char  *are = "are";
    const char  *have = "have";

    descr.strcpy(gram_capitalize(getName()));
    stats.sprintf(", %s\n", healthStatusDescr());
    descr.strcat(stats);
    if (getMaxMP())
    {
	stats.sprintf("  Mana: %d / %d", getMP(), getMaxMP());
	descr.strcat(stats);
    }
    descr.strcat("\n");

    if (isAvatar())
    {
	Pronoun = "You";
	possessive = "your";
    }
    if (!alive())
    {
	are = "were";
	have = "had";
    }

    if (1)
    {
	BUF		text = text_lookup("mob", getRawName());
	descr.strcat("\n");
	text.stripTrailingWS();
	descr.strcat(text);
	descr.strcat("\n");
    }

    if (!isAvatar())
    {
	const char *rarity = "a unique creature";
	if (defn().rarity > 0)
	{
	    if (defn().rarity >= 100)
		rarity = "a common creature";
	    else
		rarity = "a rare creature";
	}
	stats.sprintf("\nThe %s is %s.  %s  ",
		getName().buffer(), rarity, glb_aidefs[getAI()].descr);

	descr.strcat(stats);

	stats = buildSpecialDescr(getDefinition());
	descr.strcat(stats);
	descr.strcat("\n");
    }
    if (isAvatar())
    {
	stats.sprintf("%s %s %s.\n", Pronoun, are, foodStatusDescr());
	descr.strcat(stats);
    }

    int		reduction = getDamageReduction(ELEMENT_PHYSICAL);
    if (reduction)
    {
	stats.sprintf("Damage Reduction: %d", reduction);
	descr.strcat(stats);
    }
    if (defn().dodgebonus)
    {
	if (reduction)
	    descr.strcat("; ");
	stats.sprintf("Dodge Bonus: %d", defn().dodgebonus);
	descr.strcat(stats);
    }
    if (defn().dodgebonus || reduction)
	descr.append('\n');

    if (getMeleeAttack() != ATTACK_NONE)
    {
	descr.append('\n');
	ATTACK_DEF		*def = GAMEDEF::attackdef(getMeleeAttack());
	stats.sprintf("%s %s %s foes for %s damage.",
			Pronoun,
			def->verb, 
			possessive,
			def->damage.format().buffer());
	descr.strcat(stats);
    }

    if ((defn().range_valid && defn().range_range) || lookupRanged())
    {
	stats.sprintf("\n\n%s can also attack at a up to %d distance", Pronoun, getRangedRange());
	if (defn().range_recharge)
	    stats.appendSprintf(", every %d turns.  ", defn().range_recharge);
	else
	    stats.strcat(".  ");
	descr.strcat(stats);

	ATTACK_DEF		*def = GAMEDEF::attackdef(getRangedAttack());
	stats.sprintf("%s %s %s foes for %s damage.",
			Pronoun,
			def->verb, 
			possessive,
			def->damage.format().buffer());
	descr.strcat(stats);
    }

    descr.append('\n');

    // Describe intrinsics
    if (myInventory.entries())
    {
	int		any = 0;
	for (auto && item : inventory())
	{
	    if (!item->isFlag())
		continue;
	    BUF		 name = item->getName();
	    if (any)
		descr.strcat(", ");
	    else
	    {
		descr.strcat(Pronoun);
		descr.strcat(" ");
		descr.strcat(are);
		descr.strcat(" ");
	    }
	    any++;
	    descr.strcat(name);
	}
	if (any)
	    descr.strcat(".  ");

	// Describe anything held or worn...
	for (auto && item : inventory())
	{
	    if (item->isFlag())
		continue;
	    if (item->isEquipped() || item->isWielded())
	    {
		BUF		 name = item->getArticleName();
		descr.strcat(Pronoun);
		if (item->isWielded())
		    descr.appendSprintf(" %s wielding ", are);
		else if (item->isRanged())
		    descr.appendSprintf(" %s readied ", have);
		else
		    descr.appendSprintf(" %s wearing ", are);
		descr.strcat(name);
		descr.strcat(".  ");
		any++;
	    }
	}
	if (any)
	    descr.strcat("\n");
    }

    if (isAvatar())
    {
	buf.sprintf("\nLevel: %d   Exp: %d / %d\n",
		getLevel(), getExp(), LEVEL::expFromLevel(getLevel()+1));
	descr.strcat(buf);
    }
    if (getExtraLives() > 0)
    {
	buf.sprintf("%s %s %d extra lives.\n", Pronoun, have, getExtraLives());
	descr.strcat(buf);
    }

    return descr;
}

ITEM *
MOB::getRandomItem(bool allowequipped) const
{
    ITEM	*result;
    int		choice = 0;

    result = 0;

    if (myInventory.entries())
    {
	// I hope I was drunk when I did multi-sample here?
	// Admittedly, this is the only code which has generated hate
	// mail, so it must be doing something right!
	int		tries = 10;
	while (tries--)
	{
	    choice = rand_choice(myInventory.entries());

	    // Ignore flags
	    if (myInventory(choice)->isFlag())
		continue;

	    // Ignore equipped if asked
	    if (myInventory(choice)->isEquipped() && !allowequipped)
		continue;
	    if (myInventory(choice)->isWielded() && !allowequipped)
		continue;

	    return myInventory(choice);
	}
    }
    return result;
}

ITEM *
MOB::itemFromLetter(u8 letter) const
{
    // No letter is unassigned, which may be a lot of these!
    if (!letter)
	return nullptr;

    for (auto && item : inventory())
    {
	if (item->inventoryLetter() == letter)
	    return item;
    }
    return nullptr;
}

bool
MOB::anyFreeInventoryLetter() const
{
    if (!isAvatar())
	return true;
    MAP	*map = pos().map();
    if (!map)
	return false;

    bool		usedletter[26];

    for (int i = 0; i < 26; i++)
	usedletter[i] = false;
    for (auto && item : inventory())
    {
	u8 sym = item->inventoryLetter();
	if (sym >= 'a' && sym <= 'z')
	{
	    usedletter[sym-'a'] = true;
	}
    }
    for (int i = 0; i < 26; i++)
    {
	if (!usedletter[i])
	    return true;
    }
    return false;
}

bool
MOB::assignInventoryLetter(ITEM *assignee)
{
    if (!isAvatar())
	return false;

    MAP	*map = pos().map();
    if (!map)
	return false;

    map->setMobLetterTimeStamp(map->mobLetterTimeStamp()+1);

    // It is unlikely we overflow an int, but it would be somewhat
    // catastrophic as we'd lock open.  So just reset when we are big.
    if (map->mobLetterTimeStamp() > 1000*1000*1000)
    {
	map->setMobLetterTimeStamp(1);
	for (int i = 0; i < 26; i++)
	    map->setMobLastSeen(i, 0);
    }

    for (auto && item : inventory())
    {
	u8 sym = item->inventoryLetter();
	if (sym >= 'a' && sym <= 'z')
	{
	    map->setMobLastSeen(sym - 'a', map->mobLetterTimeStamp());
	}
    }

    int		minstamp = map->mobLastSeen(0);
    u8		lastletter = 0;
    for (int i = 0; i < 26; i++)
    {
	if (map->mobLastSeen(i) < minstamp)
	{
	    minstamp = map->mobLastSeen(i);
	    lastletter = i;
	}
    }

    // If this is the current water mark we failed.
    if (minstamp == map->mobLetterTimeStamp())
    {
	// No new slots!
	return false;
    }

    map->setMobLastSeen(lastletter, map->mobLetterTimeStamp());
    assignee->setInventoryLetter(lastletter+'a');

    return true;
}

void
MOB::stealNonEquipped(MOB *src)
{
    // NO-ops
    if (!src || src == this)
	return;

    for (int i = src->myInventory.entries(); i --> 0;)
    {
	ITEM *item = src->myInventory(i);

	if (item->isEquipped())
	    continue;
	if (item->isWielded())
	    continue;

	if (item->isFlag())
	    continue;

	src->removeItem(item, true);
	addItem(item);
    }
    // COrrect backwards loop
    myInventory.reverse();
}

void
MOB::stealAllItems(MOB *src)
{
    // NO-ops
    if (!src || src == this)
	return;

    for (int i = src->myInventory.entries(); i --> 0;)
    {
	ITEM *item = src->myInventory(i);

	if (item->isFlag())
	    continue;

	// Erase, keeping the equipped flags!
	src->myInventory.set(i, nullptr);
	// I'm going to regret this, but we can't use addItem
	myInventory.append(item);
    }
    src->myInventory.collapse();
    // Correct backwards loop
    myInventory.reverse();
}

bool
MOB::hasAnyItems() const
{
    for (auto && item : inventory())
	if (!item->isFlag())
	    return true;

    return false;
}

bool		 
MOB::ownsSpecificItem(ITEM *test) const
{
    for (auto && item : inventory())
	if (item == test)
	    return true;

    return false;
}

bool
MOB::hasItem(ITEM_NAMES itemname) const
{
    if (lookupItem(itemname))
	return true;

    return false;
}

bool
MOB::hasUnbrokenItem(ITEM_NAMES itemname) const
{
    if (lookupUnbrokenItem(itemname))
	return true;

    return false;
}

ITEM *
MOB::giftItem(ITEM_NAMES itemname)
{
    ITEM		*item;

    item = ITEM::create(map(), itemname);

    addItem(item);

    // Allow for stacking!
    item = lookupItem(itemname);

    return item;
}

bool
MOB::destroyItem(ITEM_NAMES itemname)
{
    ITEM		*item = lookupItem(itemname);
    if (!item)
	return false;
    removeItem(item);
    delete item;
    return true;
}

ITEM *
MOB::lookupWeapon() const
{
    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->isWielded())
	    return myInventory(i);
    }
    return 0;
}

int
MOB::getDamageReduction(ELEMENT_NAMES element) const
{
    int		reduction = defn().damagereduction;
    ITEM	*armour = lookupArmour();
    if (armour)
    {
	reduction += armour->getDamageReduction();
    }

    if (element != ELEMENT_PHYSICAL)
	reduction = 0;

    ITEM *ring = lookupRing();
    if (ring)
    {
	RING_NAMES	 ringname = ring->asRing();
	if (ringname != RING_NONE)
	{
	    if (GAMEDEF::ringdef(ringname)->resist == element)
	    {
		reduction += GAMEDEF::ringdef(ringname)->resist_amt + ring->getBonus();
		// Identify.
		if (visibleFromAvatar())
		{
		    ring->markMagicClassKnown();
		    ring->markBonusKnown();
		}
	    }
	}
    }

    return reduction;
}

ITEM *
MOB::lookupArmour() const
{
    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->isArmour() && myInventory(i)->isEquipped())
	    return myInventory(i);
    }
    return 0;
}

ITEM *
MOB::lookupRanged() const
{
    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->isRanged() && myInventory(i)->isEquipped())
	    return myInventory(i);
    }
    return 0;
}

ITEM *
MOB::lookupKnownItem(ITEM_NAMES itemname) const
{
    if (isAvatar())
    {
	if (map() && !map()->itemIded(itemname))
	    return nullptr;
    }
    return lookupItem(itemname);
}

ITEM *
MOB::lookupKnownUnbrokenItem(ITEM_NAMES itemname) const
{
    if (isAvatar())
    {
	if (map() && !map()->itemIded(itemname))
	    return nullptr;
    }
    return lookupUnbrokenItem(itemname);
}

ITEM *
MOB::lookupUnbrokenItem(ITEM_NAMES itemname) const
{
    for (auto && item : inventory())
    {
	if (!item) continue;
	if (item->isBroken())
	    continue;
	if (item->getDefinition() == itemname)
	    return item;
    }
    return 0;
}

ITEM *
MOB::lookupItem(ITEM_NAMES itemname) const
{
    int		i;

    for (i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->getDefinition() == itemname)
	    return myInventory(i);
    }
    return 0;
}

ITEM *
MOB::lookupItem(ITEM_NAMES itemname, MATERIAL_NAMES materialname) const
{
    int		i;

    for (i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->getDefinition() == itemname)
	{
	    if (myInventory(i)->material() == materialname)
		return myInventory(i);
	}
    }
    return 0;
}

ITEM *
MOB::lookupRing() const
{
    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (!myInventory(i)) continue;
	if (myInventory(i)->isRing() && myInventory(i)->isEquipped())
	    return myInventory(i);
    }
    return 0;
}

RING_NAMES
MOB::lookupRingName() const
{
    ITEM		*ring = lookupRing();
    if (!ring)
	return RING_NONE;

    return ring->asRing();
}

void
MOB::stopRunning()
{
    if (hasItem(ITEM_QUICKBOOST))
	removeItem(lookupItem(ITEM_QUICKBOOST));
    myLastDirCount = 0;
}

bool
MOB::canMoveDir(int dx, int dy, bool checkmob, bool onlypassive) const
{
    POS		goal;

    goal = myPos.delta(dx, dy);

    return canMove(goal, checkmob, onlypassive);
}

bool
MOB::canMove(POS pos, bool checkmob, bool onlypassive) const
{
    if (!pos.valid())
	return false;

    if (!pos.isPassable())
    {
	if (pos.defn().isphaseable && defn().passwall)
	{
	    // Can move through it...
	}
	else if (pos.defn().isdiggable && defn().candig && !onlypassive)
	{
	    // Could dig through it.
	}
	else if (defn().breakdoors && pos.tile() == TILE_ICEWALL && !onlypassive)
        {
            // Can smash ice walls
        }
	else if (isAvatar() && defn().kickdoors && pos.tile() == TILE_ALTAR && !onlypassive)
	{
	    // Smash altars.
	}
	else if ((defn().opendoors || defn().breakdoors) && pos.defn().isdoorway && !onlypassive)
	{
	    // You need to smash or open to go on...
	    if (!defn().opendoors && !defn().breakdoors)
		return false;

	    bool locked = false;
	    switch (pos.tile())
	    {
		case TILE_DOOR_OPEN:
		case TILE_DOOR_BROKEN:
		case TILE_DOOR_EMPTY:
		    // Should not occur as should be passable.
		    break;
		case TILE_DOOR_CLOSED:
		    // Anyone with hands will attempt this.
		    break;
		case TILE_DOOR_LOCKED_UNKNOWN:
		case TILE_DOOR_LOCKED:
		    locked = true;
		    // FALLTHROUGH
		case TILE_DOOR_JAMMED_UNKNOWN:
		case TILE_DOOR_JAMMED:
		    // Monsters know not to try these, but the
		    // avatar is unwise.
		    if (isAvatar())
			break;
		    // No stopping some monsters.
		    if (defn().breakdoors)
			break;
		    if (defn().useitems && locked)
		    {
			// Can pick the door...
			if (hasUnbrokenItem(ITEM_LOCKPICK))
			    return true;
		    }
		    // Do not attempt if a mob.
		    return false;
	    }
	}
	else
	{
	    // Failed...
	    return false;
	}
    }

    if (checkmob && pos.mob())
	return false;

    return true;
}

void
MOB::move(POS newpos, bool ignoreangle)
{
    // If we have swallowed something, move it too.
    if (!isSwallowed())
    {
	PTRLIST<MOB *>	moblist;
	int		i;

	pos().getAllMobs(moblist);
	for (i = 0; i < moblist.entries(); i++)
	{
	    if (moblist(i)->isSwallowed())
	    {
		moblist(i)->move(newpos, true);
	    }
	}
    }

    if (ignoreangle)
	newpos.setAngle(pos().angle());

    POS oldpos = pos();

    // Check if we are entering a new room...
    if (isAvatar() && (newpos.roomId() != pos().roomId()))
    {
	if (newpos.map())
	{
	    newpos.map()->expandActiveRooms(newpos.roomId(), 2);
	}
    }

    setPos(newpos);

    reportSquare(pos(), oldpos);
}

void
MOB::heardNoise(POS src, int loudness)
{
    if (hasItem(ITEM_ASLEEP))
    {
	if (loudness > rand_choice(4))
	{
	    destroyItem(ITEM_ASLEEP);
	}
    }
    else
    {
	// Do we attract monsters as well?
	if (!myTarget.valid())
	    myTarget = src;
    }
}

int
MOB::getFood() const
{
    return myFood;
}

void
MOB::gainFood(int food)
{
    FOODLEVEL_NAMES oldstatus = foodStatus();

    // We globally scale the value of the food to adjust the food
    // clock as the consumption clock is fixed...
    // food *= 2;
    myFood += food;
    // We allow ourselves to be oversatiated!
    if (oldstatus != foodStatus())
    {
	formatAndReport("%S <be> now %o.", foodStatusDescr());
    }
}

void
MOB::eatFood(int food)
{
    FOODLEVEL_NAMES oldstatus = foodStatus();
    myFood -= food;
    if (myFood < 0)
    {
	myFood = 0;
    }
    if (oldstatus != foodStatus())
    {
	formatAndReport("%S <be> now %o.", foodStatusDescr());
    }
}

int
MOB::getMaxFood() const
{
    return 200;
}

const char *
MOB::healthStatusDescr() const
{
    HEALTHLEVEL_NAMES l = healthStatus();

    if (defn().isconstruct)
	return glb_healthleveldefs[l].construct;
    return glb_healthleveldefs[l].normal;
}

HEALTHLEVEL_NAMES
MOB::healthStatus() const
{
    // THe order is somewhat important to make sure 1 hp creatures
    // with max 1 hp aren't at death's gate.
    int		hp = getHP();
    int		maxhp = getMaxHP();

    if (hp <= 0)
	return HEALTHLEVEL_DEAD;
    if (hp >= maxhp)
	return HEALTHLEVEL_FULL;
    if (hp == 1)
	return HEALTHLEVEL_DEATHSGATE;

    // Rest is vaguely by percentage...
    float frac = (float)hp / (float)maxhp;

    if (frac > 0.75)
	return HEALTHLEVEL_SCRATCHED;
    if (frac > 0.5)
	return HEALTHLEVEL_BRUISED;
    if (frac > 0.3)
	return HEALTHLEVEL_BLOODIED;
    if (frac > 0.2)
	return HEALTHLEVEL_INJURED;
    if (frac > 0.1)
	return HEALTHLEVEL_SEVERELYINJURED;

    return HEALTHLEVEL_CRITICALLYINJURED;
}

const char *
MOB::manaStatusDescr() const
{
    MANALEVEL_NAMES l = manaStatus();

    return glb_manaleveldefs[l].normal;
}

MANALEVEL_NAMES
MOB::manaStatus() const
{
    int		mp = getMP();
    int		maxmp = getMaxMP();

    if (mp < 5)
	return MANALEVEL_EMPTY;
    if (mp >= maxmp)
	return MANALEVEL_FULL;

    if (mp < 10)
	return MANALEVEL_LOW;
    if (mp < 15)
	return MANALEVEL_MED;

    return MANALEVEL_NORMAL;
}

const char *
MOB::foodStatusDescr() const
{
    FOODLEVEL_NAMES l = foodStatus();

    return glb_foodleveldefs[l].normal;
}

FOODLEVEL_NAMES
MOB::foodStatus() const
{
    // THe order is somewhat important to make sure 1 hp creatures
    // with max 1 hp aren't at death's gate.
    int		food = getFood();
    int		maxfood = getMaxFood();

    if (food <= 0)
	return FOODLEVEL_STARVING;
    if (food >= maxfood)
	return FOODLEVEL_SATIATED;
    if (food * 4 < maxfood)
	return FOODLEVEL_VERYHUNGRY;
    if (food * 2 < maxfood)
	return FOODLEVEL_HUNGRY;
    return FOODLEVEL_FULL;
}

int
MOB::getMaxHP() const
{
    return myMaxHP;
}

void
MOB::gainHP(int hp)
{
    int		maxhp;

    HEALTHLEVEL_NAMES oldstatus = healthStatus();

    maxhp = getMaxHP();

    if (hasItem(ITEM_PLAGUE))
    {
	// Do not heal as well when sick.
	if (hp > 0)
	    hp = (hp + 1) / 2;
    }

    myHP += hp;
    if (myHP > maxhp)
	myHP = maxhp;
    if (myHP < 0)
	myHP = 0;

    if (oldstatus != healthStatus())
	formatAndReport("%S <be> now %o.", healthStatusDescr());
}

int
MOB::getMaxMP() const
{
    return myMaxMP;
}

void
MOB::gainMP(int mp)
{
    int		maxmp;

    maxmp = getMaxMP();

    myMP += mp;
    if (myMP > maxmp)
	myMP = maxmp;
    if (myMP < 0)
	myMP = 0;
}


bool
MOB::putToSleep(bool doresistcheck, int checkdc, int timer, bool silent)
{
    if (hasItem(ITEM_ASLEEP))
    {
        // Sleeping critters blissfully unware.
        ITEM *item = lookupItem(ITEM_ASLEEP);
        if (item->getTimer() > 0)
        {
            if (timer >= 0)
                item->addTimer(timer);
            else
                item->setTimer(timer);          // permanent
        }
        formatAndReport("%S <snore>.");
    }
    else if (!defn().cansleep)
    {
        if (!silent)
            formatAndReport("%S <be> unaffected.");
    }
    else if (!doresistcheck ||
            rand_choice(getLevel() + defn().depth) <= rand_choice(checkdc))
    {
        ITEM *sleep = giftItem(ITEM_ASLEEP);
        sleep->setTimer(timer);
    }
    else
    {
        formatAndReport("%S <resist>.");
    }
    return true;
}

void
MOB::postEvent(EVENTTYPE_NAMES type, u8 sym, ATTR_NAMES attr, const char *text) const
{
    if (pos().map())
	pos().map()->getDisplay()->postEvent(EVENT((MOB *)this, sym, attr, type, text));
}

void
MOB::doEmote(const char *text)
{
    postEvent( (EVENTTYPE_NAMES) (EVENTTYPE_SHOUT | EVENTTYPE_LONG),
		    ' ', ATTR_EMOTE,
		    text);
}

void
MOB::doShout(const char *text)
{
    postEvent( (EVENTTYPE_NAMES) (EVENTTYPE_SHOUT | EVENTTYPE_LONG),
		    ' ', ATTR_SHOUT,
		    text);
}

BUF
MOB::buildEffectDescr(EFFECT_NAMES effect)
{
    BUF		descr;
    EFFECT_DEF	*def = GAMEDEF::effectdef(effect);

    if (effect == EFFECT_NONE)
	return descr;
    switch (def->type)
    {
	case NUM_EFFECTCLASSS:
	case EFFECTCLASS_NONE:
	    break;
	case EFFECTCLASS_HEAL:
	    descr.sprintf("Heals %s.  ",
			    def->amount.format().buffer());
	    break;
	case EFFECTCLASS_GAINMANA:
	    descr.sprintf("Regains %s mana.  ",
			    def->amount.format().buffer());
	    break;
	case EFFECTCLASS_LOSEMANA:
	    descr.sprintf("Drains %d (%s) mana.  ",
			    def->amount.format().buffer());
	    break;
	case EFFECTCLASS_GAINFOOD:
	    descr.sprintf("Creates %s food.  ",
			    def->amount.format().buffer());
	    break;
	case EFFECTCLASS_LOSEFOOD:
	    descr.sprintf("Consumes %s food.  ",
			    def->amount.format().buffer());
	    break;
	case EFFECTCLASS_POISON:
	    descr.sprintf("Poisons for %s turns.  ",
			    def->duration.format().buffer());
	    break;
	case EFFECTCLASS_CURE:
	    descr.sprintf("Cures poison%s.  ",
			    "");
	    break;
	case EFFECTCLASS_DAMAGE:
	    descr.sprintf("Causes %s %s damage.  ",
			def->amount.format().buffer(),
			GAMEDEF::elementdef(def->element)->name);
	    break;
	case EFFECTCLASS_SCALED_DAMAGE:
	    descr.sprintf("Causes %s%% damage.  ",
			def->amount.format().buffer(),
			GAMEDEF::elementdef(def->element)->name);
	    break;
	case EFFECTCLASS_RESIST:
	    descr.sprintf("Grant resist %s for %s turns.  ",
			GAMEDEF::elementdef(def->element)->name,
			def->duration.format().buffer());
	    break;
	case EFFECTCLASS_VULNERABLE:
	    descr.sprintf("Grant vulnerable to %s for %s turns.  ",
			GAMEDEF::elementdef(def->element)->name,
			def->duration.format().buffer());
	    break;
	case EFFECTCLASS_GAINITEM:
	    descr.sprintf("Grants %s for %s turns.  ",
			    GAMEDEF::itemdef(def->itemflag)->name,
			    def->duration.format().buffer());
	    break;
    }

    if (def->next != EFFECT_NONE)
    {
	BUF furtherdescr = buildEffectDescr(def->next);

	BUF total;
	total.sprintf("%s%s", descr.buffer(), furtherdescr.buffer());
	descr = total;
    }

    return descr;
}

bool
MOB::applyEffect(MOB *src, EFFECT_NAMES effect, ATTACKSTYLE_NAMES attackstyle)
{
    if (effect == EFFECT_NONE)
	return false;

    EFFECT_DEF	*def = GAMEDEF::effectdef(effect);

    switch (def->type)
    {
	case NUM_EFFECTCLASSS:
	case EFFECTCLASS_NONE:
	    break;

	case EFFECTCLASS_HEAL:
	{
	    BUF		healstring;
	    int		heal = def->amount.roll();
	    
	    // heal = MIN(heal, getMaxHP()-getHP());

	    healstring.sprintf("%%S <heal> %d damage.", heal);
	    formatAndReport(healstring);
	    gainHP(heal);
	    break;
	}

	case EFFECTCLASS_GAINMANA:
	{
	    BUF		healstring;
	    int		heal = def->amount.roll();

	    healstring.sprintf("%%S <regain> %d mana.", heal);
	    formatAndReport(healstring);
	    gainMP(heal);
	    break;
	}

	case EFFECTCLASS_LOSEMANA:
	{
	    BUF		healstring;
	    int		heal = def->amount.roll();
	    healstring.sprintf("%%S <be> drained of %d mana.", heal);
	    formatAndReport(healstring);
	    gainMP(-heal);
	    break;
	}

	case EFFECTCLASS_GAINFOOD:
	{
	    BUF		healstring;
	    int		heal = def->amount.roll();

	    heal = MIN(heal, getMaxFood()-getFood());

	    healstring.sprintf("%%S <gain> %d food.", heal);
	    formatAndReport(healstring);
	    gainFood(heal);
	    break;
	}

	case EFFECTCLASS_LOSEFOOD:
	{
	    BUF		healstring;
	    int		heal = def->amount.roll();
	    heal = MIN(heal, getFood());

	    healstring.sprintf("%%S <be> drained of %d food.", heal);
	    formatAndReport(healstring);
	    eatFood(heal);
	    break;
	}

	case EFFECTCLASS_CURE:
	{
	    ITEM		*item = lookupItem(ITEM_POISON);

	    if (!item)
	    {
		formatAndReport("%S <feel> healthy.");
	    }
	    else
	    {
		removeItem(item);
		delete item;
	    }
	    break;
	}

	case EFFECTCLASS_DAMAGE:
	{
	    int damage = def->amount.roll();
	    if (src)
	    {
		src->formatAndReport("%S %v %O.", GAMEDEF::elementdef(def->element)->damageverb, this);
	    }
	    else
	    {
		formatAndReport("%S <be> %V.", GAMEDEF::elementdef(def->element)->damageverb, this);
	    }
	    if (applyDamage(src, damage, def->element, attackstyle))
		return true;
	    break;
	}

	case EFFECTCLASS_SCALED_DAMAGE:
	{
	    int damage = def->amount.roll();
            damage = (getHP() * damage) / 100;
            if (!damage) damage = 1;
	    if (src)
	    {
		src->formatAndReport("%S %v %O.", GAMEDEF::elementdef(def->element)->damageverb, this);
	    }
	    else
	    {
		formatAndReport("%S <be> %V.", GAMEDEF::elementdef(def->element)->damageverb, this);
	    }
	    if (applyDamage(src, damage, def->element, attackstyle))
		return true;
	    break;
	}

	case EFFECTCLASS_RESIST:
	case EFFECTCLASS_VULNERABLE:
	case EFFECTCLASS_GAINITEM:
	case EFFECTCLASS_POISON:
	{
	    ITEM_NAMES		 itemname;
	    // Probably should have all been GAINITEM to begin with!
	    if (def->type == EFFECTCLASS_RESIST)
		itemname = ITEM_RESIST;
	    else if (def->type == EFFECTCLASS_VULNERABLE)
		itemname = ITEM_VULNERABLE;
	    else if (def->type == EFFECTCLASS_GAINITEM)
		itemname = def->itemflag;
	    else if (def->type == EFFECTCLASS_POISON)
		itemname = ITEM_POISON;
	    else
	    {
		J_ASSERT(!"Unhandled effect class");
		break;
	    }

	    // If have the corresponding resistance item, skip.
	    if (hasItem(def->itemresist))
		break;

	    ITEM		*item = ITEM::create(map(), itemname);

	    int		duration = def->duration.roll();
	    item->setTimer(duration);
	    item->setElement(def->element);

	    addItem(item);
	    break;
	}
    }

    // Chain the effect (still alive as we'd return if we died)
    if (def->next != EFFECT_NONE)
    {
	return applyEffect(src, def->next, attackstyle);
    }

    return false;
}

bool
MOB::isVulnerable(ELEMENT_NAMES element) const
{
    if (defn().vulnerability == element)
	return true;

    // Check any items...
    for (int i = 0; i < myInventory.entries(); i++)
    {
	ITEM	*item = myInventory(i);
	if (item->getDefinition() == ITEM_VULNERABLE)
	{
	    if (item->element() == element)
		return true;
	}
    }
    return false;
}

bool
MOB::applyRestoration(MOB *src)
{
    bool		effect = false;
    if (hasItem(ITEM_PLAGUE))
    {
	removeItem(lookupItem(ITEM_PLAGUE));
	effect = true;
    }
    if (hasItem(ITEM_POISON))
    {
	removeItem(lookupItem(ITEM_POISON));
	effect = true;
    }

    if (hasItem(ITEM_POLYMORPH))
    {
	actionUnpolymorph(src);
	// Might be gone now.
	return true;
    }

    return effect;
}

bool
MOB::isAvatarBlind() const
{
    if (isAvatar()) return isBlind();
    if (!map()) return false;
    if (!map()->avatar()) return false;
    return map()->avatar()->isBlind();
}

bool
MOB::visibleFromAvatar() const
{
    // Always "see" yourself
    if (isAvatar()) return true;
    return pos().visibleFromAvatar();
}

bool
MOB::applyAttack(ATTACK_NAMES attack, MOB *src, 
		 ATTACKSTYLE_NAMES attackstyle, bool silent)
{
    ATTACK_DEF	*def = GAMEDEF::attackdef(attack);

    int		damage = def->damage.roll();
    if (damage)
    {
	// Non zero damage, so apply it.
	if (applyDamage(src, damage, def->element, attackstyle, silent))
	    return true;		// is ded
    }

    // Apply the effect.
    if (applyEffect(src, def->effect, attackstyle))
	return true;			// is dead

    return false;
}

bool
MOB::applyDamage(MOB *src, int hits, ELEMENT_NAMES element, ATTACKSTYLE_NAMES attackstyle, bool silent)
{
    // Being hit isn't boring.
    if (!silent)
    {
	clearBoredom();
	stopWaiting("%S <be> hit, so <end> %r rest.");
    }

    // Being hit will wake you up.
    destroyItem(ITEM_ASLEEP);

    if (hasItem(ITEM_INVULNERABLE))
	return false;

    {
	ITEM *armour = lookupArmour();
	if (armour && armour->getDefinition() == ITEM_STEEL_CLOAK)
	{
	    formatAndReport("%S <ignore> the attack.");
	    return false;
	}
    }

    // Check if wearing 

    if (element == ELEMENT_LIGHT && isBlind())
    {
	return false;
    }

    if (src && src->isAvatar() && src != this)
    {
	if (!myAngryWithAvatar && getAI() == AI_PEACEFUL)
	{
	    formatAndReport("%S <grow> angry with %o!", src);
	    myAngryWithAvatar = true;
	}
	// Reset our AI target to the source so we don't pick
	// up the arrow that just hit us.
	myTarget = src->pos();
    }

    // Adjust damage down for armour.
    float		 multiplier = 1;
    int			 reduction = 0;

    reduction = getDamageReduction(element);

    // No armour if the attack came from within.
    // But magical elemental resistance from rings should handle this,
    // and get damage reduction already ignores non-physical elements
    // for armour...
    //if (attackstyle == ATTACKSTYLE_INTERNAL)
    //	  reduction = 0;
    
    // Check for resist/vulnerability
    if (element != ELEMENT_NONE)
    {
	if (defn().vulnerability == element)
	    multiplier += 1;
	if (defn().resistance == element)
	    multiplier -= 1;

	// Check any items...
	for (int i = 0; i < myInventory.entries(); i++)
	{
	    ITEM	*item = myInventory(i);
	    if (item->getDefinition() == ITEM_RESIST)
	    {
		if (item->element() == element)
		    multiplier -= 1;
	    }
	    if (item->getDefinition() == ITEM_VULNERABLE)
	    {
		if (item->element() == element)
		    multiplier += 1;
	    }
	}
	if (multiplier < 0)
	    multiplier = 0;
    }

    if (multiplier == 0)
    {
	formatAndReport("%S <ignore> the attack.");
	return false;
    }

    if (multiplier != 1.0f)
    {
	float		fhits = hits * multiplier;

	hits = int(fhits);
	fhits -= hits;

	// Add the round off error.
	if (rand_double() < fhits)
	    hits++;
    }

    if (hasItem(ITEM_PLAGUE))
    {
	// Take extra damage.
	hits += hits / 3;
    }

    // Add in reduction
    reduction = rand_range(0, reduction);
    hits -= reduction;
    if (hits < 0)
	hits = 0;

    if (hits >= getHP())
    {
	// If we are polyd, and have a previous form...
	if (hasItem(ITEM_POLYMORPH) && myOrigBody)
	{
	    actionUnpolymorph(src);

	    return true;
	}

	// If we die, but have an extra life...
	if (getExtraLives())
	{
	    formatAndReport("%S <fall> to the ground dead.");
	    formatAndReport("%S <glow> brightly and <return> to life!");
	    if (isAvatar())
	    {
		text_registervariable("MOB", getName());
		glbEngine->popupText(text_lookup("death", "Another Chance"));
	    }
	    // All flags are reset with our new life, both bonus an malus.
	    loseTempItems();

	    myExtraLives--;
	    gainHP(getMaxHP() - getHP());
	    return true;
	}
	kill(src);
	return true;
    }

    // If the attack element is light, they are now blind
    if (element == ELEMENT_LIGHT)
    {
	giftItem(ITEM_BLIND);
    }

    // They lived.  They get the chance to yell.
    if (src && src->isAvatar() && isFriends(src) && !isAvatar())
    {
	// This is a free action.
	actionYell(YELL_MURDERER);
	giftItem(ITEM_ENRAGED);
    }

    // Flash that they are hit.
    if (hits && !silent)
	pos().postEvent(EVENTTYPE_FOREBACK, ' ', ATTR_HEALTH);

    HEALTHLEVEL_NAMES oldstatus = healthStatus();
    myHP -= hits;
    if (oldstatus != healthStatus())
	formatAndReport("%S <be> now %o.", healthStatusDescr());

    return false;
}

void
MOB::doDeathDrop()
{
    // Acquire any loot.
    int		gold = 0;
    if (defn().loot >= 0)
	gold = rand_choice(defn().loot+1);
    if (gold)
    {
	gainGold(gold);
    }

    // Drop any stuff we have.
    int i;
    for (i = 0; i < myInventory.entries(); i++)
    {
	myInventory(i)->setEquipped(false);
	myInventory(i)->setWielded(false);
	if (myInventory(i)->isFlag())
	    delete myInventory(i);
	else
	    myInventory(i)->move(pos());
    }
    myInventory.clear();
}

void
MOB::kill(MOB *src)
{
    // Ensure we are dead to alive()
    myHP = 0;
    myNumDeaths++;
    // Record for inverse monster memory.
    edefn().totalkillsever++;
    // Rather mean.
    myMP = 0;

    // Death!
    if (src)
	src->formatAndReport("%S <kill> %O!", this);
    else
	formatAndReport("%S <be> killed!");

    // If there is a source, and they are swallowed,
    // they are released.
    if (src && src->isSwallowed())
    {
	src->setSwallowed(false);
    }
    
    // If we are the avatar, special stuff happens.
    if (isAvatar())
    {
	// never update on this thread!
	// Swap out someone else to be our leader!
	// Only if everyone is gone do we fail

	// msg_update();
	// TODO: Pause something here?
    }
    else
    {
	doDeathDrop();
    }

    // Really should be assigned to the source, but most creatures
    // don't fight and you should get exp for things you kill
    // indirectly...
    // We don't want to get experience for killing ourselves though,
    // despite it being quite an experience.  As if you also gain a level
    // at the same time you'll then insta-revive.
    if (getAvatar() && !isAvatar())
    {
	// Assign exp based on target's level.
	// Because the same number of mobs is spread out we give a boost
	// to exp here.
	getAvatar()->gainExp( 2 * (10 + defn().depth + getLevel()), /*silent=*/ false );
	// getAvatar()->gainExp( 100 + defn().depth + getLevel(), /*silent=*/ false );
    }

    if (src && src->isAvatar())
    {
	// Award our score.

    }

    // Flash the screen
    pos().postEvent(EVENTTYPE_FOREBACK, ' ', ATTR_INVULNERABLE);

    // Note that avatar doesn't actually die...
    if (!isAvatar())
    {
	// Anyone who witnessed this can yell.
	if (src && src->isAvatar() && isFriends(src))
	{
	    MOBLIST		allmobs;

	    pos().map()->getAllMobs(allmobs);

	    for (int i = 0; i < allmobs.entries(); i++)
	    {
		if (allmobs(i)->pos().isFOV() && allmobs(i) != this &&
		    !allmobs(i)->isAvatar() &&
		    allmobs(i)->alive() &&
		    allmobs(i)->isFriends(src))
		{
		    allmobs(i)->actionYell(YELL_MURDERER);
		    allmobs(i)->giftItem(ITEM_ENRAGED);
		    break;
		}
	    }
	}

	// Don't actually delete, but die.
	MAP		*map = pos().map();

	{
	    if (rand_chance(defn().corpsechance))
	    {
		ITEM	*corpse = ITEM::create(map, ITEM_CORPSE);
		corpse->setMobType(getDefinition());

		corpse->move(pos());
	    }
	}

	myPos.removeMob(this);
	map->addDeadMob(this);
	clearAllPos();
	loseTempItems();
    }
    else
    {
	// Either the avatar, or someone in the party dies.
	// If it is the avatar it is the main player so we have more
	// work to do.
	bool	avatardeath = isAvatar();

	// Make sure we drop our blind attribute..
	loseTempItems();

	// End any meditation
	myMeditatePos = POS();

	// No matter what, the source sees it (we may be meditating)
	if (src && avatardeath)
	    src->mySawVictory = true;

	if (pos().isFOV() && avatardeath)
	{
	    MOBLIST		allmobs;

	    pos().map()->getAllMobs(allmobs);

	    for (int i = 0; i < allmobs.entries(); i++)
	    {
		if (allmobs(i)->pos().isFOV())
		    allmobs(i)->mySawVictory = true;
	    }
	}

	// Now activate the next party member if we have more than one.
	MAP *map = pos().map();
	POS  thispos = pos();

	{
	    if (rand_chance(defn().corpsechance))
	    {
		ITEM	*corpse = ITEM::create(map, ITEM_CORPSE);
		corpse->setMobType(getDefinition());

		corpse->move(thispos);
	    }
	}

	// Total party kill and no one in town.
	// We have to make the dead avatar active in the party
	// so that findAvatar() will locate a dead avatar
	// and we trigger the end conditions.
    }
}

VERB_PERSON
MOB::getPerson() const
{
    if (isAvatar())
	return VERB_YOU;

    return VERB_IT;
}

bool
MOB::isFriends(const MOB *other) const
{
    if (other == this)
	return true;

    if (hasItem(ITEM_ENRAGED) || other->hasItem(ITEM_ENRAGED))
	return false;
    
    if (isAvatar())
    {
	if (other->defn().isfriendly)
	    return true;
	else
	    return false;
    }
    if (other->isAvatar())
    {
	if (defn().isfriendly)
	    return true;
	else
	    return false;
    }

    // Friendly hate non friendly, vice versa.
    return defn().isfriendly == other->defn().isfriendly;
}

AI_NAMES
MOB::getAI() const
{
    return defn().ai;
}

void
MOB::reportSquare(POS t, POS oldpos)
{
    if (!isAvatar())
	return;

    if (glbConfig->myVerbose && oldpos.metaData() != t.metaData())
    {
	CHAMBER_NAMES	chamber = t.chamber();
	if (oldpos.depth() != t.depth())
	{
	    // Reported by map.
	}
	else if (chamber != CHAMBER_NONE)
	    formatAndReport("%S <enter> %o.",
			glb_chamberdefs[chamber].descr);
    }

    if (t.defn().describe || glbConfig->myVerbose)
    {
	formatAndReport("%o.", t.defn().legend);
    }

    bool	isblind = isBlind();

    if (t.mob() && t.mob() != this)
    {
	if (isblind)
	    formatAndReport("%S <feel> %O.", t.mob());
	else
	    formatAndReport("%S <see> %O.", t.mob());
    }
    // Can't sense ground items when swallowed!
    if (t.item() && !isSwallowed())
    {
	ITEMLIST	itemlist;

	t.allItems(itemlist);
	if (itemlist.entries())
	{
	    BUF		msg;
	    if (isblind)
		msg.strcpy("%S <feel> ");
	    else
		msg.strcpy("%S <see> ");
	    for (int i = 0; i < itemlist.entries(); i++)
	    {
		if (i)
		{
		    if (i == itemlist.entries()-1)
			msg.strcat(" and ");
		    else
			msg.strcat(", ");
		}

		msg.strcat(itemlist(i)->getArticleName());
	    }

	    msg.strcat(".");
	    formatAndReport(msg);
	}
    }
}

bool
MOB::shouldPush(MOB *victim) const
{
    // Global rank:
    return true;
}

void
MOB::meditateMove(POS t, bool decouple)
{
    POS oldpos = myMeditatePos;
    myMeditatePos = t;
    myDecoupledFromBody = decouple;
    // Don't spam because we have no cursor.
    // reportSquare(t, oldpos);
}


void
MOB::searchOffset(int dx, int dy, bool silent)
{
    POS		square = pos().delta(dx, dy);

    // If the square isn't seen, we can't search it.
    if (!square.isFOV())
	return;

    if (square.isTrap())
    {
	TRAP_NAMES		trap = (TRAP_NAMES) rand_choice(NUM_TRAPS);

	formatAndReport("%S find %O and disarm it.", glb_trapdefs[trap].name);
	square.clearTrap();

	square.postEvent((EVENTTYPE_NAMES)(EVENTTYPE_ALL | EVENTTYPE_LONG), 
			glb_trapdefs[trap].sym,
			glb_trapdefs[trap].attr);
    }
    else if (square.tile() == TILE_SECRETDOOR)
    {
	formatAndReport("%S <reveal> a secret door.");
	square.setTile(TILE_DOOR_CLOSED);
	square.postEvent(EVENTTYPE_FOREBACK, '+', ATTR_SEARCH);
    }
    else
    {
	if (!silent)
	    square.postEvent(EVENTTYPE_FOREBACK, ' ', ATTR_SEARCH);
    }
}


int
MOB::rollStrengthPoints() const
{
    int points = 0;
    // 4 / 25 is a good starter.
    for (int attempts = 5; attempts --> 0; )
	points += rand_chance(34 + getLevel());

    return points;
}


ATTACK_NAMES
MOB::getMeleeAttack() const
{
    ITEM	*w;

    w = lookupWeapon();
    if (!w)
	return defn().melee_attack;

    return w->getMeleeAttack();
}

int
MOB::getMeleeCanonicalDamage() const
{
    ITEM	*weap;

    weap = lookupWeapon();

    int		base = attackdefn().damage.buildDPDF().expectedValueRounded();
    if (weap)
	base += weap->getBonus();
    return base;
}

const char *
MOB::getMeleeVerb() const
{
    return attackdefn().verb;
}

ELEMENT_NAMES
MOB::getMeleeElement() const
{
    ATTACK_NAMES	attack = getMeleeAttack();

    return GAMEDEF::attackdef(attack)->element;
}

const char *
MOB::getMeleeWeaponName() const
{
    ITEM	*w;

    w = lookupWeapon();
    if (!w)
	return attackdefn().noun;

    return w->defn().name;
}

int
MOB::evaluateMeleeDamage() const
{
    ATTACK_NAMES	attack = getMeleeAttack();

    return evaluateAttackDamage(attack, lookupWeapon(), nullptr);
}

bool
MOB::evaluateMeleeWillHit(MOB *victim) const
{
    ATTACK_NAMES	attack = getMeleeAttack();

    if (victim && victim->hasItem(ITEM_ASLEEP))
	return true;

    return evaluateWillHit(attack, lookupWeapon(), nullptr, victim);
}

bool
MOB::evaluateWillHit(ATTACK_NAMES attack, ITEM *weap, ITEM *ammo, MOB *victim) const
{
    // We can always hit inanimate objects?
    if (!victim)
	return true;

    ATTACK_DEF	*def = GAMEDEF::attackdef(attack);

    int		tohit = 10;

    tohit -= def->chancebonus;
    tohit += victim->defn().dodgebonus;
    ITEM *armour = victim->lookupArmour();
    if (armour)
    {
	tohit += armour->getArmourClass(def->damageclass);
	if (victim->pos().isFOV() || pos().isFOV())
	    armour->markBonusKnown();
    }

    // We have the difference so a factor of 20 is overwhelming.
    if (weap)
    {
	tohit -= weap->getBonus();
	if (victim->pos().isFOV() || pos().isFOV())
	    weap->markBonusKnown();
	if (!isProficientWith(weap))
	    tohit += 10;
    }
    if (ammo)
    {
	tohit -= ammo->getBonus();
	if (victim->pos().isFOV() || pos().isFOV())
	    ammo->markBonusKnown();
    }

    // We use our depth, not our level, so as not to overpower the
    // avatar but ensure things can punch through plate eventually.
    tohit -= defn().depth/2;
    tohit -= getLevel()/3;	// Slow thac0...


    // I keep turning back to the d20 system.  The simple fact is
    // that 5% is right at the human threshold for noticing, so is
    // by far the most convenient to express our boni with.
    int		dieroll = rand_range(1, 20);

    if (dieroll != 1 &&
	(dieroll == 20 || (dieroll >= tohit)))
	return true;
    return false;
}

int
MOB::getRangedCanonicalDamage() const
{
    ITEM	*weap;

    weap = lookupRanged();

    int		base = rangeattackdefn().damage.buildDPDF().expectedValueRounded();
    if (weap)
	base += weap->getBonus();

    return base;
}


int
MOB::evaluateAttackDamage(ATTACK_NAMES attack, ITEM *weap, ITEM *ammo) const
{
    ATTACK_DEF	*def = GAMEDEF::attackdef(attack);

    int		base = def->damage.roll();
    if (weap)
    {
	base += weap->getBonus();
	if (pos().isFOV())
	    weap->markBonusKnown();
    }
    if (ammo)
    {
	// Don't apply full bonus as you get a sick stack
	// between the ammo and the launcher.
	base += rand_choice(ammo->getBonus());
	if (pos().isFOV())
	    ammo->markBonusKnown();
    }

    return base;
}

int
MOB::evaluateDistribution(int amount, DISTRIBUTION_NAMES distribution)
{
    int		base = amount;

    switch (distribution)
    {
	case NUM_DISTRIBUTIONS:
	    break;
	case DISTRIBUTION_CONSTANT:
	    // damage is just damage
	    break;
	case DISTRIBUTION_UNIFORM:
	    // Uniform in double range gives same average.
	    base = rand_range(0, base * 2-1);
	    base++;
	    break;
	case DISTRIBUTION_BIMODAL:
	{
	    // Half the time we do a low-end distribution,
	    // half the time a high end.
	    if (rand_choice(2))
	    {
		// We don't double here, but add 50 percent
		// so our average matches.
		base += base / 2;
	    }
	    else
	    {
		base = base/2;
		if (!base)
		    base = 1;
	    }
		
	    //FALL THROUGH
	}
	case DISTRIBUTION_GAUSSIAN:
	{
	    // Sum of four uniforms.
	    int		total = 0;
	    total += rand_range(0, base * 2-1);
	    total += rand_range(0, base * 2-1);
	    total += rand_range(0, base * 2-1);
	    total += rand_range(0, base * 2-1);

	    total += 2;
	    total /= 4;

	    base = total + 1;
	    break;
	}
    }

    return base;
}

ATTACK_NAMES
MOB::getRangedAttack() const
{
    ITEM	*w;

    w = lookupRanged();
    if (!w)
	return defn().range_attack;

    return w->getRangeAttack();
}

int
MOB::getRangedRange() const
{
    ITEM	*w;

    w = lookupRanged();
    if (!w)
	return defn().range_range;

    return w->getRangeRange();
}

int
MOB::getRangedArea() const
{
    ITEM	*w;

    w = lookupRanged();
    if (!w)
	return defn().range_area;

    return w->getRangeArea();
}

const char *
MOB::getRangedWeaponName() const
{
    ITEM	*w;

    w = lookupRanged();
    if (!w)
	return rangeattackdefn().noun;

    return w->defn().name;
}

const char *
MOB::getRangedVerb() const
{
    return rangeattackdefn().verb;
}

const char *
MOB::getRangedFireball() const
{
    return rangeattackdefn().fireball;
}


ELEMENT_NAMES
MOB::getRangedElement() const
{
    return rangeattackdefn().element;
}

void
MOB::getRangedLook(u8 &symbol, ATTR_NAMES &attr) const
{
    ITEM	*w;

    w = lookupRanged();
    if (!w)
    {
	symbol = defn().range_symbol;
	attr = defn().range_attr;
    }
    else
    {
	symbol = w->defn().range_symbol;
	attr = w->defn().range_attr;
    }
}

BUF
MOB::getRangedAnnounce() const
{
    BUF		result;
    ITEM *w = lookupRanged();
    if (!w)
	result.reference(defn().range_announce);
    else
	result.reference(w->defn().range_announce);
	
    return result;
}

bool
MOB::roleListContains(ROLELIST_NAMES roles, ROLE_NAMES role)
{
    // QUestionable??
    if (role == ROLE_NONE)
	return true;

    while (roles != ROLELIST_NONE)
    {
	if (glb_rolelistdefs[roles].role == role)
	    return true;
	roles = glb_rolelistdefs[roles].next;
    }
    return false;
}

bool
MOB::isProficientWith(ITEM *item) const
{
    J_ASSERT(item);
    if (!item) return false;

    // We are fully proficient as we have only one role.
    return true;
//     return roleListContains(item->roles(), role());
}


void
MOB::triggerManaUse(SPELL_NAMES spell, int manacost)
{
}

int
MOB::learnChance(SPELL_NAMES spell) const
{
    int		hardness = GAMEDEF::spelldef(spell)->difficulty;

    int		level = getLevel() - 1;
    int		chance = 0;
    int		halflevel = 0;

    switch (hardness)
    {
	default:
	    J_ASSERT(!"Unhandled diffciulty");
	    // FALL THROUGH
	case 1:
	    chance = 50 + 5 * level;
	    break;
	case 2:
	    chance = 25 + 5 * level;
	    break;
	case 3:
	    chance = 5 + 5 * level;
	    break;
    }
    if (everKnewSpell(spell))
	chance += 25;
    return chance;
}

int
MOB::forgetChance(SPELL_NAMES spell) const
{
    // Don't forget spells.
    return 0;
    int		hardness = GAMEDEF::spelldef(spell)->difficulty;

    int		level = getLevel() - 1;
    int		chance = 0;
    int		halflevel = 0;

    switch (hardness)
    {
	default:
	    J_ASSERT(!"Unhandled diffciulty");
	    // FALL THROUGH
	case 1:
	    chance = 50 - 5 * level;
	    break;
	case 2:
	    chance = 75 - 5 * level;
	    break;
	case 3:
	    chance = 100 - 5 * level;
	    break;
    }
    return chance;
}

void
MOB::memorizeSpell(SPELL_NAMES spell)
{
    if (myKnownSpells[spell])
    {
	// Relearn.
	formatAndReport("%S <relearn> %o.", GAMEDEF::spelldef(spell)->name);
	myKnownSpells[spell] = tolower(myKnownSpells[spell]);
    }
    else
    {
	// Find free slot...
	u8		minsymbol = 'a'-1;
	SPELL_NAMES	search;
	FOREACH_SPELL(search)
	{
	    if (myKnownSpells[search])
	    {
		u8 key = tolower(myKnownSpells[search]);
		minsymbol = MAX(key, minsymbol);
	    }
	}
	minsymbol++;
	myKnownSpells[spell] = minsymbol;
    }
}

bool
MOB::isSpellMemorized(SPELL_NAMES spell) const
{
    if (myKnownSpells[spell] >= 'a')
	return true;
    return false;
}

int
MOB::numSpellsMemorized() const
{
    SPELL_NAMES spell;
    int count = 0;
    FOREACH_SPELL(spell)
    {
	if (isSpellMemorized(spell))
	    count++;
    }
    return count;
}

int
MOB::maxSpellSlots() const
{
    return getLevel();
}

bool
MOB::everKnewSpell(SPELL_NAMES spell) const
{
    if (myKnownSpells[spell])
	return true;
    return false;
}

SPELL_NAMES
MOB::spellFromLetter(u8 letter) const
{
    if (!letter)
	return SPELL_NONE;

    SPELL_NAMES spell;
    FOREACH_SPELL(spell)
    {
	if (myKnownSpells[spell] == letter)
	    return spell;
    }
    return SPELL_NONE;
}

void
MOB::forgetSpell(SPELL_NAMES spell)
{
    if (myKnownSpells[spell])
    {
	// Forget!.
	formatAndReport("%o fades from %r mind.", GAMEDEF::spelldef(spell)->name);
	myKnownSpells[spell] = toupper(myKnownSpells[spell]);
    }
}

void
MOB::getKnownSpells(PTRLIST<SPELL_NAMES> &spells) const
{
    SPELL_NAMES spell;
    spells.clear();
    FOREACH_SPELL(spell)
    {
	if (isSpellMemorized(spell))
	{
	    spells.append(spell);
	}
    }
    // Sort by bound key
    spells.stableSort([self = this](SPELL_NAMES a, SPELL_NAMES b) -> bool
    {
	return self->spellLetter(a) < self->spellLetter(b);
    });
}

int
MOB::getGold() const
{
    return getCoin(MATERIAL_GOLD);
}

int
MOB::getCoin(MATERIAL_NAMES material) const
{
    ITEM		*gold;
    int			 ngold;

    gold = lookupItem(ITEM_COIN, material);
    
    ngold = 0;
    if (gold)
	ngold = gold->getStackCount();

    return ngold;
}

void
MOB::gainGold(int delta) 
{
    gainCoin(MATERIAL_GOLD, delta);
}

void
MOB::gainExp(int exp, bool silent)
{
    myExp += exp;

    int level = LEVEL::levelFromExp(myExp);
    while (level > myLevel)
	gainLevel(silent);
}

void
MOB::gainLevel(bool silent)
{
    myLevel++;
    if (!silent)
    {
	formatAndReport("%S <be> more experienced.");
	const char *msg = nullptr;
	static const char *msgs[] =
	{
	    "Strength is needed for success.",
	    "These skills I will put to use.",
	    "Foes should fear my advancement!",
	    "With power comes responsibility.",
	    0
	};
	msg = rand_string(msgs);

	if (msg)
	{
	    doShout(msg);
	}
    }

    // Increase max hp
    int         bonus = defn().hp_gain.roll();
    myMaxHP += bonus;

    // Full heal.  Will always report new health, which is good to point
    // out this feature...
    //gainHP(getMaxHP() - getHP());
    // Instead only gain your new hp
    gainHP(bonus);

    // Increase MP likewise
    bonus = defn().mp_gain.roll();
    myMaxMP += bonus;
    gainMP(bonus);
}

void
MOB::grantExtraLife()
{
    myExtraLives++;
    formatAndReport("%S <glow> from within, gaining the ability to thwart death!");
    gainHP(getMaxHP() - getHP());
}

void
MOB::gainCoin(MATERIAL_NAMES material, int delta)
{
    ITEM		*gold;

    if (!delta)
	return;

    gold = lookupItem(ITEM_COIN, material);
    if (!gold && delta>0)
    {
	gold = ITEM::create(map(), ITEM_COIN);
	gold->setMaterial(material);
	gold->setStackCount(0);
	addItem(gold);
    }
    if (!gold && delta<=0)
    {
	// Can't go negative?
	J_ASSERT(!"Negative gold?");
	return;
    }
    
    gold->setStackCount(gold->getStackCount() + delta);

    if (gold->getStackCount() <= 0)
    {
	// All gone, get rid of it so we don't have a No Coins entry.
	// (as cute as that is)
	removeItem(gold);
	delete gold;
    }
}

bool
MOB::castDestroyWalls()
{
    formatAndReport("The walls around %S shake.");

    bool		interesting;
    interesting = pos().forceConnectNeighbours();

    if (interesting)
    {
	formatAndReport("New passages open up.");
    }
    else
    {
	formatAndReport("Nothing happens.");
    }

    return true;
}

bool
MOB::knockBack(int dx, int dy)
{
    stopRunning();

    if (isSwallowed())
	return false;

    POS		 t;
    t = pos().delta(dx, dy);
    if (canMove(t, /*checkmob*/true, /*onlypassive*/true))
    {
	formatAndReport("%S <be> knocked back.");
	move(t);
	return true;
    }
    // no room just silently fails.
    return false;
}


void
MOB::updateEquippedItems()
{
    ITEM		*item;

    // Non avatars auto equip.
    if (!isAvatar() && !isAvatarDef())
    {
	// Mark everything unequipped
	for (int i = 0; i < myInventory.entries(); i++)
	{
	    item = myInventory(i);

	    if (!item->isRing())
	    {
		item->setEquipped(false);
		item->setWielded(false);
	    }
	}

	if (!defn().useitems)
	    return;

	// Equip our weapon and armour
	ITEM		*weap = 0, *arm = 0, *wand = 0;

	for (int i = 0; i < myInventory.entries(); i++)
	{
	    if (myInventory(i)->isWeapon() && !myInventory(i)->isBroken())
		weap = aiLikeMoreWeapon(myInventory(i), weap);
	}
	if (weap)
	    weap->setWielded(true);
	for (int i = 0; i < myInventory.entries(); i++)
	{
	    if (myInventory(i)->isRanged() && !myInventory(i)->isBroken())
		wand = aiLikeMoreWand(myInventory(i), wand);
	}
	if (wand)
	    wand->setEquipped(true);
	for (int i = 0; i < myInventory.entries(); i++)
	{
	    if (myInventory(i)->isArmour() && !myInventory(i)->isBroken())
		arm = aiLikeMoreArmour(myInventory(i), arm);
	}
	if (arm)
	    arm->setEquipped(true);
    }

    // Count total items
    int		total = 0;
    for (auto && item : inventory())
    {
	if (item->isFlag())
	    continue;
	total++;
	if (isAvatar() && !item->inventoryLetter())
	{
	    assignInventoryLetter(item);
	}
    }
    const int max_items = 26;
    if (total > max_items)
    {
	// Yay!  Chacne to use count down operator.
	// We want to run in reverse here.
	formatAndReport("%r backpack is out of room!");
	for (int i = myInventory.entries(); i --> 0; )
	{
	    item = myInventory(i);
	    if (item->isFlag())
		continue;

	    formatAndReport("%S <drop> %O.", item);
	    item->move(pos());
	    item->setEquipped(false);
	    item->setWielded(false);
	    myInventory.removeAt(i);

	    total--;
	    if (total <= max_items)
		break;
	}
    }
}

void
MOB::updateEquippedPostPoly()
{
    ITEMLIST	newinv;

    BUF		droplist;
    BUF		lastdrop;
    bool	dropcomma = false;

    for (auto && item : myInventory)
    {
	if (item->isFlag())
	{
	    newinv.append(item);
	    continue;
	}

	if (item->size() > size())
	{
	    // Drop as too big.
	    item->setEquipped(false);
	    item->setWielded(false);
	    if (lastdrop.isstring())
	    {
		if (droplist.isstring())
		{
		    droplist.appendSprintf(", %s", lastdrop.buffer());
		    dropcomma = true;
		}
		else
		    droplist = lastdrop;
	    }
	    lastdrop = item->getArticleName();
	    // Drop to ground.
	    item->move(pos());
	    // Continue without adding to our newinv!
	    continue;
	}

	newinv.append(item);
    }
    myInventory = newinv;

    for (auto && item : myInventory)
    {
	// Possibly unwield/equip if can't use items.
	if (item->isWielded() && !defn().canwield)
	{
	    formatAndReport("%S <can> no longer wield %O.", item);
	    item->setWielded(false);
	}
	if (item->isEquipped() && !defn().canequip)
	{
	    formatAndReport("%S <can> no longer wear %O.", item);
	    item->setEquipped(false);
	}
    }

    if (lastdrop.isstring())
    {
	if (droplist.isstring())
	{
	    // Yes, I care this much about the oxford comma!
	    if (dropcomma)
		droplist.strcat(", and ");
	    else
		droplist.strcat(" and ");
	}
	droplist.strcat(lastdrop);
	droplist.prefix("%S <drop> ");
	droplist.strcat(" as it is now too heavy.");
	formatAndReport(droplist);
    }

    // Fall to the normal update to possilby re-equip something
    // you are big enough to wield.
    updateEquippedItems();
}

bool
MOB::roomInBackpackForItem(ITEM *item) const
{
    // Flags are always "carriable"
    if (item->isFlag()) return true;
    // Non-avatar don't have letter limits
    if (!isAvatar()) return true;

    // First, check to see if we can merge...
    for (auto && inv : inventory())
    {
	if (item->canStackWith(inv))
	{
	    return true;
	}
    }

    // If the item has a letter assigned and that letter is free
    // we can pick up...
    if (item->inventoryLetter() &&
	!itemFromLetter(item->inventoryLetter()))
    {
	return true;
    }

    // We need a new letter...
    return anyFreeInventoryLetter();
}

void
MOB::addItem(ITEM *item)
{
    int			 i;

    if (!item)
	return;
    
    // Alert the world to our acquirement.
    if (isAvatar() || pos().isFOV())
	if (item->defn().gaintxt)
	    formatAndReport(item->defn().gaintxt, item);

    // First, check to see if we can merge...
    for (i = 0; i < myInventory.entries(); i++)
    {
	if (item->canStackWith(myInventory(i)))
	{
	    myInventory(i)->combineItem(item);
	    delete item;
	    // Pop that inventory to the top.
	    ITEM *merge = myInventory(i);
	    if (isAvatar() && !merge->isFlag() && merge->inventoryLetter())
	    {
		BUF	buf;
		buf.sprintf("[%c] %s.", merge->inventoryLetter(), merge->getName().buffer());
		msg_report(buf);
	    }
	    myInventory.removePtr(merge);
	    myInventory.append(merge);
	    return;
	}
    }

    // Brand new item.
    if (isAvatar() && !item->isFlag())
    {
	// Check if we have a conflict... or unassigned
	if (!item->inventoryLetter() ||
	    itemFromLetter(item->inventoryLetter()))
	{
	    if (!assignInventoryLetter(item))
	    {
		formatAndReport("%S <lack> room to carry %O.", item);
		item->move(pos());
		return;
	    }
	}
	{
	    BUF	buf;
	    buf.sprintf("[%c] %s.", item->inventoryLetter(), item->getName().buffer());
	    msg_report(buf);
	}
    }
    myInventory.append(item);

    // If we didn't stack, we may need to update our best items.
    updateEquippedItems();
}

ITEM *
MOB::splitStack(ITEM *item, int newcount)
{
    // Must actually be an item in our pack!
    J_ASSERT(!item->pos().valid());
    J_ASSERT(ownsSpecificItem(item));
    if (item->getStackCount() > newcount)
    {
	ITEM	*result;

	item->decStackCount(newcount);
	result = item->createCopy();
	result->setStackCount(newcount);

	result->setEquipped(false);
	result->setWielded(false);

	return result;
    }
    else
    {
	item->setEquipped(false);
	item->setWielded(false);
	removeItem(item);
	return item;
    }
}

void
MOB::removeItem(ITEM *item, bool quiet)
{
    // Alert the world to our acquirement.
    if ((isAvatar() || pos().isFOV()) && !quiet)
	if (item->defn().losetxt)
	    formatAndReport(item->defn().losetxt, item);

    if (item)
    {
	item->setEquipped(false);
	item->setWielded(false);
    }
    myInventory.removePtr(item);

    updateEquippedItems();
}

void
MOB::loseAllItems()
{
    int		i;
    ITEM	*item;

    for (i = myInventory.entries(); i --> 0;)
    {
	item = myInventory(i);
	removeItem(item, true);
	delete item;
    }
}

void
MOB::loseTempItems()
{
    int		i;
    ITEM	*item;

    for (i = myInventory.entries(); i --> 0;)
    {
	item = myInventory(i);
	// All flags and timers are temp.
	if (item->getTimer() >= 0 || item->isFlag())
	{
	    removeItem(item, true);
	    delete item;
	}
    }
}

void
MOB::save(ostream &os) const
{
    int		val;
    u8		c;

    val = getDefinition();
    os.write((const char *) &val, sizeof(int));

    myName.save(os);

    myPos.save(os);
    myMeditatePos.save(os);
    myTarget.save(os);
    myHome.save(os);

    c = myWaiting;
    os.write((const char *) &c, sizeof(c));

    os.write((const char *) &myHP, sizeof(int));
    os.write((const char *) &myMaxHP, sizeof(int));
    os.write((const char *) &myMP, sizeof(int));
    os.write((const char *) &myMaxMP, sizeof(int));
    os.write((const char *) &myFood, sizeof(int));
    os.write((const char *) &myAIState, sizeof(int));
    os.write((const char *) &myFleeCount, sizeof(int));
    os.write((const char *) &myBoredom, sizeof(int));
    os.write((const char *) &myYellHystersis, sizeof(int));
    os.write((const char *) &myNumDeaths, sizeof(int));
    os.write((const char *) &myUID, sizeof(int));
    os.write((const char *) &mySeed, sizeof(int));
    os.write((const char *) &myLevel, sizeof(int));
    os.write((const char *) &myExp, sizeof(int));
    os.write((const char *) &myExtraLives, sizeof(int));
    os.write((const char *) &mySearchPower, sizeof(int));
    os.write((const char *) &myCowardice, sizeof(int));
    os.write((const char *) &myKills, sizeof(int));
    os.write((const char *) &myRangeTimeout, sizeof(int));
    os.write((const char *) &mySpellTimeout, sizeof(int));

    os.write((const char *) &myLastDir, sizeof(int));
    os.write((const char *) &myLastDirCount, sizeof(int));

    val = myHasTriedSuicide;
    os.write((const char *) &val, sizeof(int));
    val = myHasWon;
    os.write((const char *) &val, sizeof(int));
    val = mySkipNextTurn;
    os.write((const char *) &val, sizeof(int));

    val = isSwallowed();
    os.write((const char *) &val, sizeof(int));

    val = myAngryWithAvatar;
    os.write((const char *) &val, sizeof(int));

    val = mySeenAvatar;
    os.write((const char *) &val, sizeof(int));

    int			 numitem;
    int			 i;

    c = mySawMurder;
    os.write((const char *) &c, 1);
    c = mySawMeanMurder;
    os.write((const char *) &c, 1);
    c = mySawVictory;
    os.write((const char *) &c, 1);
    c = myAvatarHasRanged;
    os.write((const char *) &c, 1);

    YELL_NAMES		yell;
    FOREACH_YELL(yell)
    {
	c = myHeardYell[yell];
	os.write((const char *) &c, 1);
	c = myHeardYellSameRoom[yell];
	os.write((const char *) &c, 1);
    }

    SPELL_NAMES		spell;
    FOREACH_SPELL(spell)
    {
	c = myKnownSpells[spell];
	os.write((const char *) &c, 1);
    }

    {
	numitem = myOrigBody ? 1 : 0;
	os.write((const char *) &numitem, sizeof(int));
	if (numitem)
	{
	    myOrigBody->save(os);
	}
    }

    numitem = myInventory.entries();
    os.write((const char *) &numitem, sizeof(int));

    for (i = 0; i < myInventory.entries(); i++)
    {
	myInventory(i)->save(os);
    }
}

MOB *
MOB::load(istream &is)
{
    int		 val, num, i;
    u8		 c;
    MOB		*mob;

    mob = new MOB();

    is.read((char *)&val, sizeof(int));
    mob->myDefinition = (MOB_NAMES) val;

    if (mob->myDefinition >= GAMEDEF::getNumMob())
    {
	mob->myDefinition = MOB_NONE;
    }

    mob->myName.load(is);

    mob->myPos.load(is);
    mob->myMeditatePos.load(is);
    mob->myTarget.load(is);
    mob->myHome.load(is);

    is.read((char *) &c, sizeof(c));
    mob->myWaiting = c ? true : false;
    
    is.read((char *)&mob->myHP, sizeof(int));
    is.read((char *)&mob->myMaxHP, sizeof(int));
    is.read((char *)&mob->myMP, sizeof(int));
    is.read((char *)&mob->myMaxMP, sizeof(int));
    is.read((char *)&mob->myFood, sizeof(int));
    is.read((char *)&mob->myAIState, sizeof(int));
    is.read((char *)&mob->myFleeCount, sizeof(int));
    is.read((char *)&mob->myBoredom, sizeof(int));
    is.read((char *)&mob->myYellHystersis, sizeof(int));
    is.read((char *)&mob->myNumDeaths, sizeof(int));
    is.read((char *)&mob->myUID, sizeof(int));
    is.read((char *)&mob->mySeed, sizeof(int));
    is.read((char *)&mob->myLevel, sizeof(int));
    is.read((char *)&mob->myExp, sizeof(int));
    is.read((char *)&mob->myExtraLives, sizeof(int));
    is.read((char *)&mob->mySearchPower, sizeof(int));
    is.read((char *)&mob->myCowardice, sizeof(int));
    is.read((char *)&mob->myKills, sizeof(int));
    is.read((char *)&mob->myRangeTimeout, sizeof(int));
    is.read((char *)&mob->mySpellTimeout, sizeof(int));

    is.read((char *) &mob->myLastDir, sizeof(int));
    is.read((char *) &mob->myLastDirCount, sizeof(int));

    is.read((char *)&val, sizeof(int));
    mob->myHasTriedSuicide = val ? true : false;
    is.read((char *)&val, sizeof(int));
    mob->myHasWon = val ? true : false;
    is.read((char *)&val, sizeof(int));
    mob->mySkipNextTurn = val ? true : false;

    is.read((char *)&val, sizeof(int));
    mob->setSwallowed(val ? true : false);

    is.read((char *)&val, sizeof(int));
    mob->myAngryWithAvatar = (val ? true : false);

    is.read((char *)&val, sizeof(int));
    mob->mySeenAvatar = (val ? true : false);

    is.read((char *)&c, 1);
    mob->mySawMurder = (c ? true : false);
    is.read((char *)&c, 1);
    mob->mySawMeanMurder = (c ? true : false);
    is.read((char *)&c, 1);
    mob->mySawVictory = (c ? true : false);
    is.read((char *)&c, 1);
    mob->myAvatarHasRanged = (c ? true : false);
    
    YELL_NAMES		yell;
    FOREACH_YELL(yell)
    {
	is.read((char *)&c, 1);
	mob->myHeardYell[yell] = (c ? true : false);
	is.read((char *)&c, 1);
	mob->myHeardYellSameRoom[yell] = (c ? true : false);
    }

    SPELL_NAMES		spell;
    FOREACH_SPELL(spell)
    {
	is.read((char *)&c, 1);
	mob->myKnownSpells[spell] = c;
    }

    {
	is.read((char *)&num, sizeof(int));
	if (num)
	{
	    J_ASSERT(num == 1);
	    MOB *origbody = MOB::load(is);
	    mob->myOrigBody.reset(origbody);
	}
    }

    is.read((char *)&num, sizeof(int));

    for (i = 0; i < num; i++)
    {
	mob->myInventory.append(ITEM::load(is));
    }

    return mob;
}

ITEM *
MOB::getItemFromId(int itemid) const
{
    for (int i = 0; i < myInventory.entries(); i++) 
    { 
	if (myInventory(i)->getUID() == itemid) 
	    return myInventory(i);
    } 
    return 0; 
}

bool
MOB::hasVisibleEnemies() const
{
    PTRLIST<MOB *> list;

    getVisibleEnemies(list);
    if (list.entries())
	return true;
    return false;
}

ITEM *
MOB::getWieldedOrTopBackpackItem() const
{
    ITEM *item = lookupWeapon();
    if (item)
	return item;
    return getTopBackpackItem();
}

ITEM *
MOB::getTopBackpackItem(int depth) const
{
    for (int n = myInventory.entries(); n --> 0; )
    {
	ITEM *drop = myInventory(n);
	if (drop->isFlag())
	    continue;
	if (drop->isEquipped())
	    continue;
	if (drop->isWielded())
	    continue;

	if (!depth)
	    return drop;
	depth--;
    }
    return 0;
}

bool
MOB::hasSurplusItems() const
{
    bool	hasrange = false;
    bool	hasmelee = false;

    for (int i = 0; i < myInventory.entries(); i++)
    {
	// We can always drink these.
	if (myInventory(i)->isPotion())
	    continue;
	if (myInventory(i)->isFlag())
	    continue;
	// Gold isn't surplus!
	if (myInventory(i)->getDefinition() == ITEM_COIN)
	    continue;
	// Stuff we wear is important!
	if (myInventory(i)->isEquipped())
	    continue;
	if (myInventory(i)->isWielded())
	    continue;
	return true;
    }

    return false;
}

int
MOB::numSurplusRange() const
{
    int		numrange = 0;

    for (int i = 0; i < myInventory.entries(); i++)
    {
	if (myInventory(i)->isRanged())
	{
	    numrange += myInventory(i)->getStackCount();
	}
    }

    // Reserve one range for our own use.
    return numrange - 1;
}

void
MOB::getVisibleEnemies(PTRLIST<MOB *> &list) const
{
    // If we are blind, we can't see anyone.
    if (isBlind())
	return;

    if (!map()) return;

    MOBLIST	mobs;
    map()->getVisibleMobs(mobs);

    for (auto && mob : mobs)
    {
	if (!mob->alive())
	    continue;
	// Ignore none hostile
	if (isFriends(mob))
	    continue;

	list.append(mob);
    }
}

void
MOB::formatAndReport(const char *msg) const
{
    if (isAvatar() || pos().isFOV())
    {
	msg_format(msg, this);
    }
}

void
MOB::formatAndReport(const char *msg, MOB *object) const
{
    if (isAvatar() || pos().isFOV())
    {
	msg_format(msg, this, object);
    }
}

void
MOB::formatAndReport(const char *msg, const char *verb, MOB *object) const
{
    if (isAvatar() || pos().isFOV())
    {
	msg_format(msg, this, verb, object);
    }
}

void
MOB::formatAndReport(const char *msg, ITEM *object) const
{
    if (isAvatar() || pos().isFOV())
    {
	msg_format(msg, this, object);
    }
}

void
MOB::formatAndReport(const char *msg, const char *object) const
{
    if (isAvatar() || pos().isFOV())
    {
	msg_format(msg, this, object);
    }
}

int
MOB::numberMeleeHostiles() const
{
    int		dx, dy;
    MOB		*mob;
    int		hostiles = 0;

    FORALL_8DIR(dx, dy)
    {
	mob = pos().delta(dx, dy).mob();

	if (mob && !mob->isFriends(this))
	    hostiles++;
    }
    return hostiles;
}

void
MOB::clearCollision()
{
    myCollisionSources.clear();
    myCollisionTarget = 0;
}

void
MOB::setCollisionTarget(MOB *target)
{
    if (myCollisionTarget == target)
	return;

    J_ASSERT(!target || !myCollisionTarget);

    myCollisionTarget = target;
    if (target)
	target->collisionSources().append(this);
}

void
MOB::makeNoise(int loudness) const
{
    // Maybe account for stealth?
    pos().makeNoise(loudness);
}

bool
MOB::buildPortalAtLocation(POS vpos, int portal) const
{
    int		origangle;

    origangle = vpos.angle();

    TILE_NAMES	wall_tile = (TILE_NAMES) vpos.roomDefn().wall_tile;
    TILE_NAMES	tunnelwall_tile = (TILE_NAMES) vpos.roomDefn().tunnelwall_tile;
    TILE_NAMES	floor_tile = (TILE_NAMES) vpos.roomDefn().floor_tile;

    // We want all our portals in the same space.
    vpos.setAngle(0);

    // Check if it is a valid portal pos?
    if (!vpos.prepSquareForDestruction())
    {
	return false;
    }

    // Verify the portal is well formed, ie, three neighbours are now
    // walls and the other is a floor.
    int		dir, floordir = -1;
    TILE_NAMES	tile;

    for (dir = 0; dir < 4; dir++)
    {
	tile = vpos.delta4Direction(dir).tile();
	if (tile == floor_tile)
	{
	    if (floordir < 0)
		floordir = dir;
	    else
	    {
		// Uh oh.
		return false;

	    }
	}
	// Note invalid shouldn't occur here as we will have transformed
	// that into a wall in prep for destrction.
	else if (tile == wall_tile || tile == TILE_PROTOPORTAL ||
		 tile == tunnelwall_tile || tile == TILE_INVALID)
	{
	    // All good.
	}
	else
	{
	    // Uh oh.
	    return false;
	}
    }

    // Failed to find a proper portal.
    if (floordir < 0)
	return false;

    // In floordir+2 we will be placing the mirror of the opposite
    // portal.  It is important that square is not accessible.  We
    // merely make sure it isn't a floor
    // Still an issue of having ants dig it out.  Ideally we'd
    // have the virtual portal flagged with MAPFLAG_PORTAL but
    // we don't do that currently in buildPortal and doing so
    // would mean we'd have to clean it up properly.

    POS		virtualportal;

    virtualportal = vpos.delta4Direction((floordir+2)&3);
    virtualportal = virtualportal.delta4Direction((floordir+2)&3);

    // We now point to the square behind the proposed virtual portal
    // this should be wall or invalid
    tile = virtualportal.tile();
    if (tile != wall_tile && tile != tunnelwall_tile && tile != TILE_PROTOPORTAL && tile != TILE_INVALID)
	return false;
    // Try neighbours.
    tile = virtualportal.delta4Direction((floordir+1)&3).tile();
    if (tile != wall_tile && tile != tunnelwall_tile && tile != TILE_PROTOPORTAL && tile != TILE_INVALID)
	return false;
    tile = virtualportal.delta4Direction((floordir-1)&3).tile();
    if (tile != wall_tile && tile != tunnelwall_tile && tile != TILE_PROTOPORTAL && tile != TILE_INVALID)
	return false;

    // We now know we aren't an isolated island.  Yet.

    vpos.map()->buildUserPortal(vpos, portal, (floordir+2) & 3, origangle);

    return true;
}

