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

#include <libtcod.hpp>
#undef CLAMP

#include "mob.h"

#include "map.h"
#include "msg.h"
#include "text.h"
#include "item.h"
#include "config.h"

bool
MOB::aiAvoidDirection(int dx, int dy) const
{
    // Kobolds found it superstitious to step on corpses.
    // *Band mobs are not so choosy.
#if 0
    POS	qp = pos().delta(dx, dy);
    ITEMLIST	items;

    if (qp.allItems(items))
    {
	for (int i = 0; i < items.entries(); i++)
	{
	    if (items(i)->getDefinition() == ITEM_CORPSE)
		return true;
	}
    }
#endif
    return false;
}

bool
MOB::aiForcedAction()
{
    // Check for the phase...
    PHASE_NAMES		phase;

    phase = map()->getPhase();

    if (!alive())
	return false;

    myYellHystersis--;
    if (myYellHystersis < 0)
	myYellHystersis = 0;

    myBoredom++;
    if (myBoredom > 1000)
	myBoredom = 1000;

    // On normal & slow phases, items get heartbeat.
    if (phase == PHASE_NORMAL || phase == PHASE_SLOW)
    {
	myRangeTimeout--;
	if (myRangeTimeout < 0)
	    myRangeTimeout = 0;

	mySpellTimeout--;
	if (mySpellTimeout < 0)
	    mySpellTimeout = 0;

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

		    if (item->getDefinition() == ITEM_POLYMORPH)
		    {
			// Return to your original form.  I certainly
			// hope you have a body still!
			actionUnpolymorph(nullptr);
			return true;
		    }

		    removeItem(item);
		    delete item;
		}
	    }
	}
	// Only the avatar heals with food....
	if (isAvatar())
	{
	    // Apply any cold effects.
	    applyLocalTemperature();

	    // Eat food aggressively by shivering if cold...
	    // Note use of hystersis to avoid re-triggering
	    if (getCoreTemp() < glb_coretempleveldefs[CORETEMPLEVEL_SHIVERING].cutoff - 25)
	    {
		eatFood(4);
	    }
	    if (getCoreTemp() < glb_coretempleveldefs[CORETEMPLEVEL_FREEZING].cutoff)
	    {
		if (rand_chance(5))
		{
		    formatAndReport("%S <freeze>!", this);
		    // AGain, like digestion, this scales to your level
		    // (I don't kno wwhat that comment means?)
		    if (applyDamage(0, rand_range(1, 5), ELEMENT_ICE, ATTACKSTYLE_INTERNAL))
			return true;

		    if (rand_chance(10) && getDefinition() == MOB_AVATAR && myToeCount)
		    {
			formatAndReport("%S <lose> a toe!");
			myToeCount--;
		    }
		}
	    }
	    if (getCoreTemp() < glb_coretempleveldefs[CORETEMPLEVEL_FROZEN].cutoff)
	    {
		formatAndReport("%S <freeze> solid!", this);
		// AGain, like digestion, this scales to your level
		// (I don't kno wwhat that comment means?)
		if (applyDamage(0, rand_range(5, 25), ELEMENT_ICE, ATTACKSTYLE_INTERNAL))
		    return true;
	    }
	    if (getCoreTemp() > glb_coretempleveldefs[CORETEMPLEVEL_OVERHEATING].cutoff)
	    {
		if (rand_chance(10))
		{
		    formatAndReport("%S <overheat>!", this);
		    // AGain, like digestion, this scales to your level
		    // (I don't kno wwhat that comment means?)
		    if (applyDamage(0, rand_range(1, 5), ELEMENT_FIRE, ATTACKSTYLE_INTERNAL))
			return true;
		}
	    }

	    // Eat food.
	    // We have a real clock so don't need to eat food
	    // during normal travel as aggressively.
	    // But if we are injured we always eat food, otherwise
	    // it is one pellet per hp...
	    if (!rand_choice(5))
		eatFood(1);

	    if (getFood() && getHP() < getMaxHP())
	    {
		eatFood(1);
		if (hasItem(ITEM_PLAGUE))
		{
		}
		else
		{
		    if (!rand_choice(4))
		    {
			gainHP(1);
		    }
		}
	    }
	    if (getFood() && getMP() < getMaxMP())
	    {
		eatFood(1);
		if (hasItem(ITEM_PLAGUE))
		{
		}
		else
		{
		    if (!rand_choice(2))
			gainMP(1);
		}
	    }
	}

	// Regenerate if we are capable of it
	if (defn().isregenerate || hasItem(ITEM_REGEN))
	{
	    gainHP(1);
	}

	// Apply special rings.
	{
	    ITEM *ring = lookupRing();
	    if (ring && ring->asRing() == RING_REGENERATION)
	    {
		if (getFood() && getHP() < getMaxHP())
		{
		    if (isAvatar())
			ring->markMagicClassKnown();
		    eatFood(1);
		    gainHP(1);
		}
	    }
	    if (ring && ring->asRing() == RING_HUNGER)
	    {
		if (getFood())
		{
		    eatFood(1);
		    // There is a chance you figure this out...
		    if (isAvatar() && rand_chance(1))
			ring->markMagicClassKnown();
		}
	    }
	}

	// Check if we are at full stats
	// If warming up, you can rest...
	if (!isWaiting())
	{
	    // No need for expensive checks if we aren't waiting.
	}
	else if (getCoreTemp() < pos().ambientTemp() && getCoreTemp() < 0)
	{
	    // Warming up you can rest even if full health or
	    // starving.
	}
	else
	{
	    if (getHP() == getMaxHP() && getMP() == getMaxMP())
	    {
		stopWaiting("%S <be> at full health, so <stop> resting.");
	    }
	    else
	    {
		if (!getFood())
		    stopWaiting("%S <be> too hungry to rest!");
	    }
	}

	// Apply damage if inside.
	if (isSwallowed())
	{
	    formatAndReport("%S <be> digested!");

	    // Make it a force action if it kills us.
	    // This is a bit weird where damage depends on your level,
	    // not the attackers... Ah vell, perhaps I'lll retcon it
	    // that dissolving is a percentage attack?
	    if (applyDamage(0, rand_range(1, 10), ELEMENT_ACID, ATTACKSTYLE_INTERNAL))
		return true;
	}

	// Suffocate
	if (hasItem(ITEM_CHOKING) && defn().canbreathe)
	{
	    formatAndReport("%S <choke>!", this);
	    // AGain, like digestion, this scales to your level
	    if (applyDamage(0, rand_range(1, 5), ELEMENT_PHYSICAL, ATTACKSTYLE_INTERNAL))
		return true;
	}

	// Apply poison damage
	if (hasItem(ITEM_POISON))
	{
	    if (rand_chance(50))
	    {
		formatAndReport("%S <be> poisoned!", this);
		// AGain, like digestion, this scales to your level
		if (applyDamage(0, rand_range(1, 5), ELEMENT_POISON, ATTACKSTYLE_INTERNAL))
		    return true;
	    }
	}

	// Burn in fires.
	if (pos().isOnFire())
	{
	    formatAndReport("%S <burn> in the fire!", this);
	    if (applyDamage(0, rand_range(1, 5), ELEMENT_FIRE, ATTACKSTYLE_EXTERNAL))
		return true;

	}
    }


    // People waiting twiddle their thumbs!
    if (isWaiting())
    {
	// Effectively get a free skip next turn!
	mySkipNextTurn = false;
	return true;
    }
    else if (isMeditating())
    {
	// People meditating move very fast.
	// Nah, we want them the same speed!  Slow thinkers think slow!
    }
    switch (phase)
    {
	case PHASE_FAST:
	    // Not fast, no phase
	    if (defn().isfast || hasItem(ITEM_HASTED))
		break;
	    return true;

	case PHASE_QUICK:
	    // Not quick, no phase!
	    if (hasItem(ITEM_QUICKBOOST))
		break;
	    return true;

	case PHASE_SLOW:
	    if (defn().isslow)
		return true;
	    if (hasItem(ITEM_SLOW))
		return true;
	    break;

	case PHASE_NORMAL:
	    break;
        case NUM_PHASES:
            break;
    }
    if (mySkipNextTurn)
    {
	mySkipNextTurn = false;
	return true;
    }
    // Keep sleeping!
    if (hasItem(ITEM_ASLEEP))
	return true;
    return false;
}

bool
MOB::aiDoAI()
{
    AI_NAMES		ai = getAI();

    return aiDoAIType(ai);
}

bool
MOB::aiPickup(bool ignoretarget)
{
    if (aiWantsItem(pos().item(), ignoretarget))
    {
	return actionPickup();
    }
    return false;
}

bool
MOB::aiPrepareForBattle()
{
    if (isAvatar())
	return false;
	    
    // Peaceful don't prepare
    if (!hasItem(ITEM_ENRAGED) &&
        (getAI() == AI_PEACEFUL && !myAngryWithAvatar)
       )
	return false;

    MOB		*a = getAvatar();

    if (!a)
	return false;
    if (!a->alive())
	return false;

    // If we are in melee range, don't use our ranged attack!
    if (pos().dist(a->pos()) <= 1)
    {
	return false;
    }

    // Make sure we are aware...
    if (!pos().isFOV())
    {
	// Not in FOV, so do other battle prep like laying eggs.
	if (defn().eggtype != MOB_NONE)
	{
	    if (rand_prob(0.01f))
		actionLayEgg();
	}
	return false;
    }

    if (defn().useitems)
    {
	bool	canread = defn().canread && !isBlind();
	ITEM *item;
	if (canread &&
	    (item = lookupKnownItem(ITEM::lookupScroll(map(), SCROLL_SUMMON))))
	    return actionRead(item);
    }

    return false;
}

void
MOB::aiTrySpeaking()
{
    if (isAvatar())
	return;

    MOB		*avatar = getAvatar();
    if (!avatar || !avatar->alive())
	return;

    if (!pos().isFOV())
	return;

    if (!isFriends(avatar))
	return;

    // This is based on the turn time, however, which is not
    // very useful when we accelerate time - we get spam galore!
    // Instead we throw a 10 second real window on it.
    static int	lastspeak = -10;
    static int	lastspeakms = -10;

    if ((lastspeak < map()->getTime() - 15) &&
	(TCOD_sys_elapsed_milli() - lastspeakms > 10000) &&
	rand_chance(10))
    {
	const char		*speech = 0;
	bool			 emote = false;

	if (hasItem(ITEM_PLAGUE))
	{
	    speech = "*cough*";
	    emote = true;
	}

	if (speech)
	{
	    lastspeak = map()->getTime();
	    lastspeakms = TCOD_sys_elapsed_milli();
	    if (emote)
		doEmote(speech);
	    else
		doShout(speech);
	}
    }

}

void
MOB::resetAIState()
{
    myHome = POS();
    myTarget = POS();
    myAIState = 0;
    myBoredom = 0;
    myFleeCount = 0;
}

bool
MOB::aiDoAIType(AI_NAMES aitype)
{
    // Rebuild our home location.
    // Avatar uses this for its logic though.
    if (!isAvatar() && !myHome.valid())
    {
	myHome = pos();
    }

    // If we are in limbo do nothing.
    if (!pos().valid())
	return true;

    if (pos().isFOV())
	mySeenAvatar = true;

    if (myHeardYell[YELL_MURDERER] && !mySawMurder)
    {
	// Hearsay!
	mySawMurder = true;
	if (!isAvatar())
	    giftItem(ITEM_ENRAGED);
    }

    if (myFleeCount > 0)
    {
	myFleeCount--;
	if (!isAvatar() && aiFleeFromAvatar())
	    return true;
    }

    aiTrySpeaking();

    if (hasItem(ITEM_POISON))
    {
	if (aiCure())
	    return true;
    }

    if (healthStatus() <= HEALTHLEVEL_BLOODIED)
    {
	if (aiHeal())
	    return true;
    }

    if (aiPickup(/*ignoretarget*/ false))
	return true;

    if (aiPrepareForBattle())
	return true;

    if (hasItem(ITEM_ENRAGED))
	aitype = AI_CHARGE;

    if (aitype == AI_PEACEFUL && myAngryWithAvatar)
	aitype = AI_CHARGE;

    switch (aitype)
    {
	case AI_PEACEFUL:
	    return aiRandomWalk();

	case AI_NONE:
	    // Just stay put!
	    return true;

	case AI_STAYHOME:
	    // If we are at home stay put.
	    if (pos() == myHome)
		return true;

	    // Otherwise, go home.
	    // FALL THROUGH

	case AI_HOME:
	    // Move towards our home square.
	    int		dist;

	    dist = pos().dist(myHome);

	    // If we can shoot the avatar or charge him, do so.
	    if (!defn().isfriendly)
	    {
		if (aiRangeAttack())
		    return true;
		if (aiCharge(getAvatar(), AI_CHARGE))
		    return true;
	    }

	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;

	    // A good chance to just stay put.
	    if (rand_chance(70))
	    {
		return actionBump(0, 0);
	    }

	    if (rand_choice(dist))
	    {
		// Try to home.
		if (aiPathFindTo(myHome))
		    return true;
	    }
	    // Default to wandering
	    return aiRandomWalk();

	case AI_ORTHO:
	    if (aiRangeAttack())
		return true;
	    if (aiCharge(getAvatar(), AI_CHARGE, true))
		return true;
	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;
	    // Default to wandering
	    return aiRandomWalk(true);

	case AI_CHARGE:
	    if (aiRangeAttack())
		return true;
	    if (aiCharge(getAvatar(), AI_CHARGE))
		return true;
	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;
	    // Default to wandering
	    return aiRandomWalk();

	case AI_PATHTO:
	    if (aiRangeAttack())
		return true;

	    // Only start our merciless hunt on seeing them once.
	    if (mySeenAvatar && aiKillPathTo(getAvatar()))
		return true;

	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;
		    
	    // Default to wandering
	    return aiRandomWalk();

	case AI_FLANK:
	    if (aiRangeAttack())
		return true;
	    if (aiCharge(getAvatar(), AI_FLANK))
		return true;
	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;
	    // Default to wandering
	    return aiRandomWalk();

	case AI_RANGECOWARD:
	    // If we can see avatar, shoot at them!
	    // We keep ourselves at range if possible, unless
	    // retreat is blocked, in which case we will melee.
	    if (pos().isFOV())
	    {
		int		 dist, dx, dy;

		if (aiAcquireAvatar())
		{
		    pos().dirTo(myTarget, dx, dy);
		    dist = pos().dist(myTarget);

		    // Not sure if they should chase, but this will cause
		    // them I think to back off on next level...
		    if (dist == 0)
			return aiActionWhenAtTarget();
		    if (dist == 1)
		    {
			// We are in melee range.  Try to flee.
			if (aiFleeFrom(myTarget))
			    return true;

			// Failed to flee.  Kill!
			return actionMelee(dx, dy);
		    }

		    // Try ranged attack.
		    if (aiRangeAttack())
			return true;

		    // Failed range attack.  If outside of range, charge.
		    if (dist > getRangedRange())
		    {
			if (aiCharge(getAvatar(), AI_FLANK))
			    return true;
		    }
		    // Out of combat pickup
		    if (aiPickup(/*ignoretarget*/ true))
			return true;

		    // Otherwise, wander within the range hoping to line up.
		    // Trying to double think the human is likely pointless
		    // as they'll be trying to avoid lining up to.
		    return aiRandomWalk();
		}
	    }
		
	    if (aiCharge(getAvatar(), AI_FLANK))
		return true;
	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;

	    // Default to wandering
	    return aiRandomWalk();

	    
	case AI_COWARD:
	    if (aiRangeAttack())
		return true;

	    if (aiFleeFromAvatar())
		return true;
	    // Failed to flee, cornered, so charge!
	    if (aiCharge(getAvatar(), AI_CHARGE))
		return true;
	    // Out of combat pickup
	    if (aiPickup(/*ignoretarget*/ true))
		return true;

	    return aiRandomWalk();

	case AI_MOUSE:
	    return aiDoMouse();

	case AI_ORC:
	    return aiDoOrc();

	case AI_RAT:
	    return aiDoRat();

	case AI_STRAIGHTLINE:
	    return aiStraightLine();

	case AI_ADVENTURER:
	    return aiDoAdventurer();

	case AI_RANDOMBUMP:
	    return aiRandomBump();

        case NUM_AIS:
            break;
    }

    return false;
}

bool
MOB::aiBattlePrep()
{
    for (int i = 0; i < myInventory.entries(); i++)
    {
	ITEM		*item = myInventory(i);

	if (item->isPotion())
	{
	    return actionQuaff(item);
	}
    }
    return false;
}

bool
MOB::aiDestroySomethingGood(MOB *denyee)
{
    ITEM		*wand = lookupRanged();
    ITEM		*enemywand = denyee->lookupRanged();

    if (wand)
    {
	// Only bother wasting our turn destroying it if the
	// avatar hasn't yet acquired one.
	if (aiLikeMoreWand(wand, enemywand) == wand)
	{
	    return actionBreak(wand);
	}
    }
    // Deal with any left over potions
    return aiBattlePrep();
}

bool
MOB::aiTwitch(MOB *avatarvis)
{
    return false;
}


bool
MOB::aiTactics(MOB *avatarvis)
{
    return false;
}

bool
MOB::aiWantsAnyMoreItems() const
{
    // Is there anything we want for christmas?
    if (!lookupWeapon())
	return true;

    if (!lookupRanged())
	return true;

    return false;
}

bool
MOB::aiWantsItem(ITEM *item, bool ignoretarget) const
{
    if (!item)
	return false;

    // Cannot pick up large things!
    if (item->size() > size())
	return false;

    // Can't pick up if swallowed
    if (isSwallowed())
	return false;

    // Intentionally discarded are to be left.
    if (isAvatar() && item->wasDiscarded())
	return false;

    // If we can't fit in our backpack, we don't want.
    // Note we can stack so this is item dependent.
    if (!roomInBackpackForItem(item))
	return false;

    // Active target signifies we are in combat, so other than
    // yummy gold, we don't want to be distracted picking up
    // the arrows being shot at us!
    bool		activetarget = myTarget.valid();
    if (ignoretarget) activetarget = false;
    bool		useitems = defn().useitems;

    // THe first one is used if equal, so we put item second
    // so we don't pick up identicals.
    if (!activetarget && useitems && item->isWeapon() && aiLikeMoreWeapon(lookupWeapon(), item) == item)
	return true;
    if (!activetarget && useitems && item->isRanged() && aiLikeMoreWand(lookupRanged(), item) == item)
	return true;
    if (!activetarget && useitems && item->isArmour() && aiLikeMoreArmour(lookupArmour(ARMOURSLOT_BODY), item) == item)
	return true;
    if (!activetarget && item->defn().itemclass == ITEMCLASS_AMMO 
	&& 
	( (lookupRanged() && lookupRanged()->defn().ammo == item->getDefinition())
	  || item->getDefinition() == defn().range_ammo ) 
	&& !item->isBroken()
       )
	return true;
    if (!activetarget && useitems && 
	(item->isPotion() || item->isScroll() || item->isRing())
       )
	return true;
    if (!activetarget && useitems && 
	(item->isWand() && !item->isBroken())
       )
	return true;
    // covetous will be distracted by gold!
    if (defn().covetous && item->isTreasure())
	return true;
    if (!activetarget && isAvatar() &&
	(item->isFood() || item->isSpellbook()))
	return true;
    if (!activetarget && isAvatar() &&
	(item->isTool() && !item->isBroken()))
	return true;

    // All else is fodder.
    // "Everything is fodder" was Megatron's favorite saying according
    // to the summary comic books I have.
    return false;
}

bool
MOB::aiDoRat()
{
    // Rats only attack if they have a crowd.
    // They will fight if cornered, though!

    if (aiRangeAttack())
	return true;

    // If we are in FOV but don't see enough rats, run!
    if (pos().isFOV() && (map()->getNumVisibleMobsOfType(getDefinition()) <= 1))
    {
	if (aiFleeFromAvatar())
	    return true;
    }

    // Failed to flee, cornered
    // Or brave.
    // Attack!
    if (aiCharge(getAvatar(), AI_CHARGE))
	return true;

    // Out of combat pickup
    if (aiPickup(/*ignoretarget*/ true))
	return true;

    return aiRandomWalk();
}

bool
MOB::aiRandomBump()
{
    int		dx, dy;
    int		baseangle = rand_choice(9);
    int		angles[9] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };

    for (int test = 0; test < 9; test++)
    {
	int choice = rand_choice(9-test);
	std::swap(angles[choice], angles[9-test-1]);
	int angle = angles[9-test-1];
	if (angle == 8)
	    return actionBump(0, 0);
	rand_angletodir(angle, dx, dy);
	if (canMoveDir(dx, dy, /*mob*/false, /*onlypassive*/false))
	    return actionBump(dx, dy);
    }
    return false;
}

ITEM *
MOB::aiFindCloseMappedItem() const
{
    if (!map()) return nullptr;

    ITEMLIST		items;
    int			n = 1;
    int			bestdist = 100000;
    ITEM		*bestitem = nullptr;

    map()->getVisibleItems(items);

    for (auto && item : items)
    {
	if (item->pos().item() != item)
	{
	    // Ignore not top of stack
	    continue;
	}
	// Only go if we want items.
	if (aiWantsItem(item, true))
	{
	    int dist = pos().dist(item->pos());
	    if (dist < bestdist)
	    {
		bestdist = dist;
		bestitem = item;
		n = 2;
	    }
	    else if (dist == bestdist)
	    {
		if (!rand_choice(n))
		    bestitem = item;
		n++;
	    }
	}
    }

    return bestitem;
}

bool
MOB::aiAvatarExplore()
{
    // Check if there is a juicy item waiting to be pickedup.
    // Because we only pick up top of stacks, this only will query
    // the top layer.
    ITEM		*item;

    item = aiFindCloseMappedItem();
    if (item)
    {
	if (item->pos() == pos())
	{
	    return actionPickup();
	}
	myAIState = AI_STATE_PACKRAT;
	myHome = item->pos();
	return aiAvatarPackrat();
    }

    // Check if home & neighbours are mapped, in which case
    // we've done our job.
    if (myHome.valid() && myHome.isMapped())
    {
	if (myHome.left().isMapped() && myHome.right().isMapped() &&
	    myHome.up().isMapped() && myHome.down().isMapped())
	{
	    myHome = POS();
	}
    }

    // If my home position is negative, it isn't yet discovered.
    // Attempt to path to the current home.
    if (myHome.valid() && aiPathFindTo(myHome))
	return true;

    // Find a new location to search for...
    myHome = map()->findCloseUnexploredOnFloor(pos());
    if (myHome.valid())
    {
	return aiPathFindTo(myHome);
    }
	
    // Done exploring!  Let's dive.
    myAIState = AI_STATE_NEXTLEVEL;
    return aiAvatarNextLevel();
}

bool
MOB::aiAvatarPackrat()
{
    if (aiPathFindTo(myHome))
	return true;

    // Note that auto-pickup occurs before this, so we don't have
    // to pick up...

    // Failed to go to item or already done so, pop back to explore mode.
    myAIState = AI_STATE_EXPLORE;
    return aiAvatarExplore();
}

bool
MOB::aiAvatarNextLevel()
{
    // Locate stairs.
    TILE_NAMES	tile = TILE_DOWNSTAIRS;
    if (hasItem(ITEM_MACGUFFIN))
	tile = TILE_UPSTAIRS;
    myHome = map()->getRandomTileOnFloor(pos(), tile);

    // Did it fail?  Just bump...
    if (!myHome.valid())
	return aiRandomBump();

    if (pos() == myHome)
    {
	myAIState = AI_STATE_EXPLORE;
	myHome = POS();
	return actionClimb();
    }
    
    // Explore there..
    return aiPathFindTo(myHome);
}

MOB *
MOB::aiFindEnemy() const
{
    PTRLIST<MOB *>	list;
    int			i;
    MOB			*biggestfoe, *foe;
    
    // The problem with this is if we have two stationary foes
    // a and b, where we hate b more, if b is out of sight and
    // we step to a and we then step to b, but in doing so hide
    // b, so we return our attention again to a.  An infinite loop
    // then develops.
    getVisibleEnemies(list);
    biggestfoe = 0;
    for (i = 0; i < list.entries(); i++)
    {
	foe = list(i);
	biggestfoe = aiHateMore(biggestfoe, foe);
    }
    return biggestfoe;
}

MOB *
MOB::aiHateMore(MOB *a, MOB *b) const
{
    // Check which is closer.
    int		adist, bdist;
    bool	alinedup, blinedup;

    // Trivial cases.
    if (!a)
	return b;
    if (!b)
	return a;

    adist = pos().dist(a->pos());
    bdist = pos().dist(b->pos());

    // If a is melee, take it.
    if (adist <= 1)
    {
	if (bdist <= 1)
	{
	    // Both are close.  We need to take out the one projected
	    // to do the most damage over the length of time it is
	    // expected we could kill it.
	    double		admg, bdmg;
	    double		acost, bcost;

	    // Note our own damage rate affects how many turns it takes
	    // to kill the foe, so doesn't matter.  (Except this is a
	    // discrete case so windshield is windshield?)
	    admg = a->getMeleeCanonicalDamage();
	    bdmg = b->getMeleeCanonicalDamage();
	    
	    // We compare the opportunity costs of killing either
	    // a or b first.
	    // It will take a->getHP() / ourdmg turns to kill a.
	    // In that time, b will do (bdmg * a->getHP() / ourdmg)
	    // damage to us.  Since we want to minimize damage, we pick
	    // the enemy for whom this value is highest.
	    // (Thanks to Thijs van Ommen who corrected my earlier
	    // calculation)
	    // TODO: The big exception is if the enemy can regenerate.
	    // Normal regeneration should be modelled by lowering our damage
	    // output, vampirism modelled by subtracting admg from ourdmg.
	    acost = (admg * b->getHP());
	    bcost = (bdmg * a->getHP());

	    if (acost > bcost)
		return a;
	    return b;
	}
	return a;
    }
    // Ditto for b.
    if (bdist <= 1)
	return b;

    // We are at range.

    alinedup = canTargetAtRange(a->pos());
    blinedup = canTargetAtRange(b->pos());

    // Take the one that is lined up for attack
    if (alinedup)
    {
	if (blinedup)
	{
	    // Take closer..
	    if (adist < bdist)
		return a;
	    return b;
	}
	// Pick the lined up one.
	return a;
    }
    if (blinedup)
	return b;

    // Take closer
    if (adist < bdist)
    {
	return a;
    }
    // Tie, break.
    return rand_choice(2) ? a : b;
}

bool
MOB::aiAvatarHunt()
{
    MOB		*foe;
    // Find most evil enemy
    foe = aiFindEnemy();
    if (foe)
    {
	myTarget = foe->pos();

	// Try a ranged attack.
	if (aiRangeAttack(foe))
	    return true;

	// Do a path find, this will try to walk into which for avatars
	// does a melee attack.

	// If, however, we are swallowed, our worst enemy is
	// ourself.
	if (isSwallowed())
	{
	    return actionMelee(0, 0);
	}

	if (aiPathFindTo(myTarget))
	{
	    return true;
	}
    }

    // No enemy, go to last known loc.
    if (aiPathFindTo(myTarget))
    {
	return true;
    }

    // No enemy and we went to last known location of enemy, so 
    // drop down to exploration
    myAIState = AI_STATE_EXPLORE;
    myHome = POS();

    return aiAvatarExplore();
}

bool
MOB::aiDoAdventurer()
{
    // Picking up and healing is handled by the generic code
    // We don't do equipping on the theory the player wants
    // control of that.
    // Likewise we don't need a direct next level behaviour.
    if (hasVisibleEnemies())
    {
	myAIState = AI_STATE_HUNT;
    }

    // Determine our current state...
    switch (myAIState)
    {
	case AI_STATE_EXPLORE:
	    return aiAvatarExplore();
	case AI_STATE_NEXTLEVEL:
	    return aiAvatarNextLevel();
	case AI_STATE_HUNT:
	    return aiAvatarHunt();
	case AI_STATE_PACKRAT:
	    return aiAvatarPackrat();
    }
    return aiRandomBump();
}

bool
MOB::aiDoOrc()
{
    // Find all people and attack closest!
    MOBLIST		allmobs;
    MOB			*mob = 0;
    int			dist = -1;
    
    map()->getAllMobs(allmobs);
    for (int i = 0; i < allmobs.entries(); i++)
    {
	if (!allmobs(i)->pos().valid())
	    continue;

	if (allmobs(i)->defn().isfriendly)
	{
	    int	map = pos().map()->buildDistMap(allmobs(i)->pos());
	    if (map < 0)
		continue;

	    if (!mob || dist > pos().getDistance(map))
	    {
		mob = allmobs(i);
		dist = pos().getDistance(map);
	    }
	}
    }

    if (!mob)
    {
	// Out of combat pickup
	if (aiPickup(/*ignoretarget*/ true))
	    return true;
	// No one to chase!
	return aiRandomWalk();
    }

    return aiKillPathTo(mob);
}

double
avatar_weight_weapon(ITEM *item)
{
    double	weight;

    // All weapons are theoritically equal :>
    weight = item->getDepth();
    // A bit annoying here as we don't know the bonus, but....
    weight += item->getBonus();
    return weight;
}

double
avatar_weight_wand(ITEM *item)
{
    int		ia, ir;
    double	weight;
	
    item->getRangeStats(ir, ia);

    // Estimate average damage for our weight.
    // Really should do DPF here, but we don't use this and melee isn't
    // upgraded yet.
    weight = item->getDepth();
    // Area technically improves with the square, but in practice
    // you don't have everyone bunched, so make it just
    // the linear
    weight *= ia;

    // Each two points of range gives us another attack on a charging
    // foe.
    weight *= ir * 0.5;

    return weight;
}

ITEM *
MOB::aiLikeMoreWeapon(ITEM *a, ITEM *b) const
{
    if (b && b->isBroken()) return a;

    // Trivial case
    if (!a) return b;
    if (!b) return a;

    if (a->isBroken() && !b->isBroken()) return b;
    
    if (avatar_weight_weapon(b) > avatar_weight_weapon(a))
	return b;
    // Default to a.
    return a;
}

ITEM *
MOB::aiLikeMoreArmour(ITEM *a, ITEM *b) const
{
    if (b && b->isBroken()) return a;

    // Trivial case
    if (!a) return b;
    if (!b) return a;

    if (a->isBroken() && !b->isBroken()) return b;
    
    if (b->getDamageReduction() > a->getDamageReduction())
	return b;
    if (b->getTotalArmourClass() > a->getTotalArmourClass())
	return b;
    // Default to a.
    return a;
}

ITEM *
MOB::aiLikeMoreWand(ITEM *a, ITEM *b) const
{
    if (b && b->isBroken()) return a;

    // Trivial case
    if (!a) return b;
    if (!b) return a;
    
    if (a->isBroken() && !b->isBroken()) return b;
    
    if (avatar_weight_wand(b) > avatar_weight_wand(a))
	return b;
    // Default to a.
    return a;
}

bool
MOB::aiDoMouse()
{
    int		lastdir, dir, newdir;
    int		dx, dy;
    bool	found = false;
    int		i;

    // Least three bits track our last direction.
    lastdir = myAIState & 7;

    // We want to try and track a wall.  We want this wall to
    // be on our right hand side.

    // Take a look at right hand square.
    rand_angletodir((lastdir + 2) & 7, dx, dy);
    
    if (!canMoveDir(dx, dy, true))
    {
	// Got a wall to the right.  Try first available direction.
	// Because we want to hug using diagonals, we try the forward
	// and right first.
	dir = (lastdir+1) & 7;
    }
    else
    {
	// No wall on right.  Try and go straight forward by default.
	dir = lastdir;
    }

    // If everyway is blocked, bump straight forward.
    newdir = lastdir;
    for (i = 0; i < 8; i++)
    {
	rand_angletodir(dir, dx, dy);
	if (canMoveDir(dx, dy, true))
	{
	    newdir = dir;
	    break;
	}
	// Keeping wall on right means turning left first!
	dir--;
	dir &= 7;
    }

    if (newdir == -1)
	newdir = lastdir;
    else
	found = true;

    // Store our last direction.
    myAIState &= ~7;
    myAIState |= newdir;

    if (found)
    {
	return actionBump(dx, dy);
    }

    // Mouse is cornered!  Return to normal AI.
    return false;
}

bool
MOB::aiStraightLine()
{
    int		lastdir;
    int		dx, dy;
    MOB		*target;
    POS		t;

    // Least three bits track our last direction.
    lastdir = myAIState & 7;

    // Bump & go.

    // Take a look at right hand square.
    rand_angletodir(lastdir, dx, dy);

    t = pos().delta(dx, dy);
    
    // If target is avatar, run into him!
    target = t.mob();
    if (target && !isFriends(target))
    {
	return actionBump(dx, dy);
    }
    
    if (!canMove(t, true))
    {
	// Can't go there!  Pick new direction.
	// Do not want same direction again.
	lastdir += rand_choice(7) + 1;
	lastdir &= 7;
    }
    myAIState = lastdir;
    rand_angletodir(lastdir, dx, dy);
    t = pos().delta(dx, dy);
    
    // If target is avatar, run into him!
    target = t.mob();
    if (target && !isFriends(target))
    {
	return actionBump(dx, dy);
    }

    // Bump failed.
    if (!canMove(t, true))
	return false;

    // Move forward!
    return actionBump(dx, dy);
}

bool
MOB::aiFleeFromAvatar()
{
    if (aiAcquireAvatar())
    {
	// Ensure we are at our flee distance, once we are there we 
	// can relax.
	// If we've been hit, we don't have a flee distance any more
	// as we aren't sticking around to find out if they have another
	// arrow.
	int dist = pos().dist(myTarget);
	if (myAngryWithAvatar || dist < defn().fleerange)
	{
	    if (aiFleeFrom(myTarget))
		return true;
	}
	else
	    return aiRandomWalk();
    }

    return false;
}

bool
MOB::aiTeleport()
{
    // If we can intrisnically teleport, we'd do it here.
    if (defn().useitems)
    {
	bool	canread = defn().canread && !isBlind();

	ITEM *item;
	if (canread &&
	    ((item = lookupKnownItem(ITEM::lookupScroll(map(), SCROLL_TELEPORT)))))
	{
	    return actionRead(item);
	}
	if ((item = lookupKnownUnbrokenItem(ITEM::lookupWand(map(), WAND_TELEPORT))))
	{
	    return actionApplyTool(item, 0, 0, nullptr);
	}
    }
    return false;
}

bool
MOB::aiHeal()
{
    // If we can intrisnically heal, we'd do it here.

    if (defn().useitems)
    {
	ITEM *item;
	if ((item = lookupKnownItem(ITEM::lookupPotion(map(), POTION_GREATERHEAL))))
	{
	    return actionQuaff(item);
	}
	if ((item = lookupKnownItem(ITEM::lookupPotion(map(), POTION_HEAL))))
	{
	    return actionQuaff(item);
	}

	// Desperation
	if (!isAvatar() && healthStatus() <= HEALTHLEVEL_CRITICALLYINJURED)
	{
	    if ((item = lookupKnownUnbrokenItem(ITEM::lookupWand(map(), WAND_POLY))))
	    {
		return actionApplyTool(item, 0, 0, nullptr);
	    }
	}
    }
    return false;
}

bool
MOB::aiCure()
{
    // If we can intrisnically cure, we'd do it here.

    if (defn().useitems)
    {
	ITEM *item;
	if ((item = lookupKnownItem(ITEM::lookupPotion(map(), POTION_CURE))))
	{
	    return actionQuaff(item);
	}
	if ((item = lookupKnownUnbrokenItem(ITEM::lookupWand(map(), WAND_RESTORATION))))
	{
	    return actionApplyTool(item, 0, 0, nullptr);
	}
    }
    return false;
}

bool
MOB::aiFleeFrom(POS goal, bool sameroom)
{
    int		dx, dy;
    int		angle, resultangle = 0, i;
    int		choice = 0;

    pos().dirTo(goal, dx, dy);
    dx = -dx;
    dy = -dy;
    
    angle = rand_dirtoangle(dx, dy);

    // 3 ones in same direction get equal chance.
    for (i = angle-1; i <= angle+1; i++)
    {
	rand_angletodir(i, dx, dy);
	if (sameroom)
	{
	    POS	goal;
	    goal = pos().delta(dx, dy);
	    if (goal.roomId() != pos().roomId())
		continue;
	}
	if (canMoveDir(dx, dy))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = i;
	}
    }

    if (!choice)
    {
	if (aiTeleport())
	    return true;
	// Try a bit more desperately...
	for (i = angle-2; i <= angle+2; i += 4)
	{
	    rand_angletodir(i, dx, dy);
	    if (sameroom)
	    {
		POS	goal;
		goal = pos().delta(dx, dy);
		if (goal.roomId() != pos().roomId())
		    continue;
	    }
	    if (canMoveDir(dx, dy))
	    {
		choice++;
		if (!rand_choice(choice))
		    resultangle = i;
	    }
	}
    }

    if (choice)
    {
	// Move in the direction
	rand_angletodir(resultangle, dx, dy);
	return actionBump(dx, dy);
    }

    // Failed
    return false;
}

bool
MOB::aiFleeFromSafe(POS goal, bool avoidrange)
{
    if (aiFleeFromSafe(goal, avoidrange, true))
	return true;
    return aiFleeFromSafe(goal, avoidrange, false);
}

bool
MOB::aiFleeFromSafe(POS goal, bool avoidrange, bool avoidmob)
{
    int		dx, dy;
    int		angle, resultangle = 0, i;
    int		choice = 0;

    pos().dirTo(goal, dx, dy);
    dx = -dx;
    dy = -dy;
    
    angle = rand_dirtoangle(dx, dy);

    // 3 ones in same direction get equal chance.
    for (i = angle-1; i <= angle+1; i++)
    {
	rand_angletodir(i, dx, dy);

	// Ignore if it gets as beside!
	POS	g;
	g = pos().delta(dx, dy);
	if (g.dist(goal) <= 1)
	    continue;
	if (avoidrange && goal.mob())
	{
	    if (goal.mob()->canTargetAtRange(g))
		continue;
	}
	if (canMoveDir(dx, dy, avoidmob))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = i;
	}
    }

    if (!choice)
    {
	// Try a bit more desperately...
	for (i = angle-2; i <= angle+2; i += 4)
	{
	    rand_angletodir(i, dx, dy);
	    // Ignore if it gets as beside!
	    POS	g;
	    g = pos().delta(dx, dy);
	    if (g.dist(goal) <= 1)
		continue;
	    if (avoidrange && goal.mob())
	    {
		if (goal.mob()->canTargetAtRange(g))
		    continue;
	    }
	    if (canMoveDir(dx, dy, avoidmob))
	    {
		choice++;
		if (!rand_choice(choice))
		    resultangle = i;
	    }
	}
    }

    if (choice)
    {
	// Move in the direction
	rand_angletodir(resultangle, dx, dy);
	return actionBump(dx, dy);
    }

    // Failed
    return false;
}

bool
MOB::aiAcquireAvatar()
{
    MOB		*a;
    a = getAvatar();

    if (a && !a->alive())
	return false;

    return aiAcquireTarget(a);
}

bool
MOB::aiAcquireTarget(MOB *a)
{
    if (a && a->isSwallowed())
    {
	myTarget = POS();
	return false;
    }
    // If we can see avatar, charge at them!  Otherwise, move
    // to our target if set.  Otherwise random walk.
    if (a && 
	a->pos().isFOV() &&
	pos().isFOV())
    {
	int		dist;
	// Determine distance
	dist = pos().dist(a->pos());
	// Handle nearsighted:
	if (dist <= defn().sightrange)
	{
	    // Avatar has to be lit, or nearby.
	    // We have an extra grace vs player or else stepping into
	    // shadows instantly loses people.
	    if (a->pos().isLit() || dist <= 2)
	    {
		myTarget = a->pos();
		return myTarget.valid();
	    }
	}
    }

    // Random chance of forgetting avatar location.
    // Also, if current location is avatar location, we forget it.
    // We leave it to the AI to decide what to do when they reach
    // the last known spot.
    // if (pos() == myTarget)
    //    myTarget = POS();
    if (!rand_choice(10))
	myTarget = POS();

    return myTarget.valid();
}

bool
MOB::aiActionWhenAtTarget()
{
    int dist = pos().dist(myTarget);
    J_ASSERT(dist == 0);

    if (pos().tile() == TILE_DOWNSTAIRS ||
	pos().tile() == TILE_UPSTAIRS)
    {
	return actionClimb();
    }
    myTarget = POS();
    return false;
}

bool
MOB::aiRandomWalk(bool orthoonly, bool sameroom)
{
    int		dx, dy;
    int		angle, resultangle = 0;
    int		choice = 0;

    for (angle = 0; angle < 8; angle++)
    {
	if (orthoonly && (angle & 1))
	    continue;

	rand_angletodir(angle, dx, dy);
	POS	goal;
	goal = pos().delta(dx, dy);
	if (sameroom)
	{
	    if (goal.roomId() != pos().roomId())
		continue;
	    if (goal.defn().isdoorway)
	    {
		// Obviously going through a door is changing the room!
		continue;
	    }
	}

	// Don't wander onto ice as it may melt!  (together with
	// charging thus avoids orcs getting stranded in the lakes, I hope!)
	if (goal.defn().forbidrandomwander)
	    continue;

	if (aiAvoidDirection(dx, dy))
	    continue;
	if (canMoveDir(dx, dy))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = angle;
	}
    }

    if (choice)
    {
	// Move in the direction
	rand_angletodir(resultangle, dx, dy);
	return actionWalk(dx, dy);
    }

    // Failed
    return false;
}

bool
MOB::aiCharge(MOB *foe, AI_NAMES aitype, bool orthoonly)
{
    if (aiAcquireTarget(foe))
    {
	if (aitype == AI_FLANK)
	{
	    if (aiFlankTo(myTarget))
		return true;
	}
	else
	{
	    if (aiMoveTo(myTarget, orthoonly))
		return true;
	}
    }

    return false;
}

bool
MOB::aiRangeAttack(MOB *target)
{
    // Wait for recharge.  But only if we don't have a wand equipped!
    bool		 haswand = false;
    ITEM		*wand = nullptr;
    int			 wandrange;
    if (defn().useitems)
    {
	if ((wand = lookupKnownUnbrokenItem(ITEM::lookupWand(map(), WAND_FIRE))))
	{
	    haswand = true;
	    wandrange = GAMEDEF::wanddef(WAND_FIRE)->range;
	}
    }

    if (myRangeTimeout && !lookupRanged() && !haswand)
	return false;

    // See if we have a ranged attack at all!
    if (!defn().range_valid && !lookupRanged() && !haswand)
	return false;

    // See if we are out of ammo.
    ITEM_NAMES		ammo = ITEM_NONE;
    if (lookupRanged())
	ammo = lookupRanged()->defn().ammo;
    else
	ammo = defn().range_ammo;
    if (!haswand && ammo != ITEM_NONE && !hasUnbrokenItem(ammo))
        return false;

    int		 dx, dy;
    MOB		*a = getAvatar(), *v;

    // Override the target from the avatar...
    if (target)
	a = target;

    if (!a)
	return false;

    if (!a->alive())
	return false;

    pos().dirTo(a->pos(), dx, dy);

    // If we are in melee range, don't use our ranged attack!
    if (pos().dist(a->pos()) <= 1)
    {
	return false;
    }

    // Try ranged attack.
    int			range = getRangedRange();
    if (haswand)
	range = wandrange;
    v = pos().traceBullet(range, dx, dy);
    if (v == a)
    {
	// Potential range attack.
	if (haswand)
	    return actionApplyTool(wand, dx, dy, nullptr);
	return actionFire(dx, dy);
    }

    return false;
}

bool
MOB::canTargetAtRange(POS goal) const
{
    int		dx, dy;

    pos().dirTo(goal, dx, dy);

    // If we are in melee range, don't use our ranged attack!
    if (pos().dist(goal) <= 1)
    {
	return false;
    }

    // Try ranged attack.
    int		range = getRangedRange();

    POS		next = pos();

    while (range > 0)
    {
	range--;
	next = next.delta(dx, dy);
	if (next == goal)
	    return true;
	if (next.mob())
	{
	    return false;
	}

	// Stop at a wall.
	if (!next.defn().ispassable)
	    return false;
    }
    return false;
}

bool
MOB::aiMoveTo(POS t, bool orthoonly)
{
    int		dx, dy, dist;
    int		angle, resultangle = 0, i;
    int		choice = 0;

    pos().dirTo(t, dx, dy);

    if (orthoonly && dx && dy)
    {
	// Ensure source is orthogonal.
	if (rand_choice(2))
	    dx = 0;
	else
	    dy = 0;
    }

    dist = pos().dist(t);
    if (dist == 0)
	return aiActionWhenAtTarget();
    if (dist == 1)
    {
	// Attack!
	if (canMoveDir(dx, dy, false))
	    return actionBump(dx, dy);
	return false;
    }

    // Move in general direction, preferring a straight line.
    angle = rand_dirtoangle(dx, dy);

    for (i = 0; i <= 2; i++)
    {
	if (orthoonly && (i & 1))
	    continue;

	rand_angletodir(angle-i, dx, dy);
	if (canMoveDir(dx, dy))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = angle-i;
	}
	
	rand_angletodir(angle+i, dx, dy);
	if (canMoveDir(dx, dy))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = angle+i;
	}

	if (choice)
	{
	    rand_angletodir(resultangle, dx, dy);

	    // If wew can leap, maybe leap?
	    if (defn().canleap)
	    {
		if (dist > 2 &&
		    canMoveDir(2*dx, 2*dy))
		{
		    formatAndReport("%S <leap>.");
		    dx *= 2;
		    dy *= 2;
		}
	    }
	    
	    return actionWalk(dx, dy);
	}
    }

    return false;
}

bool
MOB::aiKillPathTo(MOB *target)
{
    if (!target)
	return false;

    myTarget = target->pos();
    
    return aiPathFindTo(target->pos());
}

bool
MOB::aiPathFindTo(POS goal)
{
    if (aiPathFindTo(goal, true))
	return true;
    return aiPathFindTo(goal, false);
}

bool
MOB::aiPathFindTo(POS goal, bool avoidmob)
{
    int		dx, dy, ndx, ndy;
    int		curdist, dist;
    int		distmap;

    bool	found;

    // see if already there.
    if (pos() == goal)
	return false;

    // If target is invalid, fail.
    if (!goal.valid())
	return false;

    // Really, really, inefficient.
    distmap = pos().map()->buildDistMap(goal);

    curdist = pos().getDistance(distmap);
    
    // We don't care if this square is unreachable, as if it is the
    // square with the mob, almost by definition it is unreachable
    
    found = false;
    int			nmatch = 0;
    FORALL_8DIR(dx, dy)
    {
	dist = pos().delta(dx, dy).getDistance(distmap);

	// If dist is 0, we do not ignore the mob as we want
	// to reach it!
	if (avoidmob && dist)
	    if (pos().delta(dx, dy).mob())
		continue;
	// If there is a corpse there, keep away.
	if (aiAvoidDirection(dx, dy))
	    continue;
	if (dist >= 0 && dist <= curdist)
	{
	    found = true;

	    if (dist < curdist)
	    {
		// Net new smallest
		ndx = dx;
		ndy = dy;
		curdist = dist;
		nmatch = 1;
	    }
	    else
	    {
		// Replace if chance.
		nmatch++;
		if (!rand_choice(nmatch))
		{
		    ndx = dx;
		    ndy = dy;
		}
	    }
	}
    }

    // If we didn't find a direction, abort
    if (!found)
    {
	if (isAvatar())
	    formatAndReport("%S cannot find a path there.");
	return false;
    }

    // Otherwise, act.
    return actionWalk(ndx, ndy);
}

bool
MOB::aiPathFindToAvoid(POS goal, MOB *avoid)
{
    if (aiPathFindToAvoid(goal, avoid, true))
	return true;
    return aiPathFindToAvoid(goal, avoid, false);
}

double
aiWeightedDist(double dist, double avoid)
{
    // Max effect is 10
    avoid = 10 - avoid;
    if (avoid < 0)
	return dist;

    // Go non-linear!
    avoid *= avoid;
    avoid /= 10;

    return dist + avoid;
}

bool
MOB::aiPathFindToAvoid(POS goal, MOB *avoid, bool avoidmob)
{
    int		dx, dy, ndx, ndy;
    double	curdist, dist;
    int		distmap, avoidmap;

    bool	found;

    if (!avoid)
	return aiPathFindTo(goal, avoidmob);

    // see if already there.
    if (pos() == goal)
	return false;

    // Really, really, inefficient.
    distmap = pos().map()->buildDistMap(goal);
    avoidmap = pos().map()->buildDistMap(avoid->pos());

    // We bias towards path finding so that one will flow around the
    // target rather than stop hard.
    curdist = aiWeightedDist(pos().getDistance(distmap),
			     pos().getDistance(avoidmap));
    
    // We don't care if this square is unreachable, as if it is the
    // square with the mob, almost by definition it is unreachable
    
    found = false;
    int			nmatch = 0;
    FORALL_8DIR(dx, dy)
    {
	dist = pos().delta(dx, dy).getDistance(distmap);

	if (dist < 0)
	    continue;

	if (avoidmob)
	    if (pos().delta(dx, dy).mob())
		continue;

	// Refuse to step beside the avoid mob.
	if (avoid->pos().dist(pos().delta(dx, dy)) <= 1)
	    continue;

	// If the avoid mob has a ranged attack, don't be stupid.
	if (avoid->lookupRanged())
	{
	    if (avoid->canTargetAtRange(pos().delta(dx, dy)))
		continue;
	}
	
	dist = aiWeightedDist(dist, pos().delta(dx, dy).getDistance(avoidmap));
	if (dist <= curdist)
	{
	    found = true;

	    if (dist < curdist)
	    {
		// Net new smallest
		ndx = dx;
		ndy = dy;
		curdist = dist;
		nmatch = 1;
	    }
	    else
	    {
		// Replace if chance.
		nmatch++;
		if (!rand_choice(nmatch))
		{
		    ndx = dx;
		    ndy = dy;
		}
	    }
	}
    }

    // If we didn't find a direction, abort
    if (!found)
    {
	if (isAvatar())
	    formatAndReport("%S cannot find a path there.");
	return false;
    }

    // Otherwise, act.
    return actionWalk(ndx, ndy);
}

bool
MOB::aiFlankTo(POS goal)
{
    int		dx, dy, dist;
    int		angle, resultangle = 0, i, j;
    int		choice = 0;

    MOB		*avatarvis = goal.mob();
    bool	aisrange = false;
    
    if (avatarvis)
	aisrange = avatarvis->lookupRanged() ? true : false;

    pos().dirTo(goal, dx, dy);

    dist = pos().dist(goal);

    if (dist == 0)
	return aiActionWhenAtTarget();
    if (dist == 1)
    {
	// Attack!
	if (canMove(goal, false))
	    return actionBump(dx, dy);
	return false;
    }

    // Move in general direction, preferring a straight line.
    angle = rand_dirtoangle(dx, dy);

    for (j = 0; j <= 2; j++)
    {
	// To flank, we prefer the non-straight approach.
	switch (j)
	{
	    case 0:
		i = 1;
		break;
	    case 1:
		i = 0;
		break;
	    case 2:
		i = 2;
		break;
	}
	rand_angletodir(angle-i, dx, dy);
	if (canMoveDir(dx, dy) &&
	    (!aisrange || !avatarvis->canTargetAtRange(pos().delta(dx, dy))))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = angle-i;
	}
	
	rand_angletodir(angle+i, dx, dy);
	if (canMoveDir(dx, dy) &&
	    (!aisrange || !avatarvis->canTargetAtRange(pos().delta(dx, dy))))
	{
	    choice++;
	    if (!rand_choice(choice))
		resultangle = angle+i;
	}

	if (choice)
	{
	    rand_angletodir(resultangle, dx, dy);
	    return actionWalk(dx, dy);
	}
    }

    return false;
}

