/*
 * Licensed under BSD license.  See LICENCE.TXT  
 *
 * Produced by:	Jeff Lait
 *
 *      	Jacob's Matrix Development
 *
 * NAME:        map.h ( Jacob's Matrix, C++ )
 *
 * COMMENTS:
 */

#include <libtcod.hpp>
#undef CLAMP

#define STARTSTAIRS 0

#ifdef WIN32
#else
#include <unistd.h>
#endif

#include "map.h"
#include "speed.h"
#include "config.h"

#include "mob.h"
#include "item.h"
#include "level.h"

#include "text.h"

#include "dircontrol.h"
#include "scrpos.h"
#include "display.h"
#include "disunion.h"
#include "msg.h"
#include "builder.h"
#include "engine.h"
#include "vec2.h"

#include <fstream>
using namespace std;

// #define DO_TIMING

#define APOCALYPSE_TIME 10000

#define ORIENT_FLIPX	1
#define ORIENT_FLIPY	2
#define ORIENT_FLOP	4

#define ATLAS_WIDTH	10
#define ATLAS_HEIGHT	10

PTRLIST<class FRAGMENT *> MAP::theFragRooms[NUM_ROOMTYPES];

extern const char *glbWorldName;
extern volatile int glbMapBuild;

extern void doChamberScan(MAP *map);
extern void doLookAround(MAP *map, bool onlynew);

class FRAGMENT
{
public:
    FRAGMENT(const char *fname);
    FRAGMENT(int width, int height);
    ~FRAGMENT();

    ROOM		*buildRoom(MAP *map, ROOMTYPE_NAMES type, 
	    int upstair, int downstair,
	    int difficulty, int depth, 
	    int atlasx, int atlasy,
	    const GRID8 *srctiles = nullptr, const GRIDI *srcmetadata = nullptr,
	    const GRIDI *srcelevation = nullptr, 
	    const GRIDI *srcwaterdepth = nullptr, 
	    const GRIDI *srcsnowdepth = nullptr
	    ) const;

    ROOM		*buildOvermap(MAP *map, ROOMTYPE_NAMES type) const;

protected:
    void		 swizzlePos(int &x, int &y, int orient) const;

    int		myW, myH;
    u8		*myTiles;
    BUF		myFName;
    friend class ROOM;
};

POS::POS()
{
    myRoomId = -1;
    myMap = 0;
    myX = myY = myAngle = 0;
}


size_t
POS::hash() const
{
    int	h = myX;
    h = rand_wanginthash(h);
    h ^= myY;
    h = rand_wanginthash(h);
    h ^= myRoomId;
    h = rand_wanginthash(h);
    // Angle isn't used for ==, so not for hash.
    return h;
}

POS
POS::goToCanonical(int x, int y) const
{
    POS result;
    if (!map())
	return result;

    result = map()->buildFromCanonical(x, y);
    return result;
}

bool	
POS::valid() const 
{ 
    if (tile() == TILE_INVALID) return false; 
    return true; 
}

TILE_NAMES	
POS::tile() const 
{ 
    if (!room()) return TILE_INVALID; 
    TILE_NAMES		tile = room()->getTile(myX, myY); 

    return tile;
}

void	
POS::setTile(TILE_NAMES tile) const
{ 
    if (!room()) return; 
    return room()->setTile(myX, myY, tile); 
}

int
POS::metaData() const
{
    if (!valid())
	return -1;
    return room()->getMetaData(myX, myY);
}

void
POS::setMetaData(int metadata) const
{
    if (!valid())
	return;
    room()->setMetaData(myX, myY, metadata);
}

int
POS::elevation() const
{
    if (!valid())
	return 0;
    return room()->getElevation(myX, myY);
}

void
POS::setElevation(int elevation) const
{
    if (!valid())
	return;
    room()->setElevation(myX, myY, elevation);
}

int
POS::ambientTemp() const
{
    if (!valid()) return 0;

    int		ambient = 0;
    if (depth())
    {
	// First level is still chilly.  After that it 
	// hits normal temps.
	if (depth() > 1)
	    ambient = 0;
	else
	    ambient = -300;
    }
    else
    {
	// Overworld depends on daylight...
	int		daylight = map() ? map()->getDaylight() : 100;

	// I really should add a lerp
	// Daytime you can't quite freeze solid...
	int		daytemp = -900;
	int		nighttemp = -2000;
	ambient = daylight * daytemp + (100-daylight) * nighttemp;
	ambient = (ambient+50)/100;
    }

    // Adjust by fire distance...
    ITEMLIST		items;

    spiralSearch(0, 4, [&](POS p, int r) -> bool
    {
	if (!p.valid()) return false;
	p.getAllItems(items);
	for (auto && item : items)
	{
	    if (item->defn().emitheat > r)
	    {
		int radiant = 500;
		if (r > 1)
		    radiant = 100;
		if (r > 2)
		    radiant = 000;
		if (r > 3)
		    radiant = -300;
		ambient = MAX(ambient, radiant);
	    }
	}
	return false;
    });

    return ambient;
}

int
POS::waterDepth() const
{
    if (!valid())
	return 0;
    return room()->getWaterDepth(myX, myY);
}

void
POS::setWaterDepth(int depth) const
{
    if (!valid())
	return;
    room()->setWaterDepth(myX, myY, depth);
}

int
POS::snowDepth() const
{
    if (!valid())
	return 0;
    return room()->getSnowDepth(myX, myY);
}

void
POS::setSnowDepth(int depth) const
{
    if (!valid())
	return;
    room()->setSnowDepth(myX, myY, depth);
}

int
POS::smoke() const
{
    if (!valid())
	return 0;
    return room()->getSmoke(myX, myY);
}

void
POS::setSmoke(int smoke)
{
    if (!valid())
	return;
    room()->setSmoke(myX, myY, smoke);
}

MAPFLAG_NAMES 		
POS::flag() const 
{ 
    if (!room()) return MAPFLAG_NONE; 
    return room()->getFlag(myX, myY); 
}

void	
POS::setFlag(MAPFLAG_NAMES flag, bool state) const
{ 
    if (!room()) return; 
    return room()->setFlag(myX, myY, flag, state); 
}

bool
POS::prepSquareForDestruction() const
{
    ROOM		*r = room();
    int			dx, dy;

    if (!valid())
	return false;

    if (!r)
	return false;

    if (!myX || !myY || myX == r->width()-1 || myY == r->height()-1)
    {
	// Border tile, borders to nothing, so forbid dig
	return false;
    }

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

    FORALL_8DIR(dx, dy)
    {
	if (r->getTile(myX+dx, myY+dy) == TILE_INVALID)
	{
	    // Expand our walls.
	    r->setTile(myX+dx, myY+dy, wall_tile);
	}
	// Do not dig around portals!
	if (r->getFlag(myX+dx, myY+dy) & MAPFLAG_PORTAL)
	    return false;
    }

    // If this square is a portal, don't dig!
    if (r->getFlag(myX, myY) & MAPFLAG_PORTAL)
	return false;

    return true;
}

bool
POS::forceConnectNeighbours() const
{
    if (!map())
	return false;
    if (!room())
	return false;

    return room()->forceConnectNeighbours();
}


bool
POS::digSquare() const
{
    if (!defn().isdiggable)
	return false;

    if (!prepSquareForDestruction())
	return false;

    TILE_NAMES		nt;

    nt = TILE_FLOOR;
#if 0
    if (metaData() < 0)
        nt = TILE_DOOR_EMPTY;
#endif
    if (tile() == TILE_WALL)
	nt = TILE_BROKENWALL;

    setTile(nt);

    return true;
}

bool
POS::sinkSquare() const
{
    auto makeReport = [self = this](const ITEMLIST &items, const char *verb)
    {
        if (self->isFOV() && items.entries())
        {
            bool            isplural = false;
            BUF             list;

            isplural = false;
            list = ITEM::buildList(items, &isplural);
            list.strcat(" ");
            list.strcat(gram_conjugate(verb, isplural ? VERB_THEY : VERB_IT));
            list.strcat(".");
            self->formatAndReport(gram_capitalize(list));
        }
    };

    MOB		*m = mob();
    if (m && !m->canFly())
    {
	m->soakInWater(50);
	m->giftItem(ITEM_SWIMMING);
    }

    // ALl items lost..
    ITEMLIST            lostlist;
    getAllItems(lostlist);
    makeReport(lostlist, "sink");

    // Future self will be annoyed quest are not exempt.
    for (auto && item : lostlist)
    {
	item->move(POS());
	delete item;
    }
    // All the snow melts.
    setSnowDepth(0);
    return true;
}

bool
POS::breakIce() const
{
    if (tile() != TILE_ICE)
	return false;

    formatAndReport("The ice breaks!");
    setTile(TILE_WATER);
    sinkSquare();
    return true;
}

bool
POS::burnSquare() const
{
    auto makeReport = [self = this](const ITEMLIST &items, const char *verb)
    {
        if (self->isFOV() && items.entries())
        {
            bool            isplural = false;
            BUF             list;

            isplural = false;
            list = ITEM::buildList(items, &isplural);
            list.strcat(" ");
            list.strcat(gram_conjugate(verb, isplural ? VERB_THEY : VERB_IT));
            list.strcat(".");
            self->formatAndReport(gram_capitalize(list));
        }
    };

    if (tile() == TILE_ICEWALL)
    {
        formatAndReport("The wall of ice melts.");
        if (metaData() < 0)
            setTile(TILE_DOOR_EMPTY);
        else
            setTile(TILE_DIRT);
    }
    // If the ice is exposed, it melts.
    if (!snowDepth() && tile() == TILE_ICE)
    {
        formatAndReport("The ice melts.");
	setTile(TILE_WATER);
	sinkSquare();
    }
    if (snowDepth())
    {
	formatAndReport("Snow melts.");
	int melt = MIN(snowDepth(), 5);
	setSnowDepth(snowDepth()-melt);
	setBrokenSnow(false);
    }
    ITEMLIST            items, burnlist, boillist, cooklist, bakelist, lightlist;
    ITEM		*campfire = nullptr;
    getAllItems(items);
    for (auto && item : items)
    {
        // I wish I hadn't broken materials here!
	// You could say sticks and coal should burn, but the idea
	// here is quick & fast burning.
	// instead campfires have their own logic for that.
        if (item->isSpellbook() || item->isScroll() ||
            item->getDefinition() == ITEM_SPELLBOOK_BLANK ||
            item->getDefinition() == ITEM_SCROLL_BLANK ||
	    item->getDefinition() == ITEM_PAPER ||
	    item->getDefinition() == ITEM_PITCH
	    )
        {
            burnlist.append(item);
        }
        else if (item->isPotion())
        {
            boillist.append(item);
        }
        else if (item->getDefinition() == ITEM_MEATCHUNK ||
		 item->getDefinition() == ITEM_EGG)
        {
            cooklist.append(item);
        }
        else if (item->getDefinition() == ITEM_CLAY)
        {
            bakelist.append(item);
        }
        else if (item->getDefinition() == ITEM_TORCH)
        {
            lightlist.append(item);
        }
	else if (item->getDefinition() == ITEM_CAMPFIRE)
	{
	    campfire = item;
	}
    }

    makeReport(burnlist, "burn");
    makeReport(boillist, "boil");
    makeReport(cooklist, "cook");
    makeReport(bakelist, "bake");
    makeReport(lightlist, "light");

    // Do operations...
    for (auto && item : burnlist)
    {
        item->move(POS());
        delete item;
    }

    for (auto && item : boillist)
    {
        item->move(POS());
        delete item;
    }

    for (auto && item : cooklist)
    {
	item->cook();
    }
    for (auto && item : bakelist)
    {
	// This is a path to rocks without finding the mountain..
	item->setDefinition(ITEM_ROCK);
    }
    for (auto && item : lightlist)
    {
	// This is a path to rocks without finding the mountain..
	J_ASSERT(item->getDefinition() == ITEM_TORCH);
	item->setDefinition(ITEM_LIT_TORCH);
    }

    // Now if the source of the burning is a campfire, we consume
    // any fuel in this square...
    if (campfire)
    {
	burnlist.clear();
	items.clear();
	getAllItems(items);
	int		fuel = 0;
	for (auto && item : items)
	{
	    if (item->getDefinition() == ITEM_LOG)
	    {
		burnlist.append(item);
		fuel += 200 * item->getStackCount();
	    }
	    else if (item->getDefinition() == ITEM_STICK ||
		     item->getDefinition() == ITEM_COAL)
	    {
		burnlist.append(item);
		fuel += 50 * item->getStackCount();
	    }
	}
	makeReport(burnlist, "burn");
	for (auto && item : burnlist)
	{
	    item->move(POS());
	    delete item;
	}

	// See if the tile burns...  Intentionally starting on a tile
	// always triggers.
	if (defn().burntile != TILE_NONE)
	{
	    // Final charring is silent.
	    if (defn().burntile != TILE_CHARREDGROUND)
	    {
		BUF	buf;
		buf.sprintf("The fire burns %s%s.", gram_getarticle(defn().legend), defn().legend);
		formatAndReport(buf);
	    }
	    fuel += defn().fuel;
	    setTile(defn().burntile);
	}
	if (fuel)
	{
	    campfire->addTimer(fuel);
	    formatAndReport("%S <grow> brighter.", campfire);
	}


	// Now spread the fire....
	// Has to pass the threshold to ignite adjacent.
	// 100 is sufficient to melt snow so we do have our normal
	// fires melt.
	// ignite adjacent...
	int heat = campfire->getTimer();
	ITEMLIST targetitems;
	if (heat > 100)
	{
	    int		dx, dy;
	    FORALL_8DIR_RAND(dx, dy)
	    {
		if (!rand_chance(5))
		    continue;
		POS	target = delta(dx, dy);
		if (target.snowDepth())
		{
		    target.setSnowDepth(MAX(0, target.snowDepth()-5));
		    target.formatAndReport("Snow melts.");
		    continue;
		}

		if (target.defn().combust_heat >= 0 && 
		    target.defn().combust_heat <= heat)
		{
		    // Make sure no campfire already there.
		    target.getAllItems(targetitems);
		    bool hasfire = false;
		    for (auto && targetitem : targetitems)
		    {
			if (targetitem->getDefinition() == ITEM_CAMPFIRE)
			{
			    hasfire = true;
			    break;
			}
		    }

		    if (!hasfire)
		    {
			target.formatAndReport("The fire spreads.");
			ITEM *fire = ITEM::create(map(), ITEM_CAMPFIRE);
			fire->move(target);
		    }
		}
	    }
	}

    }

    return true;
}

bool
POS::teleportItems() const
{
    ITEMLIST            items;
    getAllItems(items);

    // Remove all from map...
    bool teleported = false;
    for (auto && item : items)
    {
	// Remove first so if we fall on same square and merge
	// we won't affect something still in the list.
	removeItem(item);
    }
    for (auto && item : items)
    {
	if (isFOV())
	{
	    formatAndReport("%S <vanish>.", item);
	}
	POS	np = room()->getRandomPos(*this, nullptr);
	np.setAngle(angle());

	teleported = true;

	np.addItem(item);
	np.formatAndReport("%S <appear>.", item);
    }

    // Teleport any map features that we know how to move.
    switch (tile())
    {
	case TILE_DOOR_CLOSED:
	case TILE_DOOR_JAMMED_UNKNOWN:
	case TILE_DOOR_JAMMED:
	case TILE_DOOR_LOCKED_UNKNOWN:
	case TILE_DOOR_LOCKED:
	case TILE_DOOR_OPEN:
	case TILE_DOOR_BROKEN:
	{
	    // See if there is a new pos.  Must be a doorway.
	    POS np = room()->getRandomTile(TILE_DOOR_EMPTY, POS(), nullptr);
	    teleported = true;
	    if (np.valid())
	    {
		formatAndReport("The door vanishes.");
		TILE_NAMES door = tile();
		setTile(TILE_DOOR_EMPTY);
		np.setTile(door);
		np.formatAndReport("A door appears!");
	    }
	    else
		formatAndReport("The door shudders.");
	    break;
	}
        default:
            break;
    }
    return teleported;
}

bool
POS::polymorphItems() const
{
    ITEMLIST            items;
    getAllItems(items);

    // Remove all from map...
    bool polyd = false;
    for (auto && item : items)
    {
	polyd |= item->polymorph(!isFOV());
	// Only first stack.
	if (polyd)
	    return polyd;
    }

    // Polymorph any map features that we know how to poly.
    switch (tile())
    {
	case TILE_DOOR_CLOSED:
	case TILE_DOOR_JAMMED_UNKNOWN:
	case TILE_DOOR_JAMMED:
	case TILE_DOOR_LOCKED_UNKNOWN:
	case TILE_DOOR_LOCKED:
	case TILE_DOOR_OPEN:
	case TILE_DOOR_BROKEN:
	{
	    static const TILE_NAMES doortypes[] = 
	    {
		TILE_DOOR_CLOSED,
		TILE_DOOR_JAMMED_UNKNOWN,
		TILE_DOOR_LOCKED_UNKNOWN,
		TILE_DOOR_OPEN,
		TILE_DOOR_BROKEN,
	    };
	    polyd = true;
	    formatAndReport("The door transforms.");
	    TILE_NAMES newdoor = 
		doortypes[rand_choice(sizeof(doortypes)/sizeof(TILE_NAMES))];

	    setTile(newdoor);
	}
        default:
            break;
    }
    return polyd;
}

bool
POS::visibleFromAvatar() const
{
    if (!isFOV()) return false;		// easiest test.

    if (!map()) return false;
    if (!map()->avatar()) return true;
    if (map()->avatar()->pos() == *this) return true;	// "see" own square
    if (map()->avatar()->isBlind()) return false;
    return isFOV();
}

bool
POS::soakSquare(POTION_NAMES potion) const
{
    ITEMLIST            items;
    getAllItems(items);
    bool		didsoak = false;
    for (auto && item : items)
    {
	if (item->waterSoak(potion))
	    didsoak = true;
    }
    return didsoak;
}

bool
POS::shatterSquare() const
{
    ITEMLIST            items;
    getAllItems(items);
    bool		didshatter = false;
    for (auto && item : items)
    {
	didshatter |= item->shatter();
    }
    switch (tile())
    {
	case TILE_ICEWALL:
	    formatAndReport("The ice wall shatters.");
	    setTile(TILE_DIRT);
	    didshatter = true;
	    break;

	case TILE_ICE:
	    breakIce();
	    didshatter = true;
	    break;

	case TILE_BRIDGE:
	    formatAndReport("The bridge shatters.");
	    setTile(TILE_WATER);
	    sinkSquare();
	    didshatter = true;
	    break;

	case TILE_ALTAR:
	    formatAndReport("The altar shatters.");
	    setTile(TILE_ALTAR_BROKEN);
	    didshatter = true;
	    break;

	case TILE_DOOR_CLOSED:
	case TILE_SECRETDOOR:
	case TILE_DOOR_JAMMED_UNKNOWN:
	case TILE_DOOR_JAMMED:
	case TILE_DOOR_LOCKED_UNKNOWN:
	case TILE_DOOR_LOCKED:
	    formatAndReport("The door shatters.");
	    setTile(TILE_DOOR_BROKEN);
	    didshatter = true;
	    break;
	case TILE_DOOR_OPEN:
	case TILE_DOOR_BROKEN:
	case TILE_DOOR_EMPTY:
	    break;
        default:
            break;
    }

    return didshatter;
}

bool
POS::restoreSquare() const
{
    bool		didmend = false;

    {
	ITEMLIST            items;
	getAllItems(items);
	for (auto && item : items)
	{
	    didmend |= item->mend(nullptr);

	    if (!mob() && item->canResurrect())
	    {
		didmend = true;
		if (item->resurrect(nullptr))
		{
		    item->move(POS());
		    delete item;
		}
	    }
	    // Only do first...
	    if (didmend) return didmend;
	}
    }

    switch (tile())
    {
	case TILE_ALTAR_BROKEN:
	    formatAndReport("The altar re-assembles into its original shape.");
	    formatAndReport("You have a disturbing feeling this was a very bad idea.");
	    setTile(TILE_ALTAR);
	    didmend = true;
	    break;

	case TILE_DOOR_JAMMED:
	    formatAndReport("The door loosens.");
	    didmend = true;
	    // FALLTHROUGH
	case TILE_DOOR_JAMMED_UNKNOWN:
	    // If it was unknown you don't know it happened.
	    setTile(TILE_DOOR_CLOSED);
	    break;

	case TILE_DOOR_BROKEN:
	    // If you mend an unknown jammed door you won't know.
	    formatAndReport("The door re-assembles into place.");
	    if (rand_chance(50))
		setTile(TILE_DOOR_CLOSED);
	    else
		setTile(TILE_DOOR_LOCKED_UNKNOWN);
	    didmend = true;
	    break;

	case TILE_DOOR_CLOSED:
	case TILE_SECRETDOOR:
	case TILE_DOOR_LOCKED_UNKNOWN:
	case TILE_DOOR_LOCKED:
	case TILE_DOOR_OPEN:
	case TILE_DOOR_EMPTY:
	    break;
        default:
            break;
    }

    return didmend;
}

ROOMTYPE_NAMES
POS::roomType() const
{
    if (!room()) return ROOMTYPE_NONE;
    return room()->type();
}

bool
POS::isLit() const
{
    if (room() && room()->isLit()) return true;
    return isDirectlyLit();
}

bool
POS::breakSnow() const
{
    int		snow = snowDepth();
    bool	interest = false;
    if (snow && !isBrokenSnow())
    {
	snow = snow / 2;
	if (snow)
	{
	    setBrokenSnow(true);
	    interest = true;
	}
	setSnowDepth(snow);
    }
    return interest;
}

bool
POS::couldEverHarvest() const
{
    // Can't see if covered in snow.
    if (defn().height < snowDepth()) return false;
    // Need to stand on to harvest:
    if (!isPassable()) return false;
    if (defn().tree != TREE_NONE) return true;
    if (defn().ore != ORE_NONE) return true;
    if (tile() == TILE_REEDS) return true;
    if (tile() == TILE_CLAY) return true;
    return false;
}

void
POS::mapCurrentLevel() const
{
    // Assumes room is level.
    if (!room()) return;

    room()->setAllFlags(MAPFLAG_MAPPED, true);
}

MOB *
POS::mob() const 
{ 
    ROOM	*thisroom = room();
    if (!thisroom) return 0; 

    if (! (thisroom->getFlag(myX, myY) & MAPFLAG_MOB) )
	return 0;

    return myMap->findMobByLoc(thisroom, myX, myY);
}

int
POS::getAllMobs(MOBLIST &list) const
{
    // Can't retrieve multiple mobs at the moment!
    if (!room()) return list.entries();

    ROOM	*thisroom = room();
    if (! (thisroom->getFlag(myX, myY) & MAPFLAG_MOB) )
	return 0;

    return myMap->findAllMobsByLoc(list, thisroom, myX, myY);
}

int
POS::getDistance(int distmap) const 
{ 
    if (!room()) return -1; 

    return room()->getDistance(distmap, myX, myY); 
}

void
POS::setDistance(int distmap, int dist) const 
{ 
    if (!room()) return; 

    return room()->setDistance(distmap, myX, myY, dist); 
}

int
POS::getAllItems(ITEMLIST &list) const
{
    ROOM	*thisroom = room();
    if (!thisroom) return list.entries(); 

    if (! (thisroom->getFlag(myX, myY) & MAPFLAG_ITEM) )
	return list.entries();

    return myMap->findAllItemsByLoc(list, thisroom, myX, myY);
}

ITEM *
POS::item() const 
{ 
    ROOM	*thisroom = room();
    if (!thisroom) return 0; 

    if (! (thisroom->getFlag(myX, myY) & MAPFLAG_ITEM) )
	return 0;

    return myMap->findItemByLoc(thisroom, myX, myY);
}

int
POS::allItems(ITEMLIST &items) const 
{ 
    items.clear();
    if (!room()) return 0; 
    getAllItems(items);
    return items.entries();
}

int
POS::depth() const
{
    ROOM *r = room();
    if (!r)
	return 0;
    return r->getDepth();
}

ROOM *
POS::room() const 
{ 
    if (myRoomId < 0) return 0; 
    return myMap->myRooms(myRoomId); 
}

u8
POS::room_symbol() const
{
    if (room())
	return room()->getSymbol();
    return 'X';
}

ATTR_NAMES
POS::room_color() const
{
    ATTR_NAMES		result = ATTR_NORMAL;

    if (room())
    {
	result = MAP::depthColor(room()->getDepth(), false);
    }

    return result;
}

u8
POS::colorblind_symbol(u8 oldsym) const
{
    if (room())
    {
	if (oldsym == '#')
	{
	    static const char *wallsym = "#%&*O+";
	    int	depth = BOUND(room()->getDepth(), 0, 5);
	    oldsym = wallsym[depth];
	}
    }
    return oldsym;
}


int		 
POS::roomSeed() const 
{ 
    if (room()) 
	return room()->getSeed(); 
    return -1; 
}

int
POS::getRoomDepth() const
{
    return depth();
}

int         
POS::getAllRoomTiles(TILE_NAMES tile, POSLIST &list) const
{ 
    list.clear(); 
    if (!room()) return 0; 
    room()->getAllTiles(tile, list); 
    return list.entries();
}

void
POS::moveMob(MOB *mob, POS oldpos) const
{
    // If wasn't in a room, add it!
    if (oldpos.myRoomId < 0)
    {
	if (myRoomId >= 0)
	{
	    addMob(mob);
	}
    }
    else
    {
	// Was in a room before.  Either a delete, or a real move.
	if (myRoomId < 0)
	{
	    // Removal.
	    oldpos.removeMob(mob);
	}
	else
	{
	    // A proper move.
	    myMap->moveMob(mob, *this, oldpos);

	    if (!oldpos.mob())
		oldpos.setFlag(MAPFLAG_MOB, false);
	    setFlag(MAPFLAG_MOB, true);
	}
    }
}

void
POS::removeMob(MOB *m) const
{
    if (myRoomId < 0) return; 

    myMap->removeMob(m, *this);
    if (!mob())
	setFlag(MAPFLAG_MOB, false);
}

void
POS::addMob(MOB *m) const
{
    if (myRoomId < 0) return; 

    myMap->addMob(m, *this);
    setFlag(MAPFLAG_MOB, true);
}

void
POS::replaceMob(MOB *orig, MOB *newmob) const
{
    if (!valid()) return;
    myMap->replaceMob(orig, newmob);
}


ITEM *
POS::splitStack(ITEM *item) const
{
    J_ASSERT(myRoomId >= 0);
    if (myRoomId < 0) return item;

    if (item->getStackCount() > 1)
    {
	ITEM	*result;

	item->decStackCount();
	result = item->createCopy(map());
	result->setStackCount(1);

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

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

void
POS::removeItem(ITEM *it) const
{
    if (myRoomId < 0) return; 

    myMap->removeItem(it, *this);

    if (!item())
	setFlag(MAPFLAG_ITEM, false);
}

void
POS::addItem(ITEM *it) const
{
    if (myRoomId < 0) return; 

    ITEMLIST its;
    getAllItems(its);

    if (!its.entries())
	setFlag(MAPFLAG_ITEM, true);

    for (int i = 0; i < its.entries(); i++)
    {
	if (it->canStackWith(its(i)))
	{
	    its(i)->combineItem(it);
	    delete it;
	    return;
	}
    }

    myMap->addItem(it, *this);
}

void
POS::save(ostream &os) const
{
    os.write((const char *)&myX, sizeof(int));
    os.write((const char *)&myY, sizeof(int));
    os.write((const char *)&myAngle, sizeof(int));
    os.write((const char *)&myRoomId, sizeof(int));
}

void
POS::load(istream &is)
{
    is.read((char *)&myX, sizeof(int));
    is.read((char *)&myY, sizeof(int));
    is.read((char *)&myAngle, sizeof(int));
    is.read((char *)&myRoomId, sizeof(int));
}

MOB *
POS::traceBullet(int range, int dx, int dy, int *rangeleft) const
{
    if (rangeleft)
	*rangeleft = 0;
    if (!dx && !dy)
	return mob();

    POS		next = *this;

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

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

POS
POS::traceBulletPos(int range, int dx, int dy, bool stopbeforewall, bool stopatmob) const
{
    if (!dx && !dy)
	return *this;

    POS		next = *this;
    POS		last = *this;

    while (range > 0)
    {
	range--;
	next = next.delta(dx, dy);

	// Stop at a mob.
	if (stopatmob && next.mob())
	    return next;

	if (!next.defn().ispassable)
	{
	    // Hit a wall.  Either return next or last.
	    if (stopbeforewall)
		return last;
	    return next;
	}
	last = next;
    }
    return next;
}

void
POS::postEvent(EVENTTYPE_NAMES type, u8 sym, ATTR_NAMES attr) const
{
    map()->myDisplay->postEvent(EVENT(*this, sym, attr, type));
}

void
POS::postEvent(EVENTTYPE_NAMES type, u8 sym, ATTR_NAMES attr, const char *text) const
{
    map()->myDisplay->postEvent(EVENT(*this, sym, attr, type, text));
}

void
POS::displayBullet(int range, int dx, int dy, u8 sym, ATTR_NAMES attr, bool stopatmob) const
{
    if (!dx && !dy)
	return;

    POS		next = delta(dx, dy);

    while (range > 0)
    {
	if (!next.valid())
	    return;
	
	// Stop at walls.
	if (!next.isPassable())
	    return;

	// Stop at mobs
	if (stopatmob && next.mob())
	    return;

	next.postEvent(EVENTTYPE_FORESYM, sym, attr);

	range--;
	next = next.delta(dx, dy);
    }
}

void
POS::rotateByAngle(int angle, int &dx, int &dy)
{
    int		odx = dx;
    int		ody = dy;
    switch (angle & 3)
    {
	case 0:			// ^
	    break;		
	case 1:			// ->
	    dx = -ody;
	    dy = odx;
	    break;
	case 2:			// V
	    dx = -odx;
	    dy = -ody;
	    break;
	case 3:			// <-
	    dx = ody;
	    dy = -odx;
	    break;
    }
}

void
POS::rotateToLocal(int &dx, int &dy) const
{
    rotateByAngle(myAngle, dx, dy);
}

void
POS::rotateToWorld(int &dx, int &dy) const
{
    int		odx = dx;
    int		ody = dy;
    switch (myAngle & 3)
    {
	case 0:			// ^
	    break;		
	case 1:			// ->
	    dx = ody;
	    dy = -odx;
	    break;
	case 2:			// V
	    dx = -odx;
	    dy = -ody;
	    break;
	case 3:			// <-
	    dx = -ody;
	    dy = odx;
	    break;
    }
}

int
POS::dist(POS goal) const
{
    if (!goal.valid() || !valid())
    {
	if (!room())
	    return 1000;
	return room()->width() + room()->height();
    }
    if (goal.myRoomId == myRoomId)
    {
	return MAX(ABS(goal.myX-myX), ABS(goal.myY-myY));
    }

    // Check to see if we are in the map cache.
    int		dx, dy;
    if (map()->computeDelta(*this, goal, dx, dy))
    {
	// Yah, something useful.
	return MAX(ABS(dx), ABS(dy));
    }

    // Consider it infinitely far.  Very dangerous as monsters
    // will get stuck on portals :>
    return room()->width() + room()->height();
}

void
POS::dirTo(POS goal, int &dx, int &dy) const
{
    if (!goal.valid() || !valid())
    {
	// Arbitrary...
	dx = 0;
	dy = 1;
	rotateToWorld(dx, dy);
    }

    if (goal.myRoomId == myRoomId)
    {
	dx = SIGN(goal.myX - myX);
	dy = SIGN(goal.myY - myY);

	// Convert to world!
	rotateToWorld(dx, dy);
	return;
    }

    // Check to see if we are in the map cache.
    if (map()->computeDelta(*this, goal, dx, dy))
    {
	// Map is nice enough to give it in world coords.
	dx = SIGN(dx);
	dy = SIGN(dy);
	return;
    }

    // Umm.. Be arbitrary?
    rand_direction(dx, dy);
}

bool
POS::deltaTo(POS goal, int &dx, int &dy) const
{
    dx = 0;
    dy = 0;
    if (!goal.valid() || !valid())
    {
	// Arbitrary...
	return false;
    }

    if (goal.myRoomId == myRoomId)
    {
	dx = (goal.myX - myX);
	dy = (goal.myY - myY);

	// Convert to world!
	rotateToWorld(dx, dy);
	return true;
    }

    // Check to see if we are in the map cache.
    if (map()->computeDelta(*this, goal, dx, dy))
    {
	// Map is nice enough to give it in world coords.
	return true;
    }

    return false;
}

void
POS::setAngle(int angle)
{
    myAngle = angle & 3;
}

POS
POS::rotate(int angle) const
{
    POS		p;

    p = *this;
    p.myAngle += angle;
    p.myAngle &= 3;

    return p;
}

POS
POS::delta4Direction(int angle) const
{
    int		dx, dy;

    rand_getdirection(angle, dx, dy);

    return delta(dx, dy);
}

POS
POS::delta(int dx, int dy) const
{
    if (!valid())
	return *this;

    if (!dx && !dy)
	return *this;

    // Fast check for simple cases without portals.
    ROOM		*thisroom = room();
    if (!thisroom->myPortals.entries())
    {
	int		 ldx = dx, ldy = dy;
	rotateToLocal(ldx, ldy);

	int		nx = myX + ldx, ny = myY + ldy;

	if (nx >= 0 && nx < thisroom->width() &&
	    ny >= 0 && ny < thisroom->height())
	{
	    // Trivial update!
	    POS		dst;
	    dst = *this;
	    dst.myX = nx;
	    dst.myY = ny;
	    return dst;
	}
    }

    // We want to make a single step in +x or +y.
    // The problem is diagonals.  We want to step in both xy and yx orders
    // and take whichever is valid.  In most cases they will be equal...
    if (dx && dy)
    {
	POS	xfirst, yfirst;
	int	sdx = SIGN(dx);
	int	sdy = SIGN(dy);

	xfirst = delta(sdx, 0).delta(0, sdy);
	yfirst = delta(0, sdy).delta(sdx, 0);

	if (!xfirst.valid())
	{
	    // Must take yfirst
	    return yfirst.delta(dx - sdx, dy - sdy);
	}
	if (!yfirst.valid())
	{
	    // Must take xfirst.
	    return xfirst.delta(dx - sdx, dy - sdy);
	}

	// Both are valid.  In all likeliehoods, identical.
	// But if one is wall and one is not, we want the non-wall!
	if (!xfirst.defn().ispassable)
	    return yfirst.delta(dx - sdx, dy - sdy);

	// WLOG, use xfirst now.
	return xfirst.delta(dx - sdx, dy - sdy);
    }

    // We now have a simple case of a horizontal or vertical step
    rotateToLocal(dx, dy);

    int		sdx = SIGN(dx);
    int		sdy = SIGN(dy);

    PORTAL	*portal;

    portal = thisroom->getPortal(myX + sdx, myY + sdy);
    if (portal)
    {
	// Teleport!
	POS		dst = portal->myDst;

	// We should remain on the same map!
	J_ASSERT(dst.map() == myMap);

	// Apply the portal's twist.
	dst.myAngle += myAngle;
	dst.myAngle &= 3;

	// Portals have no thickness so we re-apply our original delta!
	// Actually, I changed my mind since then, so it is rather important
	// we actually do dec dx/dy.
	dx -= sdx;
	dy -= sdy;
	rotateToWorld(dx, dy);
	return dst.delta(dx, dy);
    }

    // Find the world delta to apply to our next square.
    dx -= sdx;
    dy -= sdy;
    rotateToWorld(dx, dy);

    // Get our next square...
    POS		dst = *this;

    dst.myX += sdx;
    dst.myY += sdy;

    if (dst.myX < 0 || dst.myX >= thisroom->width() ||
	dst.myY < 0 || dst.myY >= thisroom->height())
    {
	// An invalid square!
	// Check our connectivity for implicit connections, for these
	// we jump up to the atlas layer.
	int angle = rand_dir4_toangle(sdx, sdy);
	if (thisroom->myConnectivity[angle] == ROOM::IMPLICIT)
	{
	    int		newatlasx = thisroom->atlasX() + sdx;
	    int		newatlasy = thisroom->atlasY() + sdy;

	    // Wrap.
	    newatlasx = newatlasx % ATLAS_WIDTH;
	    if (newatlasx < 0) newatlasx += ATLAS_WIDTH;
	    newatlasy = newatlasy % ATLAS_HEIGHT;
	    if (newatlasy < 0) newatlasy += ATLAS_HEIGHT;

	    ROOM	*droom = map()->findAtlasRoom(newatlasx, newatlasy);

	    if (!droom)
	    {
		// Failed!
		dst.myRoomId = -1;
	    }
	    else
	    {
		// Reset the coordinate that left our range with the
		// equivalnet maximal in destination.
		if (dst.myX < 0)
		    dst.myX = droom->width()-1;
		else if (dst.myX >= thisroom->width())
		    dst.myX = 0;
		if (dst.myY < 0)
		    dst.myY = droom->height()-1;
		else if (dst.myY >= thisroom->height())
		    dst.myY = 0;
		// Now validate we are inside the dest room coordinates
		// (as we assume the non-wrapped will matchup, but it
		// might not!
		if (dst.myX < 0 || dst.myX >= droom->width() ||
		    dst.myY < 0 || dst.myY >= droom->height())
		{
		    // Failed!
		    dst.myRoomId = -1;
		}
		else
		{
		    // We have a wrap!
		    dst.myRoomId = droom->getId();
		}
	    }
	}
	else
	{
	    // either should have gone through a portal or closed.
	    // Limbo might technically need to expand, but delta()
	    // is expected to be read only on the maps.
	    dst.myRoomId = -1;
	}
	return dst.delta(dx, dy);
    }

    return dst.delta(dx, dy);
}

POS
POS::randAdjacent(bool checkmob, bool checkpassable) const
{
    int		dx, dy;
    int		angle = rand_choice(8);
    POS 	result;
    for (int offset = 0; offset < 8; offset++)
    {
	rand_angletodir(angle+offset, dx, dy);
	POS np = this->delta(dx, dy);
	if (np.valid() && (!checkmob || !np.mob()) && (!checkpassable || np.isPassable()))
	{
	    result = np;
	    break;
	}
    }
    return result;
}

void
POS::makeNoise(int loudness) const
{
    if (!valid())
	return;

    MOBLIST		mobs;
    map()->getAllMobs(mobs);
    for (auto && mob : mobs)
    {
	if (!mob) continue;
	POS	pos = mob->pos();
	if (pos.depth() != depth())
	    continue;

	int distance = dist(pos);
	int attenuated = loudness - distance / 8;
	if (attenuated > 0)
	{
	    mob->heardNoise(*this, attenuated);
	}
    }
}

void
POS::reportFireball(int radius, const char *fireball) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;

    if (radius <= 1)
	return;

    if (!fireball || !*fireball)
	return;

    MOB		*avatar = map()->avatar();
    if (!avatar)
	return;

    int		dx, dy;
    bool hasdelta = avatar->pos().deltaTo(
			*this,
			dx, dy);
    if (!hasdelta)
	return;
    BUF		report;
    report.sprintf("A %s%s at %s.",
            radius > 2 ? "large " : "",
	    fireball,
	    rand_describeoffset(dx, dy).buffer());

    msg_report(report);
}

void
POS::reportCone(int dx, int dy, int radius, const char *fireball) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;

    if (radius <= 1)
	return;

    if (!fireball || !*fireball)
	return;

    MOB		*avatar = map()->avatar();
    if (!avatar)
	return;

    BUF		report;
    report.sprintf("A %s%s to the %s.",
            radius > 2 ? "large " : "",
	    fireball,
	    rand_dirtoname(dx, dy));

    msg_report(report);
}

void
POS::reportRay(int dx, int dy, const char *fireball) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;

    if (!fireball || !*fireball)
	return;

    MOB		*avatar = map()->avatar();
    if (!avatar)
	return;

    BUF		report;
    report.sprintf("A %s shoots to the %s.",
	    fireball,
	    rand_dirtoname(dx, dy));

    msg_report(report);
}

void
POS::formatAndReport(const char *msg) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;
    // Probably need this but need the one square exception?
    //if (!isLit())
    //	return;

    msg_report(msg);
}

void
POS::formatAndReport(const char *msg, const MOB *subject) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;

    msg_format(msg, subject);
}

void
POS::formatAndReport(const char *msg, const ITEM *subject) const
{
    if (!valid())
	return;
    if (!isFOV())
	return;

    msg_format(msg, subject);
}

void
POS::describeSquare(bool isblind) const
{
    BUF		 buf;
    buf.reference(defn().legend);
    ITEMLIST	 itemlist;

    // Replace description if snow is deep.
    if (snowDepth() > defn().height)
    {
	if (isBrokenSnow())
	    buf.reference(defn(TILE_BROKENSNOW).legend);
	else
	    buf.reference(defn(TILE_SNOW).legend);
    }

    // Always report terrain from memory!
    if (!isblind)
	msg_format("%o.", 0, buf);
    else
	msg_format("Mapped %o.", 0, buf);
    
    if (!isblind)
    {
	if (mob())
	    msg_format("You see %O.", 0, mob());

	allItems(itemlist);
	if (itemlist.entries())
	{
	    BUF		msg;
	    msg.strcpy("You 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(".");
	    msg_report(msg);
	}
    }
}

void
POS::describeChamber(const char *prompt) const
{
    BUF		msg;

    CHAMBER_NAMES	cham = chamber();
    msg.sprintf("%s %s.  ", prompt, glb_chamberdefs[cham].descr);
    msg_report(msg);
}

CHAMBER_NAMES
POS::chamber() const
{
    int		m = metaData();
    if (m < 0)
	return CHAMBER_NONE;

    int		segments = m & 7;
    m >>= 3;
    int		a1 = m & 3;
    m >>= 2;
    int		a2 = m & 3;

    switch (segments)
    {
	case 4:
	    return CHAMBER_LARGEROOM;

	case 3:
	{
	    // Gotta be a smarter way then this, oh well.
	    // Especially as I built this with N and S reversed!
	    switch (a1)
	    {
		case 0:		// S
		    switch (a2)
		    {
			case 0:	// S
			    break;
			case 1: // E
			    return CHAMBER_ELBOW_SE;
			case 2: // N
			    return CHAMBER_LONGHALLWAY_NS;
			case 3: // W
			    return CHAMBER_ELBOW_SW;
		    }
		    break;
		case 1:		// E
		    switch (a2)
		    {
			case 0:	// S
			    return CHAMBER_ELBOW_SE;
			case 1: // E
			    break;
			case 2: // N
			    return CHAMBER_ELBOW_NE;
			case 3: // W
			    return CHAMBER_LONGHALLWAY_WE;
		    }
		    break;
		case 2:		// N
		    switch (a2)
		    {
			case 0:	// S
			    return CHAMBER_LONGHALLWAY_NS;
			case 1: // E
			    return CHAMBER_ELBOW_NE;
			case 2: // N
			    break;
			case 3: // W
			    return CHAMBER_ELBOW_NW;
		    }
		case 3:		// W
		    switch (a2)
		    {
			case 0:	// S
			    return CHAMBER_ELBOW_SW;
			case 1: // E
			    return CHAMBER_LONGHALLWAY_WE;
			case 2: // N
			    return CHAMBER_ELBOW_NW;
			case 3: // W
			    break;
		    }
	    }
	    break;
	}

	case 2:
	{
	    switch (a1)
	    {
		case 0:
		case 2:
		    return CHAMBER_SHORTHALLWAY_NS;
		case 1:
		case 3:
		    return CHAMBER_SHORTHALLWAY_WE;
	    }
	    break;
	}
	case 1:
	    return CHAMBER_SMALLROOM;
    }

    J_ASSERT(!"Invalid room type!");
    return CHAMBER_NONE;
}

CHAMBER_NAMES
POS::scanChamber(POSLIST &exits, POSLIST &moblocs, POSLIST &itemlocs) const
{
    exits.clear();
    moblocs.clear();
    itemlocs.clear();

    CHAMBER_NAMES	cham = chamber();
    if (cham == CHAMBER_NONE)
	return cham;

    // Flood fill our meta data
    // Very bad n^2 here as we don't have simple hash tables :<
    POSLIST		visited;
    POSLIST		stack;
    stack.append(*this);
    int			seed = metaData();
    J_ASSERT(seed != -1);
    while (stack.entries())
    {
	POS	p = stack.pop();
	visited.append(p);
	if (p.isFOV() && p.mob() && !moblocs.contains(p))
	    moblocs.append(p);
	if (p.isFOV() && p.item() && !itemlocs.contains(p))
	    itemlocs.append(p);
	if (p.isMapped() && (p.tile() == TILE_DOWNSTAIRS || p.tile() == TILE_UPSTAIRS) && !exits.contains(p))
	    exits.append(p);
	for (int dir = 0; dir < 4; dir++)
	{
	    POS np = p.delta4Direction(dir);
	    if (np.metaData() == p.metaData() &&
		!visited.contains(np))	// <- Yuck!!!  Future Jeff will cry?
	    {
		stack.append(np);
	    }

	    if (np.metaData() == -1 && np.isMapped())
	    {
		// This is a boundary.  See if it is an exit.
		if (np.defn().isdoorway && !exits.contains(np))
		    exits.append(np);
		else if (np.isPassable() && !exits.contains(np))
		    exits.append(np);
	    }
	}
    }

    return cham;
}

///
/// ROOM functions
///

ROOM::ROOM()
{
    myId = -1;
    myFragment = 0;
    myType = ROOMTYPE_NONE;
    mySymbol = 'X';
    myAtlasX = myAtlasY = 0;
    mySeed = rand_int();
    myConstantFlags = 0;
    myMaxSmoke = 0;

    memset(myConnectivity, 0, sizeof(u8) * 4);
}

ROOM::ROOM(const ROOM &room)
{
    *this = room;
}

void
ROOM::save(ostream &os) const
{
    s32		val;
    u8		c;
    int			i;

    val = myId;
    os.write((const char *) &val, sizeof(s32));

    val = mySeed;
    os.write((const char *) &val, sizeof(s32));

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

    val = myDepth;
    os.write((const char *) &val, sizeof(s32));

    val = myType;
    os.write((const char *) &val, sizeof(s32));

    val = myWidth;
    os.write((const char *) &val, sizeof(s32));
    val = myHeight;
    os.write((const char *) &val, sizeof(s32));

    val = myPortals.entries();
    os.write((const char *) &val, sizeof(s32));
    for (i = 0; i < myPortals.entries(); i++)
    {
	myPortals(i).save(os);
    }

    os.write((const char *) myTiles.data(), myWidth * myHeight);
    os.write((const char *) myFlags.data(), myWidth * myHeight * sizeof(short));
    os.write((const char *) &myConstantFlags, sizeof(short));
    os.write((const char *) mySmoke.data(), myWidth * myHeight);
    os.write((const char *) myMetaData.data(), myWidth * myHeight * sizeof(int));

    os.write((const char *) myElevation.data(), myWidth * myHeight * sizeof(int));
    os.write((const char *) myWaterDepth.data(), myWidth * myHeight * sizeof(int));
    os.write((const char *) mySnowDepth.data(), myWidth * myHeight * sizeof(int));
    val = myAtlasX;
    os.write((const char *) &val, sizeof(s32));
    val = myAtlasY;
    os.write((const char *) &val, sizeof(s32));
    for (i = 0; i < 4; i++)
    {
	val = myConnectivity[i];
	os.write((const char *) &val, sizeof(s32));
    }

}

ROOM *
ROOM::load(istream &is)
{
    ROOM		*result;
    s32		val;
    u8		c;
    int			i, w, h, n;

    result = new ROOM();

    is.read((char *) &val, sizeof(s32));
    result->myId = val;

    is.read((char *) &val, sizeof(s32));
    result->mySeed = val;

    is.read((char *) &c, sizeof(u8));
    result->mySymbol = c;

    is.read((char *) &val, sizeof(s32));
    result->myDepth = val;
    is.read((char *) &val, sizeof(s32));
    result->myType = (ROOMTYPE_NAMES) val;

    is.read((char *) &val, sizeof(s32));
    w = val;
    is.read((char *) &val, sizeof(s32));
    h = val;

    result->resize(w, h);

    is.read((char *) &val, sizeof(s32));
    n = val;
    for (i = 0; i < n; i++)
    {
	result->myPortals.append(PORTAL::load(is));
    }

    is.read( (char *) result->myTiles.writeableData(), w * h);
    is.read( (char *) result->myFlags.writeableData(), w * h * sizeof(short));
    is.read( (char *) &result->myConstantFlags, sizeof(short));
    is.read( (char *) result->mySmoke.writeableData(), w * h);
    is.read( (char *) result->myMetaData.writeableData(), w * h * sizeof(int));

    is.read( (char *) result->myElevation.writeableData(), w * h * sizeof(int));
    is.read( (char *) result->myWaterDepth.writeableData(), w * h * sizeof(int));
    is.read( (char *) result->mySnowDepth.writeableData(), w * h * sizeof(int));

    is.read((char *) &val, sizeof(s32));
    result->myAtlasX = val;
    is.read((char *) &val, sizeof(s32));
    result->myAtlasY = val;

    for (i = 0; i < 4; i++)
    {
	is.read((char *) &val, sizeof(s32));
	result->myConnectivity[i] = val;
    }

    return result;
}

ROOM &
ROOM::operator=(const ROOM &room)
{
    if (this == &room)
	return *this;

    deleteContents();

    myFragment = room.myFragment;

    myTiles = room.myTiles;
    myFlags = room.myFlags;
    mySmoke = room.mySmoke;
    myMetaData = room.myMetaData;
    myElevation = room.myElevation;
    myWaterDepth = room.myWaterDepth;
    mySnowDepth = room.mySnowDepth;
    myDists = room.myDists;
    // this is suspicious, as we should have correct size
    // in source already???
    // But we don't clear on resize so escaped it unitl we had
    // myFromCanon!
    resize(room.myWidth, room.myHeight);

    myConstantFlags = room.myConstantFlags;
    myMaxSmoke = room.myMaxSmoke;

    myPortals = room.myPortals;
    myId = room.myId;
    mySeed = room.mySeed;
    myType = room.myType;
    myDepth = room.myDepth;
    mySymbol = room.mySymbol;

    myAtlasX = room.myAtlasX;
    myAtlasY = room.myAtlasY;
    memcpy(myConnectivity, room.myConnectivity, sizeof(u8) * 4 );

    // This usually requires some one to explicitly call setMap()
    myMap = 0;

    return *this;
}


ROOM::~ROOM()
{
    deleteContents();
}

void
ROOM::deleteContents()
{
    myPortals.clear();

    myTiles.reset();
    myFlags.reset();
    mySmoke.reset();
    myMetaData.reset();
    myElevation.reset();
    myWaterDepth.reset();
    mySnowDepth.reset();
    myDists.reset();
}


TILE_NAMES
ROOM::getTile(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return TILE_INVALID;

    return (TILE_NAMES) myTiles.get(x, y);
}

void
ROOM::setTile(int x, int y, TILE_NAMES tile)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());
    TILE_NAMES oldtile = getTile(x, y);
    if (tile != oldtile)
    {
	myTiles.makeUnique();
	myTiles.set(x, y, tile);
    }
}

MAPFLAG_NAMES
ROOM::getFlag(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return MAPFLAG_NONE;
    return (MAPFLAG_NAMES) myFlags.get(x, y);
}

void
ROOM::setMetaData(int x, int y, int metadata)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());

    int oldmeta = getMetaData(x, y);

    if (oldmeta != metadata)
    {
	myMetaData.makeUnique();
	myMetaData.set(x, y, metadata);
    }
}

int
ROOM::getMetaData(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return -1;
    return myMetaData.get(x, y);
}

void
ROOM::setElevation(int x, int y, int val)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());

    int oldval = getElevation(x, y);

    if (oldval != val)
    {
	myElevation.makeUnique();
	myElevation.set(x, y, val);
    }
}

int
ROOM::getElevation(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return 0;
    return myElevation.get(x, y);
}

void
ROOM::setWaterDepth(int x, int y, int val)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());

    int oldval = getWaterDepth(x, y);

    if (oldval != val)
    {
	myWaterDepth.makeUnique();
	myWaterDepth.set(x, y, val);
    }
}

int
ROOM::getWaterDepth(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return 0;
    return myWaterDepth.get(x, y);
}

void
ROOM::setSnowDepth(int x, int y, int val)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());

    int oldval = getSnowDepth(x, y);

    if (oldval != val)
    {
	mySnowDepth.makeUnique();
	mySnowDepth.set(x, y, val);
    }
}

int
ROOM::getSnowDepth(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return 0;
    return mySnowDepth.get(x, y);
}

void
ROOM::setSmoke(int x, int y, int smoke)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());
    if (smoke < 0) smoke = 0;
    if (smoke > 255) smoke = 255;

    int oldsmoke = getSmoke(x, y);

    if (oldsmoke != smoke)
    {
	mySmoke.makeUnique();
	mySmoke.set(x, y, smoke);
	myMaxSmoke = MAX(myMaxSmoke, smoke);
    }
}

int
ROOM::getSmoke(int x, int y) const
{
    if (x < 0 || x >= width() || y < 0 || y >= height())
	return 0;
    if (!myMaxSmoke)
	return 0;
    return mySmoke.get(x, y);
}

void
ROOM::simulateSmoke(int colour)
{
    int		x, y;

    // Trivial if we have no smoke
    if (!myMaxSmoke)
	return;
    const int SMOKE_FADE = 1;
    const int SMOKE_SPREAD = 5;

    // Always will change and we write directly
    mySmoke.makeUnique();

    POS		pos;

    FORALL_XY(x, y)
    {
	if ( ((x ^ y) & 1) != colour )
	    continue;

	int		smoke = mySmoke.get(x, y);
	if (!smoke)
	    continue;

	smoke -= SMOKE_FADE;
	if (smoke < 0)
	{
	    smoke = 0;
	    mySmoke.set(x,y, smoke);
	    continue;
	}
	int spread = (smoke * SMOKE_SPREAD) / 100;

	// Divide in each direction.

	int totalspread = 0;
	int		dx, dy;
	FORALL_4DIR(dx, dy)
	{
	    pos = buildPos(x, y).delta(dx, dy);
	    int		destsmoke = pos.smoke();

	    int		newdest = destsmoke + spread;
	    if (newdest > 255)
		newdest = 255;

	    // Recompute how much actually spread.
	    totalspread += newdest - destsmoke;

	    pos.setSmoke(newdest);
	}
	mySmoke.set(x, y, smoke - totalspread);
    }
}

void
ROOM::updateMaxSmoke()
{
    int		x, y;
    // Can't update.
    if (!myMaxSmoke)
	return;
    myMaxSmoke = 0;
    FORALL_XY(x, y)
    {
	int	smoke = mySmoke.get(x, y);
	myMaxSmoke = MAX(smoke, myMaxSmoke);
    }
}

void
ROOM::setFlag(int x, int y, MAPFLAG_NAMES flag, bool state)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());

    u16 oldflag = myFlags.get(x, y);
    if (state)
    {
	if ((oldflag & flag) == flag)
	{
	    // No change
	}
	else
	{
	    // Set
	    oldflag |= flag;
	    myFlags.makeUnique();
	    myFlags.set(x, y, oldflag);
	    myConstantFlags &= ~flag;
	}
    }
    else
    {
	if (oldflag & flag)
	{
	    // Clear
	    oldflag &= ~flag;
	    myFlags.makeUnique();
	    myFlags.set(x, y, oldflag);
	    myConstantFlags &= ~flag;
	}
	else
	{
	    // No change.
	}
    }
}

void
ROOM::setAllFlags(MAPFLAG_NAMES flag, bool state)
{
    if (myConstantFlags & flag)
    {
	setFlag(0, 0, flag, state);

	// If not cleared out on set, no one will clear it!
	if (myConstantFlags & flag)
	    return;
    }

    for (int y = 0; y < height(); y++)
	for (int x = 0; x < width(); x++)
	    setFlag(x, y, flag, state);
    // At this point we are constant!
    myConstantFlags |= flag;
}

void
ROOM::mergeFlags(MAPFLAG_NAMES flag, ROOM *src)
{
    if (!src) return;
    J_ASSERT(src->width() == width());
    J_ASSERT(src->height() == height());
    if (width() != src->width()) return;
    if (height() != src->height()) return;

    // If the source has a constant flag, this is either to set all
    // or be no-op.
    if (src->myConstantFlags & flag)
    {
	bool	state = (src->getFlag(0, 0) & flag) ? true : false;

	if (state)
	    setAllFlags(flag, true);
	return;
    }

    // if we are a constant flag, and are set, there is nothing to do.
    if (myConstantFlags & flag)
    {
	if (getFlag(0, 0) & flag)
	    return;
    }

    // Okay, do the or!
    int		x, y;
    bool	allset = true, noneset = true;
    FORALL_XY(x, y)
    {
	if (src->getFlag(x, y) & flag)
	{
	    setFlag(x, y, flag, true);
	    noneset = false;
	}
	else
	{
	    if (getFlag(x, y) & flag)
		noneset = false;
	    else
		allset = false;
	}
    }
    // We might now be constant.
    if (allset || noneset)
	myConstantFlags |= flag;
}

int
ROOM::getDistance(int mapnum, int x, int y) const
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());
    return myDists.get(x, y + mapnum * height());
}

void
ROOM::setDistance(int mapnum, int x, int y, int dist)
{
    J_ASSERT(x >= 0 && x < width() && y >= 0 && y < height());
    myDists.makeUnique();
    myDists.set(x, y + mapnum * height(), dist);
}

void
ROOM::clearDistMap(int mapnum)
{
    myDists.makeUnique();
    memset(&myDists.writeableData()[mapnum * width() * height()], 0xff, sizeof(int) * width() * height());
}

void
ROOM::setMap(MAP *map)
{
    int			i;

    myMap = map;

    for (i = 0; i < myPortals.entries(); i++)
    {
	PORTAL	p = myPortals(i);
	p.mySrc.setMap(map);
	p.myDst.setMap(map);
	myPortals.set(i, p);
    }
}

void
ROOM::resize(int w, int h)
{
    myWidth = w;
    myHeight = h;

    myTiles.resize(myWidth, myHeight);
    myFlags.resize(myWidth, myHeight);
    mySmoke.resize(myWidth, myHeight);
    myMetaData.resize(myWidth, myHeight);
    myElevation.resize(myWidth, myHeight);
    myWaterDepth.resize(myWidth, myHeight);
    mySnowDepth.resize(myWidth, myHeight);

    // We are now constant!
    myConstantFlags = 0xffff;

    // If we have distances, we'd want this.
    myDists.resize(myWidth, myHeight * DISTMAP_CACHE);
}

void
ROOM::rotateEntireRoom(int angle)
{
    // Ensure we are odd!
    J_ASSERT(width() & 1);
    J_ASSERT(height() & 1);
    J_ASSERT(width() == height());

    int		cx = width()/2;
    int		cy = height()/2;

    TEXTURE8 	newtiles(height(), width());
    TEXTURE16 	newflags(height(), width());
    TEXTUREI	newdists(height(), width() * DISTMAP_CACHE);

    // Flop everything.
    for (int y = 0; y < height(); y++)
    {
	for (int x = 0; x < width(); x++)
	{
	    int		nx, ny;
	    nx = x;
	    ny = y;
	    nx -= cx;
	    ny -= cy;
	    POS::rotateByAngle(angle, nx, ny);
	    nx += cx;
	    ny += cy;

	    newtiles.set(nx, ny, getTile(x, y));
	    newflags.set(nx, ny, getFlag(x, y));
	    for (int m = 0; m < DISTMAP_CACHE; m++)
	    {
		newdists.set(nx, ny + m * width(), 
			     myDists.get(x, y + m * height()));
	    }
	}
    }

    myDists = newdists;
    myTiles = newtiles;
    myFlags = newflags;
    // Note constant flags remain invariant under rotation.

    // TODO: Possibly rotate our width/ehgiht!

    // TODO: Portals are orphaned!

    // Cannot rotate mobs as they are globally registered!
    J_ASSERT(false);

#if 0
    for (int i = 0; i < myItems.entries(); i++)
    {
	POS		p = myItems(i)->pos();

	int	nx, ny;
	nx = p.myX;
	ny = p.myY;
	nx -= cx;
	ny -= cy;
	POS::rotateByAngle(angle, nx, ny);
	nx += cx;
	ny += cy;

	p = buildPos(nx, ny);
	myItems(i)->silentMove(p);
    }

    for (int i = 0; i < myMobs.entries(); i++)
    {
	POS		p = myMobs(i)->pos();

	int	nx, ny;
	nx = p.myX;
	ny = p.myY;
	nx -= cx;
	ny -= cy;
	POS::rotateByAngle(angle, nx, ny);
	nx += cx;
	ny += cy;

	POS		np = buildPos(nx, ny);
	np.setAngle(p.angle() + angle);

	myMobs(i)->silentMove(np);
    }
#endif
}

POS
ROOM::buildPos(int x, int y) const
{
    POS		p;

    p.myMap = myMap;
    p.myRoomId = myId;
    p.myX = x;
    p.myY = y;
    p.myAngle = 0;

    return p;
}

POS
ROOM::getRandomPos(POS p, int *n) const
{
    POS		np;
    int		x, y;
    int		ln = 1;

    if (!n)
	n = &ln;

    np.myMap = myMap;
    np.myRoomId = myId;
    np.myAngle = rand_choice(4);

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    np.myX = x; np.myY = y;

	    if (np.isPassable() && !np.mob())
	    {
		if (!rand_choice(*n))
		{
		    p = np;
		}
		// I hate ++ on * :>
		*n += 1;
	    }
	}
    }

    return p;
}

POS
ROOM::getRandomTile(TILE_NAMES tile, POS p, int *n) const
{
    POS		np;
    int		x, y;
    int		ln = 1;

    if (!n)
	n = &ln;

    np.myMap = myMap;
    np.myRoomId = myId;
    np.myAngle = rand_choice(4);

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    np.myX = x; np.myY = y;

	    if (np.tile() == tile)
	    {
		if (!rand_choice(*n))
		{
		    p = np;
		}
		// I hate ++ on * :>
		*n += 1;
	    }
	}
    }

    return p;
}

POS
ROOM::findCloseUnexplored(POS pos) const
{
    POS		np;
    int		x, y;
    int		n = 0;
    int		bestdist = 1000000;
    POS		result;

    const int	disthystersis = 4;
    const int	closechance = 1 << disthystersis;

    np.myMap = myMap;
    np.myRoomId = myId;
    np.myAngle = rand_choice(4);

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    np.myX = x; np.myY = y;

	    // Don't try to explore to impassable lcoations:
	    if (!np.isPassable())
		continue;
	    // No point if it is mapped
	    if (np.isMapped())
		continue;
	    // Make sure it is adjacent to a mapped!
	    bool nextmapped = false;
	    for (int i = 0; i < 4; i++)
	    {
		if (np.delta4Direction(i).isMapped())
		{
		    nextmapped = true;
		    break;
		}
	    }

	    if (!nextmapped)
		continue;

	    // We don't want to always pick best distance, but
	    // don't know what best is.  By having an arithmetic falloff
	    // we can retroactively update our probabilities...
	    int	dist = np.dist(pos);
	    if (dist < bestdist)
	    {
		// New best dist...
		if (bestdist - dist > disthystersis)
		    n = 0;
		else
		{
		    while (bestdist-- > dist)
		    {
			n /= 2;
		    }
		}
		bestdist = dist;
		n += closechance;
		if (rand_choice(n) < closechance)
		    result = np;
	    }
	    else if (dist == bestdist)
	    {
		n += closechance;
		if (rand_choice(n) < closechance)
		    result = np;
	    }
	    else if (dist < bestdist + disthystersis)
	    {
		int chance = closechance;
		chance >>= disthystersis - (dist - bestdist);
		n += chance;
		if (rand_choice(n) < chance)
		    result = np;
	    }
	}
    }

    return result;
}

int
ROOM::getAllTiles(TILE_NAMES tile, POSLIST &list) const
{
    eachPos([&](POS np)
    {
	if (np.tile() == tile)
	{
	    list.append(np);
	}
    });

    return list.entries();
}

int
ROOM::stairDistance() const
{
    POS		down = getRandomTile(TILE_DOWNSTAIRS, POS(), nullptr);
    if (!down.valid())
	down = getRandomTile(TILE_MEDITATIONSPOT, POS(), nullptr);
    if (!down.valid())
	return 0;

    POS		up = getRandomTile(TILE_UPSTAIRS, POS(), nullptr);
    if (!up.valid())
	return 0;

    int		distmap = down.map()->buildDistMap(down);

    return up.getDistance(distmap);
}


int
ROOM::getVisibleExits(POSLIST &list) const
{
    int		ln = 1;

    eachPos([&](POS np)
    {
	if (!np.isFOV())
	    return;

	if (np.defn().isdoorway)
	{
	    list.append(np);
	}
	else if (np.tile() == TILE_DOWNSTAIRS ||
		 np.tile() == TILE_UPSTAIRS)
	{
	    list.append(np);
	}
    });

    return list.entries();
}

int
ROOM::getConnectedRooms(ROOMLIST &list) const
{
    for (int i = 0; i < myPortals.entries(); i++)
    {
	ROOM	*dst = myPortals(i).myDst.room();

	if (list.find(dst) < 0)
	{
	    list.append(dst);
	}
    }

    return list.entries();
}

PORTAL *
ROOM::getPortal(int x, int y) const
{
    int			i;

    // Quick test.
    if (!(getFlag(x, y) & MAPFLAG_PORTAL))
	return 0;

    for (i = 0; i < myPortals.entries(); i++)
    {
	if (myPortals(i).mySrc.myX == x && myPortals(i).mySrc.myY == y)
	{
	    return myPortals.rawptr(i);
	}
    }

    return 0;
}

void
ROOM::removePortal(POS dst)
{
    int			i;

    for (i = 0; i < myPortals.entries(); i++)
    {
	if (myPortals(i).myDst == dst)
	{
	    myPortals(i).mySrc.setFlag(MAPFLAG_PORTAL, false);
	    myPortals(i).myDst.setFlag(MAPFLAG_PORTAL, false);
	    myPortals.removeAt(i);
	    i--;
	}
    }
}

void
ROOM::removeAllForeignPortals()
{
    int			i;

    for (i = 0; i < myPortals.entries(); i++)
    {
	if (myPortals(i).myDst.roomType() != type())
	{
	    myPortals(i).mySrc.setFlag(MAPFLAG_PORTAL, false);
	    myPortals(i).myDst.setFlag(MAPFLAG_PORTAL, false);
	    myPortals.removeAt(i);
	    i--;
	}
    }
}

void
ROOM::removeAllPortalsTo(ROOMTYPE_NAMES roomtype)
{
    int			i;

    for (i = 0; i < myPortals.entries(); i++)
    {
	if (myPortals(i).myDst.roomType() == roomtype)
	{
	    myPortals(i).mySrc.setFlag(MAPFLAG_PORTAL, false);
	    myPortals(i).myDst.setFlag(MAPFLAG_PORTAL, false);
	    myPortals.removeAt(i);
	    i--;
	}
    }
}

void
ROOM::removeAllPortals()
{
    myPortals.clear();
}

POS
ROOM::findProtoPortal(int dir)
{
    int		dx, dy, x, y, nfound;
    POS	result;

    rand_getdirection(dir, dx, dy);

    nfound = 0;

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    if (getTile(x, y) == TILE_PROTOPORTAL ||
	        getTile(x, y) == TILE_MOUNTAINPROTOPORTAL)
	    {
		if ((getTile(x+dx, y+dy) == TILE_INVALID)
			&&
		    ( getTile(x-dx, y-dy) == TILE_FLOOR ||
		      getTile(x-dx, y-dy) == TILE_ALTAR ||
		      getTile(x-dx, y-dy) == TILE_GRASS
		     ))
		{
		    nfound++;
		    if (!rand_choice(nfound))
		    {
			result = buildPos(x, y);
		    }
		}
	    }
	}
    }

    if (nfound)
    {
    }
    else
    {
	{
	    cerr << "Failed to locate on map " << "Damn so that is what my fragment was used for!!!" << endl;
	    if (myFragment)
	    {
		if (myFragment->myFName.buffer())
		    cerr << myFragment->myFName.buffer() << endl;
		cerr << "Direction was " << dir << " type is " << glb_roomtypedefs[myType].prefix << endl;
	    }
	}
    }
    return result;
}

void
ROOM::revertToProtoPortal()
{
    int		x, y;

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    if (getFlag(x, y) & MAPFLAG_PORTAL)
	    {
		if (getTile(x, y) == TILE_FLOOR)
		    setTile(x, y, TILE_PROTOPORTAL);
		else if (getTile(x, y) == TILE_SOLIDWALL)
		    setTile(x, y, TILE_INVALID);
		setFlag(x, y, MAPFLAG_PORTAL, false);
	    }
	}
    }
    // Clear our portal list now.
    myPortals.clear();
}

POS
ROOM::findUserProtoPortal(int dir)
{
    int		dx, dy, x, y, nfound;
    POS	result;

    rand_getdirection(dir, dx, dy);

    nfound = 0;

    for (y = 0; y < height(); y++)
    {
	for (x = 0; x < width(); x++)
	{
	    if (getTile(x, y) == TILE_USERPROTOPORTAL)
	    {
		if (( getTile(x+dx, y+dy) == TILE_INVALID ||
		      getTile(x+dx, y+dy) == defn().wall_tile ||
		      getTile(x+dx, y+dy) == defn().tunnelwall_tile
		     )
		     &&
		    ( getTile(x-dx, y-dy) == TILE_FLOOR ||
		      getTile(x-dx, y-dy) == TILE_GRASS ||
		      getTile(x-dx, y-dy) == TILE_FOREST
		     ))
		{
		    nfound++;
		    if (!rand_choice(nfound))
		    {
			result = buildPos(x, y);
		    }
		}
	    }
	}
    }

    if (nfound)
    {
    }
    else
    {
	{
	    cerr << "Failed to locate on map " << "Damn so that is what my fragment was used for!!!" << endl;
	    if (myFragment)
	    {
		cerr << myFragment->myFName.buffer() << endl;
		cerr << "Direction was " << dir << " type is " << glb_roomtypedefs[myType].prefix << endl;
	    }
	}
    }
    return result;
}

POS
ROOM::findCentralPos() const
{
    POS		center;

    center = buildPos(width()/2, height()/2);
    return spiralSearch(center, 0, false);
}

#define TEST_TILE(x, y) \
if ( POS::defn(getTile(x, y)).ispassable && (!avoidmob || !buildPos(x, y).mob()) )\
    return buildPos(x, y);

POS
ROOM::spiralSearch(POS start, int startradius, bool avoidmob) const
{
    // Spiral search for passable tile from the center.
    int		x = start.myX, y = start.myY;
    int		ir, r, maxr = MAX(width(), height())+1;
    POS		result;

    for (r = startradius; r < maxr; r++)
    {
	for (ir = -r; ir <= r; ir++)
	{
	    TEST_TILE(x+ir, y+r)
	    TEST_TILE(x+ir, y-r)
	    TEST_TILE(x+r, y+ir)
	    TEST_TILE(x-r, y+ir)
	}
    }

    J_ASSERT(!"No valid squares in a room.");
    return result;
}
#undef TEST_TILE

// Applies spiral search with op(IVEC2 p) -> bool
template <typename OP>
IVEC2
room_spiralSearch(IVEC2 start, IVEC2 bounds, int startradius, int maxradius, const OP &op)
{
    // Spiral search for passable tile from the center.
    int		x = start.x(), y = start.y();
    int		ir, r, maxr = MIN(maxradius, MAX(bounds.x(), bounds.y())+1);
    IVEC2       result(-1, -1), p;

    auto in_room = [&](IVEC2 xy) -> bool
    {
        if (xy.x() < 0 || xy.y() < 0) return false;
        if (xy.x() >= bounds.x()) return false;
        if (xy.y() >= bounds.y()) return false;
        return true;
    };

    for (r = startradius; r < maxr; r++)
    {
	int		nvalid = 0;
	if (r == 0)
	{
	    p = IVEC2(x, y);
	    if (in_room(p) && op(p)) { return p; }
	}
	else
	{
	    // This is a one-sided interval to ensure corners are
	    // tested only once.
	    for (ir = -r; ir < r; ir++)
	    {
		// y+r search does not include x+r
		p = IVEC2(x+ir, y+r);
		if (in_room(p) && op(p)) 
		{ nvalid++; if (!rand_choice(nvalid)) result = p; }
		// y-r search does not include x-r
		p = IVEC2(x-ir, y-r);
		if (in_room(p) && op(p)) 
		{ nvalid++; if (!rand_choice(nvalid)) result = p; }
		// x+r search does not include y-r
		p = IVEC2(x+r, y-ir);
		if (in_room(p) && op(p)) 
		{ nvalid++; if (!rand_choice(nvalid)) result = p; }
		// x-r search does not include y+r
		p = IVEC2(x-r, y+ir);
		if (in_room(p) && op(p)) 
		{ nvalid++; if (!rand_choice(nvalid)) result = p; }
	    }
	    if (nvalid)
		return result;
	}
    }
    return result;
}

template <typename OP>
POS
ROOM::spiralSearch(POS start, int startradius, int maxradius, const OP &op) const
{
    auto xy_test = [&](IVEC2 xy) -> bool
    {
        POS p = buildPos(xy);
        return op(p);
    };

    IVEC2 result = room_spiralSearch(IVEC2(start.myX, start.myY), IVEC2(width(), height()), startradius, maxradius, xy_test);

    if (xyInRoom(result))
        return buildPos(result);
    return POS();
}

bool
ROOM::buildPortal(POS a, int dira, POS b, int dirb, bool settiles)
{
    PORTAL		 patob, pbtoa;

    if (!a.valid() || !b.valid())
	return false;

    patob.mySrc = a.delta4Direction(dira);
    patob.myDst = b;

    pbtoa.mySrc = b.delta4Direction(dirb);
    pbtoa.myDst = a;

    if (settiles)
    {
	patob.mySrc.setTile(TILE_SOLIDWALL);
	pbtoa.mySrc.setTile(TILE_SOLIDWALL);
    }

    patob.mySrc.myAngle = 0;
    pbtoa.mySrc.myAngle = 0;

    if (settiles)
    {
	patob.myDst.setTile(TILE_FLOOR);
	pbtoa.myDst.setTile(TILE_FLOOR);
    }
    patob.myDst.setFlag(MAPFLAG_PORTAL, true);
    patob.mySrc.setFlag(MAPFLAG_PORTAL, true);
    pbtoa.myDst.setFlag(MAPFLAG_PORTAL, true);
    pbtoa.mySrc.setFlag(MAPFLAG_PORTAL, true);

    patob.myDst.myAngle = (dira - dirb + 2) & 3;
    pbtoa.myDst.myAngle = (dirb - dira + 2) & 3;

    a.room()->myPortals.append(patob);
    b.room()->myPortals.append(pbtoa);

    return true;
}



bool
ROOM::link(ROOM *a, int dira, ROOM *b, int dirb)
{
    POS		aseed, bseed;
    POS		an, bn;

    aseed = a->findProtoPortal(dira);
    bseed = b->findProtoPortal(dirb);

    if (!aseed.valid() || !bseed.valid())
	return false;

    // This code is to ensure we do as thick a match
    // as possible.
#if 1
    // Grow to the top.
    an = aseed;
    while (an.tile() == TILE_PROTOPORTAL)
    {
	aseed = an;
	an = an.delta4Direction(dira+1);
    }
    bn = bseed;
    while (bn.tile() == TILE_PROTOPORTAL)
    {
	bseed = bn;
	bn = bn.delta4Direction(dirb-1);
    }

#if 1
    int asize = 0, bsize = 0;

    an = aseed;
    while (an.tile() == TILE_PROTOPORTAL)
    {
	asize++;
	an = an.delta4Direction(dira-1);
    }
    bn = bseed;
    while (bn.tile() == TILE_PROTOPORTAL)
    {
	bsize++;
	bn = bn.delta4Direction(dirb+1);
    }

    if (asize > bsize)
    {
	int		off = rand_choice(asize - bsize);

	while (off)
	{
	    off--;
	    aseed = aseed.delta4Direction(dira-1);
	    J_ASSERT(aseed.tile() == TILE_PROTOPORTAL);
	}
    }
    else if (bsize > asize)
    {
	int		off = rand_choice(bsize - asize);

	while (off)
	{
	    off--;
	    bseed = bseed.delta4Direction(dirb+1);
	    J_ASSERT(bseed.tile() == TILE_PROTOPORTAL);
	}
    }
#endif
#endif

    ROOM::buildPortal(aseed, dira, bseed, dirb);

    // Grow our portal in both directions as far as it will go.
    an = aseed.delta4Direction(dira+1);
    bn = bseed.delta4Direction(dirb-1);
    while (an.tile() == TILE_PROTOPORTAL && bn.tile() == TILE_PROTOPORTAL)
    {
	ROOM::buildPortal(an, dira, bn, dirb);
	an = an.delta4Direction(dira+1);
	bn = bn.delta4Direction(dirb-1);
    }

    an = aseed.delta4Direction(dira-1);
    bn = bseed.delta4Direction(dirb+1);
    while (an.tile() == TILE_PROTOPORTAL && bn.tile() == TILE_PROTOPORTAL)
    {
	ROOM::buildPortal(an, dira, bn, dirb);
	an = an.delta4Direction(dira-1);
	bn = bn.delta4Direction(dirb+1);
    }

    return true;
}

bool
ROOM::forceConnectNeighbours()
{
    bool		interesting = false;

    int		dx, dy;
    FORALL_4DIR(dx, dy)
    {
	if (myConnectivity[lcl_angle] == ROOM::CONNECTED)
	{
	    continue;
	}
	if (myConnectivity[lcl_angle] == ROOM::UNDECIDED)
	{
	    // We are doing pure implicit connections here.
	    myConnectivity[lcl_angle] = ROOM::IMPLICIT;
	}

	if (myConnectivity[lcl_angle] == ROOM::DISCONNECTED)
	{
	    continue;
	}
    }

    map()->expandActiveRooms(getId(), 2);

    return interesting;
}

bool	
ROOM::isLit() const
{
    // The deep dungeon is lit as this generally makes things
    // harder, and also avoids mucking with torches that far.
    if (getDepth() > 10) return true;
    if (getDepth() > 0) return false;
    // See by any daylight...
    if (map()) return map()->getDaylight()>0;
    // No map is visible??
    return true;
}

///
/// FRAGMENT definition and functions
///


FRAGMENT::FRAGMENT(const char *fname)
{
    ifstream		is(fname);
    char		line[500];
    PTRLIST<char *>	l;
    int			i, j;

    myFName.strcpy(fname);

    while (is.getline(line, 500))
    {
	text_striplf(line);

	// Ignore blank lines...
	if (line[0])
	    l.append(glb_strdup(line));
    }

    if (l.entries() < 1)
    {
	cerr << "Empty map fragment " << fname << endl;
	exit(-1);
    }

    // Find smallest non-whitespace square.
    int			minx, maxx, miny, maxy;

    for (miny = 0; miny < l.entries(); miny++)
    {
	if (text_hasnonws(l(miny)))
	    break;
    }

    for (maxy = l.entries(); maxy --> 0; )
    {
	if (text_hasnonws(l(maxy)))
	    break;
    }

    if (miny > maxy)
    {
	cerr << "Blank map fragment " << fname << endl;
	exit(-1);
    }

    minx = MYstrlen(l(miny));
    maxx = 0;
    for (i = miny; i <= maxy; i++)
    {
	minx = MIN(minx, text_firstnonws(l(i)));
	maxx = MAX(maxx, text_lastnonws(l(i)));
    }

    // Joys of rectangles with inconsistent exclusivity!
    // pad everything by 1.
    myW = maxx - minx + 1 + 2;
    myH = maxy - miny + 1 + 2;

    myTiles = new u8 [myW * myH + 1];
    myTiles[myW * myH] = 0;

    memset(myTiles, ' ', myW * myH);

    for (i = 1; i < myH-1; i++)
    {
	if (MYstrlen(l(i+miny-1)) < myW + minx - 2)
	{
	    cerr << "Short line in fragment " << fname << endl;
	    exit(-1);
	}
	for (j = 1; j < myW-1; j++)
	{
	    myTiles[i * myW + j] = l(i+miny-1)[j+minx-1];
	}
    }

    for (i = 0; i < l.entries(); i++)
	free(l(i));
}

FRAGMENT::FRAGMENT(int w, int h)
{
    myW = w;
    myH = h;
    myTiles = new u8 [myW * myH + 1];
    myTiles[myW*myH] = 0;

    memset(myTiles, ' ', myW * myH);
}

FRAGMENT::~FRAGMENT()
{
    delete [] myTiles;
}

void
FRAGMENT::swizzlePos(int &x, int &y, int orient) const
{
    if (orient & ORIENT_FLIPX)
    {
	x = myW - x - 1;
    }

    if (orient & ORIENT_FLIPY)
    {
	y = myH - y - 1;
    }

    if (orient & ORIENT_FLOP)
    {
	int	t = y;
	y = x;
	x = t;
    }
}

ROOM *
FRAGMENT::buildOvermap(MAP *map, ROOMTYPE_NAMES type) const
{
    // In un-flopped coords
    GRIDI	metadata(myW, myH);
    metadata.constant(-1);
    GRIDI	elevation(myW, myH);
    elevation.constant(0);
    GRIDI	waterdepth(myW, myH);
    waterdepth.constant(0);
    GRIDI	snowdepth(myW, myH);
    snowdepth.constant(0);
    GRID8	tiles(myW, myH);

    int goaltiles = -1;
    int depth = 0;
    BUILDER		builder(tiles.dataNC(), &metadata, &elevation, &waterdepth, &snowdepth, myW, myH, myW, depth);
    builder.setGoalTiles(goaltiles);
    builder.build(type);

    // Now slice...
    int		slicew = myW / 100;
    int		sliceh = myH / 100;

    // 50 % for the overmap...
    glbMapBuild = 0;

    const FRAGMENT *fragslice = MAP::randomFragment(ROOMTYPE_PREBUILTSLICE);
    for (int sy = 0; sy < sliceh; sy++)
    {
	for (int sx = 0; sx < slicew; sx++)
	{
	    ROOM *room = fragslice->buildRoom(map, ROOMTYPE_PREBUILTSLICE,
				    0, 0,
				    0, 0,
				    sx, sy,
				    &tiles, &metadata, 
				    &elevation, &waterdepth, &snowdepth);
	    // Build torus:
	    for (int angle = 0; angle < 4; angle++)
		room->myConnectivity[angle] = ROOM::IMPLICIT;

	    glbMapBuild = 50*(sy*slicew+sx) / (slicew*sliceh);
	}
    }

    // Pick random starting slice that has a grass tile...
    int	 n = 0;
    POS  p = POS();
#if STARTSTAIRS
    p = map->getRandomTile(TILE_DOWNSTAIRS);
    if (p.room())
	return p.room();
#endif
    for (int sy = 0; sy < sliceh; sy++)
    {
	for (int sx = 0; sx < slicew; sx++)
	{
	    ROOM *room = map->findAtlasRoom(sx, sy);
	    p = room->getRandomTile(TILE_GRASS, p, &n);
	}
    }
    if (p.room())
	return p.room();
    // Failed!
    J_ASSERT(!"No upstairs!");
    return nullptr;
}

ROOM *
FRAGMENT::buildRoom(MAP *map, ROOMTYPE_NAMES type, 
	int upstair, int downstair,
	int difficulty, int depth, 
	int atlasx, int atlasy,
	const GRID8 *srctiles, const GRIDI *srcmetadata,
	const GRIDI *srcelevation, 
	const GRIDI *srcwaterdepth, const GRIDI *srcsnowdepth
	) const
{
    ROOM	*room;
    int		 x, y, rx, ry;
    MOB		*mob;
    ITEM	*item;
    TILE_NAMES	 tile;
    MAPFLAG_NAMES flag;
    int		 orient = 0;
    int		 roomshiftx = 0;
    int		 roomshifty = 0;
    int		 goaltiles = -1;
    const u8	*localtiles;

    localtiles = myTiles;

    if (glb_roomtypedefs[type].randomorient)
	orient = rand_choice(7);

    room = new ROOM();
    room->myFragment = this;

    if (orient & ORIENT_FLOP)
	room->resize(myH, myW);
    else
	room->resize(myW, myH);

    GRID8	xlatetiles(myW, myH);

    // IN un-flopped coords
    GRIDI	metadata(myW, myH);
    metadata.constant(-1);

    GRIDI	elevation(myW, myH);
    elevation.constant(0);
    GRIDI	waterdepth(myW, myH);
    waterdepth.constant(0);
    GRIDI	snowdepth(myW, myH);
    snowdepth.constant(0);

    room->setMap(map);
    room->myType = type;
    room->myDepth = depth;
    room->myAtlasX = atlasx;
    room->myAtlasY = atlasy;

    for (int angle = 0; angle < 4; angle++)
    {
	if (glb_roomtypedefs[type].deadend)
	    room->myConnectivity[angle] = ROOM::DISCONNECTED;
	else
	    room->myConnectivity[angle] = ROOM::UNDECIDED;
    }

    const bool isslice = type == ROOMTYPE_PREBUILTSLICE;

    if (type == ROOMTYPE_PREBUILTSLICE)
    {
	for (int y = 0; y < myH; y++)
	{
	    for (int x = 0; x < myW; x++)
	    {
		xlatetiles.set(x, y, srctiles->get(atlasx*100+x, atlasy*100+y));
		metadata.set(x, y, srcmetadata->get(atlasx*100+x, atlasy*100+y));
		elevation.set(x, y, srcelevation->get(atlasx*100+x, atlasy*100+y));
		waterdepth.set(x, y, srcwaterdepth->get(atlasx*100+x, atlasy*100+y));
		snowdepth.set(x, y, srcsnowdepth->get(atlasx*100+x, atlasy*100+y));
	    }
	}
	localtiles = xlatetiles.data();
    }
    // If this is a generator, build a new room!
    else if (glb_roomtypedefs[type].usegenerator)
    {
	BUILDER		builder(myTiles, &metadata, &elevation, &waterdepth, &snowdepth, myW, myH, myW, depth);
	builder.setGoalTiles(goaltiles);
	builder.setDownStairs(downstair);
	builder.setUpStairs(upstair);

	if (type == ROOMTYPE_WILDERNESS)
	{
	    TERRAIN_NAMES		terrain[9];
	    for (int i = 0; i < 9; i++)
		terrain[i] = TERRAIN_PLAINS;
	    builder.buildWilderness(terrain);
	}
	else
	{
	    builder.build(type);
	}
	if (glb_roomtypedefs[type].shrinktofit)
	{
	    RCT	rect = builder.boundingBox([tiles = myTiles, width = myW](IVEC2 p) -> bool
	    {
		u8		tile = tiles[p.x() + p.y()*width];
		if (tile == '#')
		    return false;
		if (tile == ' ')
		    return false;
		if (tile == '^')
		    return false;
		return true;
	    });
	    // Grow by one for a boundary of #
	    rect.inset(-1);
	    // Intersect with bounds
	    rect.doIsect(builder.bounds());
	    if (orient & ORIENT_FLOP)
		room->resize(rect.h, rect.w);
	    else
		room->resize(rect.w, rect.h);
	    roomshiftx = -rect.x;
	    roomshifty = -rect.y;
	}
    }

    room->myId = map->myRooms.entries();
    map->myRooms.append(room);

    int		ngold = 0;
    int		nitem = 0;
    int		nmob = 0;
    
    for (y = 0; y < myH; y++)
    {
	for (x = 0; x < myW; x++)
	{
	    rx = x + roomshiftx;
	    ry = y + roomshifty;
	    swizzlePos(rx, ry, orient);

	    // Ensure we are in bounds, we might be out of due
	    // to shrink to fit.
	    if (rx < 0 || rx >= room->width()) continue;
	    if (ry < 0 || ry >= room->height()) continue;

	    int metaval = metadata(x, y);
	    room->setMetaData(rx, ry, metaval);
	    {
		int val = waterdepth(x, y);
		room->setWaterDepth(rx, ry, val);
		val = snowdepth(x, y);
		room->setSnowDepth(rx, ry, val);
		val = elevation(x, y);
		room->setElevation(rx, ry, val);
	    }

	    tile = TILE_INVALID;
	    flag = MAPFLAG_NONE;
	    TILE_NAMES floortile = glb_roomtypedefs[type].floor_tile;
	    // Swap the floor tile if there are , and no . beside us...
	    if (x && x < myW-1 && y && y < myH-1)
	    {
		bool	anycomma = false;
		bool	anydot = false;
		int	dx, dy;
		FORALL_4DIR(dx, dy)
		{
		    if (localtiles[x+dx+(y+dy)*myW] == '.')
			anydot = true;
		    if (localtiles[x+dx+(y+dy)*myW] == ',')
			anycomma = true;
		}
		if (anycomma && !anydot)
		    floortile = glb_roomtypedefs[type].path_tile;
	    }

	    switch (localtiles[x + y * myW])
	    {
		case 'A':		// any monster
		{
		    int		threat = difficulty;
		    mob = MOB::createNPC(map, threat);
		    if (mob)
		    {
			mob->move(room->buildPos(rx, ry));
			// Chance for sleeping..
			if (rand_chance(50))
                            mob->putToSleep(false, 0, -1, true);
		 
		    }
		    tile = floortile;
		    break;
		}

		case 'B':		// The BOSS!
		    //mob = nullptr;
		    mob = MOB::create(map, MOB::getBossName(), true);
		    if (mob)
		    {
			mob->move(room->buildPos(rx, ry));

			ITEM		*prize;
			prize = ITEM::createMacGuffin(map);
			if (prize)
			    mob->addItem(prize);

			mob->gainExp(LEVEL::expFromLevel(difficulty), /*silent=*/true);
		    }
		    tile = floortile;
		    break;

		case '(':
		    item = ITEM::createRandomBalanced(map, difficulty);
		    if (item)
			item->move(room->buildPos(rx, ry));
		    nitem++;
		    tile = floortile;
		    break;

		case 'S':
		    tile = TILE_SECRETDOOR;
		    break;

		case '%':
		{
		    ITEM_NAMES		food = ITEM::chooseItemClass(map, ITEMCLASS_FOOD, difficulty);
		    if (food != ITEM_NONE)
		    {
			item = ITEM::create(map, food);
			if (item)
			{
			    if (item)
				item->move(room->buildPos(rx, ry));
			    nitem++;
			}
		    }
		    tile = floortile;
		    break;
		}

		case 'Q':
		    tile = TILE_MEDITATIONSPOT;
		    break;

		case '1':
		    item = ITEM::create(map, ITEM_MIDAS_WAND);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;
		case '2':
		    item = ITEM::create(map, ITEM_STEEL_CLOAK);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;
		case '3':
		    item = ITEM::create(map, ITEM_KNOWLEDGE_ORB);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;

		case '$':
		    item = ITEM::create(map, ITEM_COIN);
		    item->setMaterial(MATERIAL_GOLD);
		    // average 13, median 15!
		    item->setStackCount(rand_roll(15+difficulty, 1));
		    item->move(room->buildPos(rx, ry));
		    ngold++;
		    tile = floortile;
		    break;

		case ';':
		{
		    tile = TILE_SNOWYPATH;
		    break;
		}

		case '.':
		    tile = floortile;
		    break;

		case '\'':
		    tile = TILE_GRASS;
		    break;

		case 'x':
		{
		    switch (rand_choice(15))
		    {
			case 0:
			case 1:
			case 2:
			case 3:
			    tile = TILE_DOOR_OPEN;
			    break;
			case 4:
			case 5:
			case 6:
			case 7:
			case 8:
			    tile = TILE_DOOR_CLOSED;
			    break;
			case 9:
			case 10:
			    tile = TILE_DOOR_LOCKED_UNKNOWN;
			    break;
			case 11:
			    tile = TILE_DOOR_JAMMED_UNKNOWN;
			    break;
			case 12:
			    tile = TILE_DOOR_BROKEN;
			case 13:
			case 14:
			    tile = TILE_DOOR_EMPTY;
			    break;
			default:
			    J_ASSERT(!"I can't count");
			    break;
		    }
		    break;
		}

		case 'K':
		    item = ITEM::create(map, ITEM_STATUE, depth);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;

		case '_':
		    tile = TILE_BARESTONE;
		    break;

		case '?':
		    item = ITEM::create(map, ITEM_SHELF, depth);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;

		case '#':
		    tile = glb_roomtypedefs[type].wall_tile;
		    break;

		case 'F':
		    tile = TILE_FUTURE_FORGE;
		    break;

		case '&':
		    tile = TILE_FOREST;
		    if (metaval >= 0 && metaval < NUM_TREES)
		    {
			tile = glb_treedefs[metaval].tile;
		    }
		    break;

		case '*':
		    tile = TILE_BUSH;
		    break;

		case 'O':
		    item = ITEM::create(map, ITEM_TABLE, depth);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;

		case 'h':
		    item = ITEM::create(map, ITEM_CHAIR, depth);
		    item->move(room->buildPos(rx, ry));
		    tile = floortile;
		    break;

		case '+':
		    if (type == ROOMTYPE_VILLAGE)
			tile = TILE_MOUNTAINPROTOPORTAL;
		    else
			tile = TILE_PROTOPORTAL;
		    break;

		case '>':
		    if (isslice)
		    {
			J_ASSERT(metaval >= 0);
		    }
		    tile = TILE_DOWNSTAIRS;
		    break;

		case '<':
		    tile = TILE_UPSTAIRS;
		    break;

		case ' ':
		    tile = glb_roomtypedefs[type].tunnelwall_tile;
		    break;
		case '"':
		    tile = TILE_REEDS;
		    break;
		case '-':
		    tile = TILE_CLAY;
		    break;
		case ',':
		    tile = glb_roomtypedefs[type].path_tile;
		    break;
		case '~':
		    if (waterdepth(x, y) > 10)
		    {
			tile = TILE_WATER;
			J_ASSERT(snowdepth(x,y) == 0);
		    }
		    else
			tile = TILE_ICE;
		    break;
		case '^':
		    tile = TILE_MOUNTAIN;
		    if (snowdepth(x, y) > 10)
			tile = TILE_ICEMOUNTAIN;
		    if (metaval >= 0 && metaval < NUM_ORES)
		    {
			tile = glb_oredefs[metaval].tile;
		    }
		    break;
		case 'P':
		    tile = TILE_USERPROTOPORTAL;
		    break;
		case 'V':
		    tile = TILE_ICEMOUNTAIN;
		    break;
		case 'W':
		    tile = TILE_SNOWYPASS;
		    break;
		case '=':
		    tile = TILE_BRIDGE;
		    break;
		case ':':
		    if (rand_chance(30))
		    {
			item = ITEM::create(map, ITEM_LARGEROCK, depth);
			item->move(room->buildPos(rx, ry));
		    }
		    else
		    {
			item = ITEM::create(map, ITEM_ROCK, depth);
			item->move(room->buildPos(rx, ry));
		    }
		    tile = floortile;
		    break;

		default:
		    fprintf(stderr, "Unknown map char %c\r\n",
			    localtiles[x + y * myW]);
	    }

	    // While I like the traps, they need to be rare enough
	    // not to force people to search all the time.  But then
	    // they just become surprise insta-deaths?
	    // So, compromise: traps do very little damage so act
	    // primarily as flavour.  Which in traditional roguelike
	    // fashion is always excused.
	    if (tile == floortile && 
		glb_roomtypedefs[type].hastraps &&
		!rand_choice(25) &&
		NUM_TRAPS > 1)
	    {
		flag = (MAPFLAG_NAMES) (flag | MAPFLAG_TRAP);
	    }
	    room->setTile(rx, ry, tile);
	    room->setFlag(rx, ry, flag, true);
	}
    }

    return room;
}


static ATOMIC_INT32 glbMapId;

///
/// MAP functions
///
MAP::MAP(int depth, DISPLAY *display)
{
    myUniqueId = glbMapId.add(1);
    myAllowDelays = false;
    myVerse = nullptr;
    myNextItemId = 0;

    // We have a zero at the front to keep the uid == 0 free.
    myLiveMobs.append(nullptr);

    // Init the MOB system
    myMobLetterTimeStamp = 0;
    for (int i = 0; i < 26; i++)
	myMobLastSeen[i] = 0;

    // Init the Item system
    myItemIded.clear();
    myItemMagicClass.clear();

    {
	// Mix the potions...
	int		potionclasses[NUM_POTIONS];
	POTION_NAMES	potion;

	int pclass = 1;
	for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	     item = (ITEM_NAMES)(item+1))
	{
	    if (ITEM::defn(item).itemclass == ITEMCLASS_POTION)
	    {
		if (pclass >= NUM_POTIONS)
		{
		    J_ASSERT(!"Too many mundane potions");
		    break;
		}
		potionclasses[pclass] = item;
		pclass++;
	    }
	}
	rand_shuffle(&potionclasses[1], pclass-1);
	FOREACH_POTION(potion)
	{
	    if (potion == POTION_NONE)
		continue;
	    if (potion >= pclass)
	    {
		J_ASSERT(!"Not enough mundane potions");
		break;
	    }
	    myItemMagicClass.indexAnywhere(potionclasses[potion]) = potion;
	}
    }
    {
	// Mix the rings...
	int		ringclasses[NUM_RINGS];
	RING_NAMES	ring;

	int pclass = 1;
	for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	     item = (ITEM_NAMES)(item+1))
	{
	    if (ITEM::defn(item).itemclass == ITEMCLASS_RING)
	    {
		if (pclass >= NUM_RINGS)
		{
		    J_ASSERT(!"Too many mundane rings");
		    break;
		}
		ringclasses[pclass] = item;
		pclass++;
	    }
	}
	rand_shuffle(&ringclasses[1], pclass-1);
	FOREACH_RING(ring)
	{
	    if (ring == RING_NONE)
		continue;
	    if (ring >= pclass)
	    {
		J_ASSERT(!"Not enough mundane rings");
		break;
	    }
	    myItemMagicClass.indexAnywhere(ringclasses[ring]) = ring;
	}
    }

    {
	// Mix the spells...
	int		spellclasses[NUM_SPELLS];
	SPELL_NAMES	spell;

	int pclass = 1;
	for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	     item = (ITEM_NAMES)(item+1))
	{
	    if (ITEM::defn(item).itemclass == ITEMCLASS_SPELLBOOK)
	    {
		if (pclass >= NUM_SPELLS)
		{
		    J_ASSERT(!"Too many mundane spells");
		    break;
		}
		spellclasses[pclass] = item;
		pclass++;
	    }
	}
	rand_shuffle(&spellclasses[1], pclass-1);
	FOREACH_SPELL(spell)
	{
	    if (spell == SPELL_NONE)
		continue;
	    if (spell >= pclass)
	    {
		J_ASSERT(!"Not enough mundane spells");
		break;
	    }
	    myItemMagicClass.indexAnywhere(spellclasses[spell]) = spell;
	}
    }

    {
	// Mix the scrolls...
	int		scrollclasses[NUM_SCROLLS];
	SCROLL_NAMES	scroll;

	int pclass = 1;
	for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	     item = (ITEM_NAMES)(item+1))
	{
	    if (ITEM::defn(item).itemclass == ITEMCLASS_SCROLL)
	    {
		if (pclass >= NUM_SCROLLS)
		{
		    J_ASSERT(!"Too many mundane scrolls");
		    break;
		}
		scrollclasses[pclass] = item;
		pclass++;
	    }
	}
	rand_shuffle(&scrollclasses[1], pclass-1);
	FOREACH_SCROLL(scroll)
	{
	    if (scroll == SCROLL_NONE)
		continue;
	    if (scroll >= pclass)
	    {
		J_ASSERT(!"Not enough mundane scrolls");
		break;
	    }
	    myItemMagicClass.indexAnywhere(scrollclasses[scroll]) = scroll;
	}
    }

    {
	// Mix the wands...
	int		wandclasses[NUM_WANDS];
	WAND_NAMES	wand;

	int pclass = 1;
	for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	     item = (ITEM_NAMES)(item+1))
	{
	    if (ITEM::defn(item).itemclass == ITEMCLASS_WAND)
	    {
		if (pclass >= NUM_WANDS)
		{
		    J_ASSERT(!"Too many mundane wands");
		    break;
		}
		wandclasses[pclass] = item;
		pclass++;
	    }
	}
	rand_shuffle(&wandclasses[1], pclass-1);
	FOREACH_WAND(wand)
	{
	    if (wand == WAND_NONE)
		continue;
	    if (wand >= pclass)
	    {
		J_ASSERT(!"Not enough mundane wands");
		break;
	    }
	    myItemMagicClass.indexAnywhere(wandclasses[wand]) = wand;
	}
    }

    MOB *avatar = MOB::createAvatar(this);

    rebuild(depth, avatar, display, true);
}

void
MAP::rebuild(int depth, MOB *avatar, DISPLAY *display, bool climbeddown)
{
    myFOVCache.reset();
    myAvatar = avatar;

    for (int i = 0; i < DISTMAP_CACHE; i++)
    {
	myDistMapWeight[i] = 0.0;
    }

    for (int i = 0; i < 2; i++)
	myUserPortalDir[i] = 0;

    myMobCount.clear();

    const FRAGMENT	*frag;
    ROOM		*village = nullptr;
    ROOM		*astralplane = nullptr;

    int			 totalstairdist = 0;

    // Ensure seed room is built.
    {
	ROOMTYPE_NAMES roomtype = ROOMTYPE_ISLAND;

	frag = randomFragment(roomtype);
	// Note we want to build the village at the same atlasx as our
	// depth as that is what we index by
	// village isn't necessarily 0,0 as we have to find a random
	// valid as the 0,0 may itself by crap.
	village = frag->buildOvermap(this, roomtype);

	// DEBUG: Auto map
	// village->setAllFlags(MAPFLAG_MAPPED, true);

	// totalstairdist += village->stairDistance();

	// Sanity check we've linked everything:
	for (int i = 0; i < 32; i++)
	{
	    POS stair = findStairCase(0, false, i);
	    J_ASSERT(stair.valid());
	}

	// Populate the world!
	// Do a single pass to avoid n^3 here.
	POSLIST		birthlocs;
	getRandomTiles(birthlocs, 1000, [](POS p) -> bool
	{
	    if (!p.valid()) return false;
	    if (!p.isPassable()) return false;
	    if (p.defn().forbidrandomwander) return false;
	    if (p.mob()) return false;
	    return true;
	});

	for (auto && p : birthlocs)
	{
	    MOB *m = MOB::createSurfaceNPC(this);
	    m->move(p);
	}

	// Create tree mushrooms.
	birthlocs.clear();
	getRandomTiles(birthlocs, 1000, [](POS p) -> bool
	{
	    if (!p.valid()) return false;
	    if (!p.isPassable()) return false;
	    if (p.defn().tree == TREE_NONE) return false;
	    return true;
	});

	for (auto && p : birthlocs)
	{
	    ITEM *mushroom = ITEM::create(this, ITEM_TREE_MUSHROOM);
	    mushroom->setWoodType(p.defn().tree);
	    mushroom->move(p);
	}
    }
    // We must prebuild as we need to have a valid target when
    // we transfer.
    if (1)
    {
	int		prebuild = GAMEDEF::rules().bosslevel;
	while (GAMEDEF::getNumLevel() <= prebuild)
	{
	    GAMEDEF::createNewLevelDef();
	}

	J_ASSERT(prebuild == 20);
	ROOMTYPE_NAMES	roomtypes[20] =
	{
	    ROOMTYPE_CAVEROOMS,
	    ROOMTYPE_CAVEROOMS,
	    ROOMTYPE_CAVEROOMS,
	    ROOMTYPE_CAVE,		// more open so harder
	    ROOMTYPE_CAVE,		// 5
	    ROOMTYPE_DARKCAVE,
	    ROOMTYPE_LABYRINTH,
	    ROOMTYPE_LABYRINTH,
	    ROOMTYPE_LABYRINTH,
	    ROOMTYPE_LABYRINTH,		//10
	    ROOMTYPE_ROGUE,		// Lit from here.
	    ROOMTYPE_ROGUE,
	    ROOMTYPE_MAZE,
	    ROOMTYPE_ROGUE,
	    ROOMTYPE_ROGUE,		//15
	    ROOMTYPE_MAZE,
	    ROOMTYPE_CAVE,
	    ROOMTYPE_CAVE,		// lit so deadly!
	    ROOMTYPE_CAVE,
	    ROOMTYPE_POINTLESS_FINAL,		// 20
	};
	int	roomcounts[20] =
	{
	    16, 8, 8, 4, 4,		// 5
	    4, 4, 4, 4, 4,		// 10
	    4, 4, 4, 4, 4, 		// 15
	    2, 2, 2, 1, 1		// 20
	};

	int		totalcount = 0;
	for (int i = 0; i < 20; i++)
	    totalcount += roomcounts[i];
	int		roomcount = 0;

	for (int newdepth = 1; newdepth <= prebuild; newdepth++)
	{
	    LEVEL_DEF	*def = GAMEDEF::leveldef(newdepth);
	    def->numcreated++;

	    if (newdepth == prebuild)
	    {
		J_ASSERT(roomtypes[newdepth-1] == ROOMTYPE_POINTLESS_FINAL);
	    }

	    int		upstair = 2, downstair = 2;
	    if (newdepth == prebuild) // boss level
		upstair = downstair = 1;
	    else 
	    {
		if (roomcounts[newdepth] < roomcounts[newdepth-1])
		    downstair = 1;
		// Only one downstair to final level.
		if (newdepth == prebuild-1)
		    downstair = 1;
	    }
	    int 		upid = 0, downid = 0;
	    // Track where we have to wrap back to zero:
	    int			maxdown = downstair * roomcounts[newdepth-1];
	    if (downstair == 2)
	    {
		// If we have 2 down, we are a braid as next will
		// have 2 up.
		downid++;
	    }

	    for (int variant = 0; variant < roomcounts[newdepth-1]; variant++)
	    {
		ROOM *level = findAtlasRoom(variant, -newdepth);
		if (!level)
		{
		    //ROOMTYPE_NAMES roomtype = GAMEDEF::leveldef(newdepth)->roomtype;
		    ROOMTYPE_NAMES roomtype = roomtypes[newdepth-1];
		    const FRAGMENT		*frag;
		    frag = randomFragment(roomtype);
		    frag->buildRoom(this, roomtype, upstair, downstair, newdepth, newdepth, /*atlas*/variant, -newdepth);
		    level = findAtlasRoom(variant, -newdepth);

		    POSLIST	stairs;
		    stairs.clear();
		    level->getAllTiles(TILE_DOWNSTAIRS, stairs);
		    stairs.shuffle();
		    for (auto && stair : stairs)
		    {
			stair.setMetaData(downid++);
			if (downid >= maxdown)
			    downid = 0;
		    }

		    stairs.clear();
		    level->getAllTiles(TILE_UPSTAIRS, stairs);
		    stairs.shuffle();
		    for (auto && stair : stairs)
		    {
			stair.setMetaData(upid++);
		    }

		    roomcount++;
		    glbMapBuild = 50 + 50 * roomcount / totalcount;

		    // level->setAllFlags(MAPFLAG_MAPPED, true);
		    // totalstairdist += level->stairDistance();
		}
	    }
	    // Verify our up links
#if 1
	    for (int i = 0; i < roomcounts[newdepth-1] * upstair; i++)
	    {
		// Make sure we can find the link to climb up
		POS stair = findStairCase(newdepth-1, false, i);
		J_ASSERT(stair.valid());
		// Make sure we can find the link to climb down.
		stair = findStairCase(newdepth, true, i);
		J_ASSERT(stair.valid());
	    }
#endif
	}
    }

    {
	ROOMTYPE_NAMES roomtype = ROOMTYPE_BIGROOM;

	frag = randomFragment(roomtype);
	astralplane = frag->buildRoom(this, roomtype, 0, 0, 0, 0, -1, -1);
    }

    if (avatar)
    {
	myAvatar = avatar;
	POS		goal;

	// Build a useless room to hold the avatar.

#if STARTSTAIRS
	goal = village->getRandomTile(
			climbeddown ? TILE_UPSTAIRS
				    : TILE_DOWNSTAIRS,
			POS(), 0);
	if (!goal.valid())
	    goal = village->getRandomTile(TILE_DOWNSTAIRS, POS(), 0);
	if (goal.mob())
	{
	    // Find non-mob spot at the ladder
	    goal = village->spiralSearch(goal, 1, true);
	}
	if (!goal.valid())
#endif
	    goal = village->getRandomPos(POS(), 0);
	// Keep us oriented consistently.
	//goal = goal.rotate(rand_choice(4));
	goal.setAngle(0);
	avatar->move(goal);

	// Ensure our avatar is legal and flagged.
	avatar->updateEquippedItems();
    }

    glbMapBuild = -1;

    myDisplay = display;
}


void
MAP::expandActiveRooms(int roomid, int depth)
{
    if (roomid < 0)
    {
	// Invalid location, no expand
	return;
    }

    ROOM	*thisroom = myRooms(roomid);
    J_ASSERT(thisroom);
    
    if (depth <= 0)
	return;

#if 0
    // If we want to allow auto-expansion we do this, and need to
    // build cannoical atlas coords.
    int		dx, dy;

    FORALL_4DIR(dx, dy)
    {
	int		x = atlasx + dx;
	int		y = atlasy + dy;
	ROOM		*newroom = findAtlasRoom(x, y);

	if (thisroom->myConnectivity[lcl_angle] == ROOM::UNDECIDED)
	{
	    // It is time to decide!
	    // First we see if the decision has been made.
	    // I think this has to be closed or else we'd already
	    // have generated ourself.
	    if (newroom && newroom->myConnectivity[(lcl_angle+2)%4] != ROOM::UNDECIDED)
	    {
		thisroom->myConnectivity[lcl_angle] = newroom->myConnectivity[(lcl_angle+2)%4];
	    }
	    else
	    {
		if (x >= 0 && x < ATLAS_WIDTH && y >= 0 && y < ATLAS_HEIGHT)
		{
		    thisroom->myConnectivity[lcl_angle] = ROOM::IMPLICIT;
		}
		else
		{
		    thisroom->myConnectivity[lcl_angle] = ROOM::DISCONNECTED;
		}
	    }
	    if (newroom && newroom->myConnectivity[(lcl_angle+2)%4] == ROOM::UNDECIDED)
	    {
		// We need to update the new room with our choice.
		// We can technically wait to actually update its status
		// as it will inherit it from us.
		// However, it is important we build the link if
		// we discover it here!
		newroom->myConnectivity[(lcl_angle + 2)%4] = thisroom->myConnectivity[lcl_angle];
		if (thisroom->myConnectivity[lcl_angle] == ROOM::CONNECTED)
		{
		    // Force the new room to be connected to us!
		    // We ignore its other outlets as they will all be UNDECIDED.
		    bool		built;
		    built = ROOM::link( thisroom, lcl_angle,
				newroom, (lcl_angle+2) % 4);
		}
	    }
	}

	// Only expand if we want to be connected this way?
	if (thisroom->myConnectivity[lcl_angle] == ROOM::DISCONNECTED)
	{
	    // Chose not to be connected.
	    continue;
	}

	if (!newroom)
	{
	    // Must build a new room at this loc.
	    ROOMTYPE_NAMES	roomtype = transitionRoom(thisroom->type());

	    frag = randomFragment(roomtype);

	    newroom = frag->buildRoom(this, roomtype, MAX(ABS(x), ABS(y)), x, y, nullptr);
	    // Force the new room to be connected to us!
	    // We ignore its other outlets as they will all be UNDECIDED.
	    // Note this may be implicit or connected
	    newroom->myConnectivity[(lcl_angle + 2)%4] = thisroom->myConnectivity[lcl_angle];

	    if (thisroom->myConnectivity[lcl_angle] == ROOM::CONNECTED)
	    {
		// Requires explicit link!
		bool		built;
		built = ROOM::link( thisroom, lcl_angle,
			    newroom, (lcl_angle+2) % 4);
	    }
	}

	// In any case, expand from here.
	expandActiveRooms(x, y, depth-1);
    }
#endif
}

MAP::MAP(const MAP &map)
{
    myVerse = nullptr;
    myFOVCache.reset();
    myAllowDelays = false;
    *this = map;
}

MAP &
MAP::operator=(const MAP &map)
{
    if (this == &map)
	return *this;

    deleteContents();

    myMobCount.clear();

    myRooms.setCapacity(map.myRooms.entries());
    for (int i = 0; i < map.myRooms.entries(); i++)
    {
	myRooms.append(new ROOM(*map.myRooms(i)));
	myRooms(i)->setMap(this);
    }

    myLiveMobs.setCapacity(map.myLiveMobs.entries());
    for (int i = 0; i < map.myLiveMobs.entries(); i++)
    {
	if (map.myLiveMobs(i))
	{
	    myLiveMobs.append(map.myLiveMobs(i)->copy());
	    myLiveMobs(i)->setMap(this);
	    myMobCount.indexAnywhere(myLiveMobs(i)->getDefinition())++;
	}
	else
	    myLiveMobs.append(nullptr);
    }

    myItems.setCapacity(map.myItems.entries());
    for (int i = 0; i < map.myItems.entries(); i++)
    {
	if (map.myItems(i))
	{
	    myItems.append(map.myItems(i)->copy());
	    myItems(i)->setMap(this);
	}
	else
	    myItems.append(nullptr);
    }
    myNextItemId = map.myNextItemId;

    for (int i = 0; i < 2; i++)
    {
	myUserPortal[i] = map.myUserPortal[i];
	myUserPortal[i].setMap(this);
	myUserPortalDir[i] = map.myUserPortalDir[i];
    }

    for (int i = 0; i < DISTMAP_CACHE; i++)
    {
	myDistMapCache[i] = map.myDistMapCache[i];
	myDistMapCache[i].setMap(this);
	// This is our decay coefficient so stuff eventually will
	// flush out regardless of how popular they were.
	myDistMapWeight[i] = map.myDistMapWeight[i] * 0.75;
    }

    myAvatar = findAvatar();
    
    myUniqueId = map.getId();

    myDisplay = map.myDisplay;

    // We do not have to rebuild fov as we re-write the map
    // pointers on lookup.
    myFOVCache = map.myFOVCache;

    myMobLetterTimeStamp = map.myMobLetterTimeStamp;
    for (int i = 0; i < 26; i++)
	myMobLastSeen[i] = map.myMobLastSeen[i];

    myItemIded = map.myItemIded;
    myItemMagicClass = map.myItemMagicClass;

    mySpeed = map.mySpeed;

    // Does not copy verse ownership!

    return *this;
}

void
MAP::deleteContents()
{
    reapMobs();

    for (int i = 0; i < myRooms.entries(); i++)
    {
	delete myRooms(i);
    }
    myRooms.clear();

    // We must do a backwards loop as invoking delete on a MOB
    // will then invoke removeMob() on ourselves.
    for (int i = myLiveMobs.entries(); i --> 0; )
    {
	if (myLiveMobs(i))
	{
	    myLiveMobs(i)->clearAllPos();
	    delete myLiveMobs(i);
	}
    }
    myLiveMobs.clear();

    // Must do a backwards loop as delete item will invoke removeItem
    for (int i = myItems.entries(); i --> 0; )
    {
	if (myItems(i))
	{
	    myItems(i)->clearAllPos();
	    delete myItems(i);
	}
    }
    myItems.clear();

    for (int i = 0; i < DISTMAP_CACHE; i++)
    {
	myDistMapWeight[i] = 0.0;
	myDistMapCache[i] = POS();
    }

    for (int i = 0; i < 2; i++)
    {
	myUserPortal[i] = POS();
	myUserPortalDir[i] = 0;
    }

    myAvatar = 0;

    myFOVCache.reset();

    // NOTE: Keep the display

    myDelayMobList.clear();
    myMobCount.clear();

    myMobLetterTimeStamp = 0;
    for (int i = 0; i < 26; i++)
	myMobLastSeen[i] = 0;

    myItemIded.clear();
    myItemMagicClass.clear();
}

MAP::~MAP()
{
    deleteContents();
}

void map_buildrandomfloor(TILE_NAMES tile)
{
    TILE_DEF *def = GAMEDEF::tiledef(tile);

    const char *symbols = ".,_'~..-";
    ATTR_NAMES attrs[8] =
    {
	ATTR_BROWN,
	ATTR_WHITE,
	ATTR_DKGREEN,
	ATTR_GREEN,
	ATTR_LIGHTBLACK,
	ATTR_NORMAL,
	ATTR_LTGREY,
	ATTR_LIGHTBROWN
    };


    def->symbol = symbols[rand_choice((int)strlen(symbols))];
    def->attr = attrs[rand_choice(8)];
}

void map_buildrandomdoor(TILE_NAMES tile)
{
    TILE_DEF *def = GAMEDEF::tiledef(tile);

    const char *symbols = "+++XX/\\";
    ATTR_NAMES wallattrs[5] =
    {
	ATTR_BROWN,
	ATTR_WHITE,
	ATTR_NORMAL,
	ATTR_LTGREY,
	ATTR_LIGHTBROWN
    };

    def->symbol = symbols[rand_choice((int)strlen(symbols))];
    def->attr = wallattrs[rand_choice(5)];
}

void map_buildrandomwall(TILE_NAMES tile)
{
    TILE_DEF *def = GAMEDEF::tiledef(tile);

    const char *symbols = "##  &";
    ATTR_NAMES wallattrs[5] =
    {
	ATTR_BROWN,
	ATTR_WHITE,
	ATTR_NORMAL,
	ATTR_LTGREY,
	ATTR_LIGHTBROWN
    };
    ATTR_NAMES treeattrs[4] =
    {
	ATTR_BROWN,
	ATTR_GREEN,
	ATTR_DKGREEN,
	ATTR_LIGHTBROWN
    };
    ATTR_NAMES spaceattrs[4] =
    {
	ATTR_INVULNERABLE,
	ATTR_HILITE,
	ATTR_DKHILITE,
	ATTR_FOOD
    };

    def->symbol = symbols[rand_choice((int)strlen(symbols))];
    if (def->symbol == '#')
	def->attr = wallattrs[rand_choice(5)];
    if (def->symbol == '&')
    {
	def->attr = treeattrs[rand_choice(4)];
	def->legend = "impassable woods";
    }
    if (def->symbol == ' ')
	def->attr = spaceattrs[rand_choice(4)];
}

void map_buildrandomtunnelwall(TILE_NAMES tile)
{
    TILE_DEF *def = GAMEDEF::tiledef(tile);

    const char *symbols = "##    &";
    ATTR_NAMES wallattrs[5] =
    {
	ATTR_BROWN,
	ATTR_WHITE,
	ATTR_NORMAL,
	ATTR_LTGREY,
	ATTR_LIGHTBROWN
    };
    ATTR_NAMES treeattrs[4] =
    {
	ATTR_BROWN,
	ATTR_GREEN,
	ATTR_DKGREEN,
	ATTR_LIGHTBROWN
    };
    ATTR_NAMES spaceattrs[8] =
    {
	ATTR_NORMAL,
	ATTR_NORMAL,
	ATTR_NORMAL,
	ATTR_NORMAL,
	ATTR_INVULNERABLE,
	ATTR_HILITE,
	ATTR_DKHILITE,
	ATTR_FOOD
    };

    def->symbol = symbols[rand_choice((int)strlen(symbols))];
    if (def->symbol == '#')
	def->attr = wallattrs[rand_choice(5)];
    if (def->symbol == '&')
    {
	def->attr = treeattrs[rand_choice(4)];
	def->legend = "impassable woods";
    }
    if (def->symbol == ' ')
	def->attr = spaceattrs[rand_choice(8)];
}

void
MAP::buildLevelList()
{
    const static ROOMTYPE_NAMES levellist[10] =
    {
	ROOMTYPE_ROGUE,
	ROOMTYPE_ROGUE,
	ROOMTYPE_ROGUE,
	ROOMTYPE_CAVE,
	ROOMTYPE_CAVE,
	ROOMTYPE_CAVE,
	ROOMTYPE_DARKCAVE,
	ROOMTYPE_MAZE,
	ROOMTYPE_MAZE,
	ROOMTYPE_BIGROOM		// Rename fortnite :>
    };
    for (int lvl = 0; lvl < GAMEDEF::rules().bosslevel; lvl++)
    {
	LEVEL_NAMES level = GAMEDEF::createNewLevelDef();
	LEVEL_DEF *def = GAMEDEF::leveldef(level);
	// Don't use big room for boss level as it is unplayable.
	def->roomtype = levellist[rand_choice(10 - (lvl == GAMEDEF::rules().bosslevel-1))];
    }

    // Build our look.
    map_buildrandomwall(TILE_WALL);
    map_buildrandomwall(TILE_MAZEWALL);
    map_buildrandomwall(TILE_CAVEWALL);
    map_buildrandomtunnelwall(TILE_TUNNELWALL);
    
    map_buildrandomfloor(TILE_FLOOR);
    map_buildrandomfloor(TILE_CAVEFLOOR);
    map_buildrandomfloor(TILE_DARKCAVEFLOOR);
    map_buildrandomfloor(TILE_MAZEFLOOR);
    map_buildrandomfloor(TILE_PATH);

    map_buildrandomdoor(TILE_DOOR_CLOSED);
    map_buildrandomdoor(TILE_MAZEDOOR);

    // Copy wall colour into broken wall colour.
    GAMEDEF::tiledef(TILE_BROKENWALL)->attr = GAMEDEF::tiledef(TILE_WALL)->attr;
}

void
MAP::buildSquare(ROOMTYPE_NAMES roomtype,
	    ROOM *entrance, int enterdir,
	    ROOM *&exit, int &exitdir,
	    int difficulty,
	    int depth,
	    int w, int h)
{
    ROOM		**rooms;

    rooms = new ROOM *[w * h];

    for (int y = 0; y < h; y++)
	for (int x = 0; x < w; x++)
	{
	    const FRAGMENT		*frag;

	    frag = randomFragment(roomtype);
	    rooms[x + y * w] = frag->buildRoom(this, roomtype, 0, 0, difficulty, depth, x, y);
	}

    // Vertical links
    for (int y = 0; y < h-1; y++)
	for (int x = 0; x < w; x++)
	{
	    ROOM::link( rooms[x + y * w], 2,
			rooms[x + (y+1)*w], 0 );
	}

    // Horizontal links
    for (int y = 0; y < h; y++)
	for (int x = 0; x < w-1; x++)
	{
	    ROOM::link( rooms[x + y * w], 1,
			rooms[x+1 + y * w], 3 );
	}

    // Link in the entrance.
    // Since we are all orientation invariant, we can always link into
    // our LHS.
    int x, y;
    x = 0;
    y = rand_choice(h);
    ROOM::link( entrance, enterdir,
		rooms[x + y * w], 3 );
    // Now choose an exit...
    int		v = rand_choice( (w-1) * 2 + h );
    if (v < w-1)
    {
	exit = rooms[v + 1 + 0 * w];
	exitdir = 0;
    }
    else
    {
	v -= w-1;
	if (v < w -1)
	{
	    exit = rooms[v + 1 + (h-1) * w];
	    exitdir = 2;
	}
	else
	{
	    v -= w - 1;
	    J_ASSERT(v < h);
	    exit = rooms[(w-1) + v * w];
	    exitdir = 1;
	}
    }

    delete [] rooms;
}

POS
MAP::findStairCase(int depth, bool down, int staircase) const
{
    TILE_NAMES	tile = down ? TILE_UPSTAIRS : TILE_DOWNSTAIRS;

    POSLIST	locs;

    if (depth == 0)
    {
	// Must have climbed up.
	J_ASSERT(!down);
	// Search all overworld rooms for stair case...
	for (auto && room : myRooms)
	{
	    if (room->atlasX() >= 0 && room->atlasY() >= 0)
	    {
		// Surface world!
		locs.clear();
		room->getAllTiles(tile, locs);
		
		for (auto && loc : locs)
		{
		    if (loc.metaData() == staircase)
			return loc;
		}
	    }
	}
	// Failed!
	return POS();
    }

    // Lower depths we can determine the atlas number constructively.
    // But we can skip that and just do a full search, not entirely
    // a bad idea because findAtlasRoom is linear anyways :>
    // But this way we don't have to know about braiding, so long
    // as staircase numbers between levels are matching.

    for (auto && room : myRooms)
    {
	if (room->atlasY() == -depth)
	{
	    // A dungeon at our depth
	    locs.clear();
	    room->getAllTiles(tile, locs);
	    
	    for (auto && loc : locs)
	    {
		if (loc.metaData() == staircase)
		    return loc;
	    }
	}
    }
    // Failed!
    return POS();
}

bool
MAP::moveAvatarToDepth(int newdepth, int staircase)
{
    MOB		*thisavatar = avatar();

    if (!thisavatar)
	return false;

    // Clamp the lowest depth as we index the level array.
    if (newdepth < 0)
	newdepth = 0;

    DISPLAY	*display = myDisplay;
    bool	 down = newdepth > thisavatar->pos().depth();
    J_ASSERT(newdepth == thisavatar->pos().depth()+1 || newdepth == thisavatar->pos().depth()-1);
    bool	 newlevel = false;

    int		 difficulty = newdepth;

    // Query about new level.
    while (GAMEDEF::getNumLevel() <= newdepth)
    {
	newlevel = true;
	GAMEDEF::createNewLevelDef();
    }
    LEVEL_DEF	*def = GAMEDEF::leveldef(newdepth);
    def->numcreated++;

    POS stair = findStairCase(newdepth, down, staircase);
    if (!stair.valid())
    {
	J_ASSERT(!"Expected to have pregenerated");
	return false;
    }

    // Move the avatar out of the map.
    thisavatar->setPos(POS());
    thisavatar->clearAllPos();

    if (stair.mob())
    {
	// Find non-mob spot at the ladder
	stair = stair.room()->spiralSearch(stair, 1, true);
    }
    if (!stair.valid())
	stair = stair.room()->getRandomPos(POS(), 0);
    stair.setAngle(0);
    {
#if 0
	BUF		report;
	int		story = newdepth;
	if (story != GAMEDEF::rules().bosslevel)
	{
	    report.sprintf("You enter floor %d of The %s.",
		    story, "Labyrinth");
	}
	else
	    report.sprintf("You enter the final floor of The %s.  The Altar is here!", "Labyrinth");
	msg_report(report);
#endif
    }
    thisavatar->move(stair);

    // Ensure our avatar is legal and flagged.
    thisavatar->updateEquippedItems();

    rebuildFOV();

    // Report new location after we've built the fov.
    if (glbConfig->myVerbose)
    {
	doChamberScan(this);
	doLookAround(this, true);
    }

    display->pendingForgetAll();

    return true;
}

bool
MAP::moveMobToDepth(MOB *mob, int newdepth, int staircase)
{
    MOB		*thisavatar = avatar();

    // Clamp the lowest depth as we index the level array.
    if (newdepth < 0)
	return false;

    bool	 down = newdepth > mob->pos().depth();
    J_ASSERT(newdepth == mob->pos().depth()+1 || newdepth == mob->pos().depth()-1);

    POS stair = findStairCase(newdepth, down, staircase);
    if (!stair.valid())
    {
	// Can't create rooms ourselves.
	return false;
    }

    // Move the avatar out of the map.
    mob->setPos(POS());
    mob->clearAllPos();

    if (stair.mob())
    {
	// Find non-mob spot at the ladder
	stair = stair.room()->spiralSearch(stair, 1, true);
    }
    if (!stair.valid())
	stair = stair.room()->getRandomPos(POS(), 0);
    stair.setAngle(0);
    mob->move(stair);

    // Ensure our avatar is legal and flagged.
    mob->updateEquippedItems();
    return true;
}

void
MAP::destroyAllItemsAndMobs()
{
    // We must do a backwards loop as invoking delete on a MOB
    // will then invoke removeMob() on ourselves.
    for (int i = myLiveMobs.entries(); i --> 0; )
    {
	if (myLiveMobs(i))
	{
	    delete myLiveMobs(i);
	}
    }
    myLiveMobs.clear();

    // Must do a backwards loop as delete item will invoke removeItem
    for (int i = myItems.entries(); i --> 0; )
    {
	if (myItems(i))
	{
	    delete myItems(i);
	}
    }
    myItems.clear();
}

int
MAP::getNumMobs() const
{
    int		i, total = 0;

    for (i = 0; i < myLiveMobs.entries(); i++)
    {
	if (myLiveMobs(i))
	    total++;
    }

    return total;
}

int
MAP::getAllItems(ITEMLIST &list) const
{
    for (int i = 0; i < myItems.entries(); i++)
    {
	if (myItems(i))
	    list.append(myItems(i));
    }

    return list.entries();
}

int
MAP::getVisibleItems(ITEMLIST &list) const
{
    for (int i = 0; i < myItems.entries(); i++)
    {
	if (myItems(i) && myItems(i)->pos().isFOV())
	    list.append(myItems(i));
    }

    return list.entries();
}

int
MAP::getAllMobs(MOBLIST &list) const
{
    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (myLiveMobs(i))
	    list.append(myLiveMobs(i));
    }

    return list.entries();
}

int
MAP::getVisibleMobs(MOBLIST &list) const
{
    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (myLiveMobs(i) && myLiveMobs(i)->pos().isFOV())
	    list.append(myLiveMobs(i));
    }

    return list.entries();
}

int
MAP::getAllMobsOfType(MOBLIST &list, MOB_NAMES type) const
{
    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (myLiveMobs(i))
	{
	    if (myLiveMobs(i)->getDefinition() == type)
		list.append(myLiveMobs(i));
	}
    }

    return list.entries();
}

int
MAP::getNumVisibleMobsOfType(MOB_NAMES type) const
{
    return myMobsInFOV.safeIndexAnywhere(type);
}

POS
MAP::getRandomTile(TILE_NAMES tile) const
{
    int			n = 1;
    POS			p;

    for (int i = 0; i < myRooms.entries(); i++)
    {
	p = myRooms(i)->getRandomTile(tile, p, &n);
    }
    return p;
}

POS
MAP::getRandomTileOnFloor(POS p, TILE_NAMES tile) const
{
    int			n = 1;

    if (!p.room()) return p;
    p = p.room()->getRandomTile(tile, p, &n);
    return p;
}

POS
MAP::findCloseUnexploredOnFloor(POS pos) const
{
    if (!pos.valid()) return POS();

    return pos.room()->findCloseUnexplored(pos);
}

int
MAP::getVisibleExits(POSLIST &list) const
{
    for (int i = 0; i < myRooms.entries(); i++)
    {
	myRooms(i)->getVisibleExits(list);
    }
    return list.entries();
}

POS
MAP::spiralSearch(POS start, int startrad, bool avoidmob) const
{
    return start.room()->spiralSearch(start, startrad, avoidmob);
}

void
MAP::setAllFlags(MAPFLAG_NAMES flag, bool state)
{
    int		i;

    for (i = 0; i < myRooms.entries(); i++)
    {
	myRooms(i)->setAllFlags(flag, state);
    }
}

ROOMTYPE_NAMES
MAP::transitionRoom(ROOMTYPE_NAMES lastroom)
{
    switch (lastroom)
    {
	case ROOMTYPE_WILDERNESS:
	    return ROOMTYPE_WILDERNESS;

	case ROOMTYPE_ATRIUM:
	    return ROOMTYPE_CORRIDOR;
	
	case ROOMTYPE_CORRIDOR:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_DININGROOM,
		ROOMTYPE_BARRACKS,
		ROOMTYPE_LIBRARY,
		ROOMTYPE_GUARDHOUSE
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_BARRACKS:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_BARRACKS,
		ROOMTYPE_GUARDHOUSE
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_GUARDHOUSE:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_THRONEROOM,
		ROOMTYPE_VAULT,
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_THRONEROOM:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_VAULT,
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_LIBRARY:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_LIBRARY,
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_KITCHEN:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_PANTRY,
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}
	case ROOMTYPE_DININGROOM:
	{
	    ROOMTYPE_NAMES	transitions[] =
	    {
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_CORRIDOR,
		ROOMTYPE_KITCHEN,
	    };

	    return transitions[rand_choice(sizeof(transitions)/sizeof(ROOMTYPE_NAMES))];
	}

        default:
            break;
    }

    return ROOMTYPE_CORRIDOR;
}

int
MAP::lookupDistMap(POS goal) const
{
    int		i;

    for (i = 0; i < DISTMAP_CACHE; i++)
    {
	if (goal == myDistMapCache[i])
	{
	    // Successful find!
	    myDistMapWeight[i] += 1.0;
	    return i;
	}
    }
    return -1;
}

int
MAP::allocDistMap(POS goal)
{
    int			i;
    double		bestweight = 1e32;
    int			bestindex = 0;

    for (i = 0; i < DISTMAP_CACHE; i++)
    {
	// Trivial success!
	if (!myDistMapCache[i].valid())
	{
	    bestindex = i;
	    break;
	}

	if (myDistMapWeight[i] < bestweight)
	{
	    bestindex = i;
	    bestweight = myDistMapWeight[i];
	}
    }

    myDistMapWeight[bestindex] = 1.0;
    myDistMapCache[bestindex] = goal;

    clearDistMap(bestindex);

    return bestindex;
}

void
MAP::clearDistMap(int distmap)
{
    int			i;

    for (i = 0; i < myRooms.entries(); i++)
    {
	myRooms(i)->clearDistMap(distmap);
    }
}

int
MAP::buildDistMap(POS goal)
{
    POSLIST		stack;
    POS			p, np;
    int			dist, dx, dy;
    int			distmap;

    J_ASSERT(goal.valid());

    stack.setCapacity(1000);

    distmap = lookupDistMap(goal);
    if (distmap >= 0)
	return distmap;

#ifdef DO_TIMING
    cerr << "Build distance map" << endl;
#endif

    distmap = allocDistMap(goal);

    goal.setDistance(distmap, 0);

    stack.append(goal);
    while (stack.entries())
    {
	p = stack.removeFirst();

	dist = p.getDistance(distmap);
	dist++;

	FORALL_8DIR(dx, dy)
	{
	    np = p.delta(dx, dy);
	    int cost = 1;

	    if (!np.valid())
		continue;

	    if (!np.isPassable())
	    {
		// You can get through doors some way or other...
		if (np.defn().isdoorway)
		    cost = 3;
		else
		    continue;
	    }

	    if (np.getDistance(distmap) >= 0)
		continue;

	    np.setDistance(distmap, dist+cost);

	    stack.append(np);
	}
    }
    return distmap;
}

void
MAP::addDeadMob(MOB *mob)
{
    if (mob)
    {
	myDeadMobs.append(mob);
    }
}

void
MAP::reapMobs()
{
    for (int i = 0; i < myDeadMobs.entries(); i++)
	delete myDeadMobs(i);

    myDeadMobs.clear();
}

void
MAP::removeMob(MOB *mob, POS pos)
{
    if (!mob)
	return;
    int		uid = mob->getUID();

    if (uid == INVALID_UID)
	return;

    MOB		*livemob = myLiveMobs(uid);
    J_ASSERT(livemob == mob);
    myLiveMobs(uid) = nullptr;

    J_ASSERT(myMobCount.entries() > mob->getDefinition());
    myMobCount.indexAnywhere(mob->getDefinition())--;
}

void
MAP::replaceMob(MOB *orig, MOB *newmob)
{
    J_ASSERT(orig && newmob);
    if (!orig || !newmob)
	return;

    int		uid = orig->getUID();
    J_ASSERT(uid != INVALID_UID);
    myLiveMobs.indexAnywhere(uid) = newmob;
    newmob->setUID(uid);

    myMobCount.indexAnywhere(orig->getDefinition())--;
    myMobCount.indexAnywhere(newmob->getDefinition())++;

    if (myAvatar == orig)
	myAvatar = newmob;
}

void
MAP::addMob(MOB *mob, POS pos)
{
    if (!mob)
	return;
    int		uid = mob->getUID();

    if (uid == INVALID_UID)
    {
	uid = myLiveMobs.append(mob);
	mob->setUID(uid);
    }
    myLiveMobs.indexAnywhere(uid) = mob;

    myMobCount.indexAnywhere(mob->getDefinition())++;
}

void
MAP::moveMob(MOB *mob, POS pos, POS oldpos)
{
    if (!mob)
	return;
    if (pos == oldpos)
	return;
    int		uid = mob->getUID();

    J_ASSERT(uid > 0);
    J_ASSERT(myLiveMobs(uid) == mob);
}

void
MAP::removeItem(ITEM *item, POS pos)
{
    if (!item)
	return;
    int		uid = item->getUID();

    // Not on map if no back pointer.
    if (uid == INVALID_UID)
	return;

    ITEM		*liveitem = myItems.safeIndexAnywhere(uid);
    J_ASSERT(liveitem == item || liveitem == nullptr);
    myItems.setAnywhere(uid, nullptr);
}

void
MAP::addItem(ITEM *item, POS pos)
{
    if (!item)
	return;
    int		uid = item->getUID();

    if (uid == INVALID_UID)
    {
	// Unlike mobs, items have uids...
	J_ASSERT(false);
    }
    item->setDropTime(getTime());
    myItems.setAnywhere(uid, item);
}

int
MAP::allocItemId() const
{
    // Yeah, could do something smarter with post increment, but
    // then you, the reader, might have to think!
    int		newid = myNextItemId;
    myNextItemId++;
    return newid;
}

void
MAP::reportItemId(int uid) const
{
    // We store next, so +1.
    myNextItemId = MAX(myNextItemId, uid+1);
}

bool
MAP::requestDelayedMove(MOB *mob, int dx, int dy)
{
    if (!myAllowDelays)
	return false;

    myDelayMobList.append( MOBDELAY(mob, dx, dy) );
    return true;
}

void
MAP::requestBurn(POS p)
{
    if (!p.valid()) return;
    myBurnList.append(p);
}

void
MAP::burnRequested()
{
    // Now burn all the requested squares...
    for (auto && p : myBurnList)
    {
	p.burnSquare();
    }
    myBurnList.clear();
}

void
mapAddAllToFinal(MOB *mob, MOBDELAYLIST &final, const MOBDELAYLIST &list)
{
    int		idx;

    if (!mob->isDelayMob())
	return;

    idx = mob->delayMobIdx();
    final.append(list(idx));
    mob->setDelayMob(false, -1);

    for (int j = 0; j < mob->collisionSources().entries(); j++)
    {
	MOB	*parent;

	parent = mob->collisionSources()(j);
	parent->setCollisionTarget(0);

	mapAddAllToFinal(parent, final, list);
    }
}

MOB *
mapFindCycle(MOB *mob)
{
    // Yay for wikipedia!
    MOB		*tortoise, *hare;

    tortoise = mob;
    hare = tortoise->collisionTarget();

    while (hare)
    {
	if (tortoise == hare)
	    return tortoise;		// Tortoise always wins :>
	tortoise = tortoise->collisionTarget();
	if (!tortoise)
	    return 0;
	hare = hare->collisionTarget();
	if (!hare)
	    return 0;
	hare = hare->collisionTarget();
	if (!hare)
	    return 0;
    }
    return 0;
}

void
mapBuildContactGraph(const MOBDELAYLIST &list)
{
    int		i;

    // Clear our old tree.
    for (i = 0; i < list.entries(); i++)
    {
	MOB		*mob = list(i).myMob;
	mob->clearCollision();
    }
    // Build our tree.
    for (i = 0; i < list.entries(); i++)
    {
	MOB		*mob = list(i).myMob;
	if (!mob->alive() || !mob->isDelayMob())
	    continue;
	mob->setCollisionTarget(mob->pos().delta( list(i).myDx,
						  list(i).myDy ).mob() );
    }
}

void
mapResolveCycle(MOB *mob)
{
    MOB		*next, *start;
    MOBLIST	cycle;
    POSLIST	goals;

    // Compute the cycle
    start = mob;
    do
    {
	next = mob->collisionTarget();

	// Disconnect anyone pointing to me.
	for (int i = 0; i < mob->collisionSources().entries(); i++)
	{
	    mob->collisionSources()(i)->setCollisionTarget(0);
	}

	cycle.append(mob);
	// We can't use next->pos as next will be null when we complete
	// our loop because the above code will disconnect the back pointer!
	// Thus the goal store *our* position.
	goals.append(mob->pos());
	mob->setDelayMob(false, -1);

	mob = next;
    } while (mob && mob != start);


    // We do this in two steps to avoid any potential issues of
    // the underlying map not liking two mobs on the same square.

    // Yank all the mobs off the map
    for (int i = 0; i < cycle.entries(); i++)
    {
	cycle(i)->setPos(POS());
    }

    // And drop them back on their goal pos.
    for (int i = 0; i < cycle.entries(); i++)
    {
	// The goals are off by one, everyone wants to get to the
	// next goal
	cycle(i)->setPos(goals((i + 1) % cycle.entries()));
    }
}

bool
mapRandomPushMob(MOB *mob, int pdx, int pdy)
{
    POSLIST		pushlist;
    POS			target, p, nt;

    pushlist.append(mob->pos());

    // We demand no pushbacks
    // So we do something like 
    int			vdx[3], vdy[3];

    if (pdx && pdy)
    {
	// Diagonal!
	vdx[0] = pdx;
	vdy[0] = pdy;
	vdx[1] = pdx;
	vdy[1] = 0;
	vdx[2] = 0;
	vdy[2] = pdy;
    }
    else
    {
	// Horizontal!
	// Too clever wall sliding code
	int			sdx = !pdx;
	int			sdy = !pdy;

	vdx[0] = pdx;
	vdy[0] = pdy;
	vdx[1] = pdx-sdx;
	vdy[1] = pdy-sdy;
	vdx[2] = pdx+sdx;
	vdy[2] = pdy+sdy;
    }

    target = mob->pos().delta(pdx, pdy);
    while (target.mob())
    {
	bool		found = false;
	int		nfound;

	if (pushlist.find(target) >= 0)
	{
	    // We hit an infinite loop, no doubt due to a looped room.
	    return false;
	}

	pushlist.append(target);

	// Find a place to push target to.
	// We only ever move in vdx possible directions.  We also
	// verify we don't end up beside the mob.  Thanks to looping rooms
	// this could happen!

	nfound = 0;
	for (int i = 0; i < 3; i++)
	{
	    p = target.delta(vdx[i], vdy[i]);
	    if (target.mob()->canMove(p))
	    {
		// Yay!
		nfound++;
		if (!rand_choice(nfound))
		{
		    nt = p;
		    found = true;
		}
	    }
	}
	if (found)
	{
	    target = nt;
	    J_ASSERT(!target.mob());
	    continue;		// But should exit!
	}

	// Try again, but find a mob to push.
	nfound = 0;
	for (int i = 0; i < 3; i++)
	{
	    p = target.delta(vdx[i], vdy[i]);
	    if (target.mob()->canMove(p, false))
	    {
		// Yay!
		nfound++;
		if (!rand_choice(nfound))
		{
		    nt = p;
		    found = true;
		}
	    }
	}
	if (!found)
	{
	    // OKay, this path failed to push.  Note that there might
	    // be other paths that *could* push.  We handle this
	    // by invoking this multiple times and having
	    // each invocation pick a random push path.
	    return false;
	}
	target = nt;
    }

    J_ASSERT(!target.mob());
    // Now we should have a poslist chain to do pushing with.
    // We start with the end and move backwards.
    while (pushlist.entries())
    {
	MOB		*pushed;

	p = pushlist.pop();

	J_ASSERT(!target.mob());

	// Move the mob from p to target.
	pushed = p.mob();
	if (pushed)
	{
	    pushed->setPos(target);
	    if (pushed->isDelayMob())
		pushed->setDelayMob(false, -1);
	    else
	    {
		// This is so no one can say that the kobolds are cheating!
		pushed->skipNextTurn();
	    }
	}

	// Now update target.
	target = p;
    }
    return true;
}

bool
mapPushMob(MOB *mob, int pdx, int pdy)
{
    // First, we only start a push if we are in one square
    // of the avatar.

    // This shouldn't happen.
    if (!pdx && !pdy)
	return false;

#if 0
    int			dx, dy;
    POS			avatarpos;
    FORALL_8DIR(dx, dy)
    {
	if (mob->pos().delta(dx, dy).mob() &&
	    mob->pos().delta(dx, dy).mob()->isAvatar())
	{
	    avatarpos = mob->pos().delta(dx, dy);
	    break;
	}
    }
    // Refuse to push, no avatar nearby.
    if (!avatarpos.valid())
	return false;
#endif

    // Each push tries a different path, we pick 10 as a good test
    // to see if any of the paths are pushable.
    for (int i = 0; i < 10; i++)
    {
	if (mapRandomPushMob(mob, pdx, pdy))
	    return true;
    }
    return false;
}


void
mapReduceList(const MOBLIST &allmobs, MOBDELAYLIST &list)
{
    // Build our collision graph.
    int			i, j;
    MOBLIST		roots;

    MOBDELAYLIST	final;

    for (i = 0; i < allmobs.entries(); i++)
    {
	allmobs(i)->clearCollision();
	allmobs(i)->setDelayMob(false, -1);
    }

    for (i = 0; i < list.entries(); i++)
    {
	MOB		*mob = list(i).myMob;
	mob->setDelayMob(true, i);
    }

    // We alternately clear out all roots, then destroy a cycle,
    // until nothing is left.
    bool		mobstoprocess = true;
    while (mobstoprocess)
    {
	mapBuildContactGraph(list);

	// Find all roots.
	roots.clear();
	for (i = 0; i < list.entries(); i++)
	{
	    MOB		*mob = list(i).myMob;
	    if (!mob->alive())
		continue;
	    if (!mob->isDelayMob())
		continue;
	    if (!mob->collisionTarget() || !mob->collisionTarget()->isDelayMob())
		roots.append(mob);
	}

	// Note that as we append to roots, this will grow as we add more
	// entries.
	for (i = 0; i < roots.entries(); i++)
	{
	    MOB		*mob = roots(i);
	    bool	 	 resolved = false;

	    if (!mob->alive())
	    {
		resolved = true;
	    }
	    // Mobs may have lots theyr delay flag if they got pushed.
	    else if (mob->isDelayMob())
	    {
		int		 dx, dy, idx;
		MOB		*victim;

		idx = mob->delayMobIdx();
		dx = list(idx).myDx;
		dy = list(idx).myDy;

		victim = mob->pos().delta(dx, dy).mob();

		// If there is no collision target, easy!
		// Note that no collision target doesn't mean no victim!
		// Someone could have moved in the mean time or died.
		if (!victim)
		{
		    resolved = mob->actionWalk(dx, dy);
		}
		else
		{
		    // Try push?
		    // We push according to rank.
		    if (mob->shouldPush(victim))
			resolved = mapPushMob(mob, dx, dy);
		}
	    }

	    if (resolved)
	    {
		mob->setDelayMob(false, -1);
		for (j = 0; j < mob->collisionSources().entries(); j++)
		{
		    // We moved out!
		    mob->collisionSources()(j)->setCollisionTarget(0);
		    roots.append(mob->collisionSources()(j));
		}
		mob->collisionSources().clear();
	    }
	    else
	    {
		// Crap, add to final.
		mapAddAllToFinal(mob, final, list);
	    }
	}

	// Rebuild our contact graph since various folks have moved or died.
	mapBuildContactGraph(list);

	// There are no roots, so either we have everything accounted for,
	// or there is a cycle.
	mobstoprocess = false;
	for (i = 0; i < list.entries(); i++)
	{
	    MOB		*mob = list(i).myMob;
	    if (!mob->alive())
		continue;
	    if (!mob->isDelayMob())
		continue;

	    // Finds a cycle, the returned mob is inside that cycle.
	    mob = mapFindCycle(mob);

	    if (mob)
	    {
		// Resolving this cycle may create new roots.
		mobstoprocess = true;
		mapResolveCycle(mob);
	    }
	}
    }

    // Only keep unprocessed delays.
    for (i = 0; i < list.entries(); i++)
    {
	MOB		*mob = list(i).myMob;
	if (mob->alive() && mob->isDelayMob())
	    final.append( list(i) );

	mob->setDelayMob(false, -1);
    }

    list = final;
}

void
MAP::doHeartbeat()
{
    mySpeed.incTime();

    // Our simulation for smoke we want to be unbiased, but we
    // write & read.  We want to splat outwards only, so run red/black
    // to decouple the passes.
    if (getPhase() == PHASE_NORMAL)
    {
	static int	toggle = 0;
	toggle = !toggle;
	for (int colour = 0; colour < 4; colour++)
	{
	    for (auto && room : myRooms)
	    {
		room->simulateSmoke((colour ^ toggle) & 1);
	    }
	}
    }
    for (auto && room : myRooms)
    {
	room->updateMaxSmoke();
    }

    // Update items
    if (getPhase() == PHASE_NORMAL || getPhase() == PHASE_SLOW)
    {
	ITEMLIST allitems;
	ITEMLIST itemstopurge;
	// we prefetch as we may generate items...
	// note that it is important our callbacks never delete OTHER
	// items, which is why there is a separate burn request queue.
	getAllItems(allitems);
	for (auto && item : allitems)
	{
	    if (!item) continue;
	    if (item->runHeartbeat(nullptr))
		itemstopurge.append(item);
	}
	for (auto && item : itemstopurge)
	{
	    item->move(POS());
	    delete item;
	}

	burnRequested();
    }
}

POS
MAP::buildPos(int x, int y, int roomid) const
{
    if (roomid < 0 || roomid >= myRooms.entries())
	return POS();
    ROOM *room = myRooms(roomid);
    if (!room)
	return POS();

    return room->buildPos(x, y);
}

POS
MAP::buildFromCanonical(int x, int y) const
{
    int		atlasx, atlasy;
    if (x < 0 || y < 0)
	return POS();

    atlasx = x / 100;
    atlasy = y / 100;
    ROOM	*room = findAtlasRoom(atlasx, atlasy);
    if (!room)
	return POS();
    J_ASSERT(x >= 0 && y >= 0);
    return room->buildPos(x % 100, y % 100);
}


void
MAP::doMoveNPC()
{
    MOBLIST		allmobs;
#ifdef DO_TIMING
    int 		movestart = TCOD_sys_elapsed_milli();
#endif

    // Note we don't need to pre-reap dead mobs since they won't
    // be in a room, so not be picked up in getAllMobs.

    // Pregather in case mobs move through portals.
    getAllMobs(allmobs);

    // Ensure we are somewhat fair.
    // Otherwise orange beats out cyan!
    allmobs.shuffle();

    // The avatar may die during this process so can't test against him
    // in the inner loop.
    allmobs.removePtr(avatar());

    bool		hasavatar = false;
    int			avatardepth = 0;
    if (avatar())
    {
	hasavatar = true;
	avatardepth = avatar()->pos().depth();
    }

    myDelayMobList.clear();
    myAllowDelays = true;

    // TODONE: A mob may kill another mob, causing this to die!
    // Our solution is just to check the alive flag.
    // We also need to handle a mob moving to stasis, so if
    // it has no valid position we assume it is in stasis and
    // it will lose this turn.
    for (int i = 0; i < allmobs.entries(); i++)
    {
	if (!allmobs(i)->alive())
	    continue;
	if (!allmobs(i)->pos().valid())
	    continue;		// Stasis
	// Only run adjacent levels so stair chasing works.
	if (ABS(allmobs(i)->pos().depth() - avatardepth) > 1)
	    continue;
	if (!allmobs(i)->aiForcedAction())
	    allmobs(i)->aiDoAI();
    }

#ifdef DO_TIMING
    int		movedoneai = TCOD_sys_elapsed_milli();
#endif

    int		delaysize = myDelayMobList.entries();
    int		prevdelay = delaysize+1;

    // So long as we are converging, it is good.
    while (delaysize && delaysize < prevdelay)
    {
	MOBDELAYLIST	list;

	// Copy the list as we will be re-registering everyone.
	list = myDelayMobList;

	// Randomizing the list helps avoid any problems from biased
	// input.
	// (A better approach would be to build a contact graph)
	list.shuffle();

	myDelayMobList.clear();

	mapReduceList(allmobs, list);

	for (int i = 0; i < list.entries(); i++)
	{
	    // One way to reduce the list :>
	    if (!list(i).myMob->alive())
		continue;
	    // Not quite right as new form takes space?
	    if (!list(i).myMob->pos().valid())
		continue;

	    // No need for force action test as it must have passed and
	    // no double jeapordy.
	    list(i).myMob->aiDoAI();
	}

	prevdelay = delaysize;

	delaysize = myDelayMobList.entries();
    }

    // Convergence!  Any left over mobs may still have another thing
    // they want to do, however, so we reinvoke with delay disabled.
    myAllowDelays = false;
    if (myDelayMobList.entries())
    {
	MOBDELAYLIST	list;

	// Copy the list as we will be re-registering everyone.
	list = myDelayMobList;

	// No need to randomize or anything.
	myDelayMobList.clear();

	for (int i = 0; i < list.entries(); i++)
	{
	    // One way to reduce the list :>
	    if (!list(i).myMob->alive())
		continue;
	    // Not quite right as new form takes space?
	    if (!list(i).myMob->pos().valid())
		continue;

	    // No need for force action test as it must have passed and
	    // no double jeapordy.
	    list(i).myMob->aiDoAI();
	}
    }

    // Delete any dead mobs
    reapMobs();

#ifdef DO_TIMING
    int		movedoneall = TCOD_sys_elapsed_milli();

    static int	movetime, posttime;

    movetime = movedoneai - movestart;
    posttime = movedoneall - movedoneai;

    cerr << "move time " << movetime << " post time " << posttime << endl;
    cerr << "Mob count " << getNumMobs() << endl;
#endif
}

MOB *
MAP::findAvatar()
{
    // Pregather in case mobs move through portals.
    for (auto && mob : myLiveMobs)
    {
	if (!mob)
	    continue;
	if (mob->isAvatarDef())
	    return mob;
    }
    return nullptr;
}

const FRAGMENT *
MAP::randomFragment(ROOMTYPE_NAMES roomtype)
{
    J_ASSERT(roomtype >= 0 && roomtype <= NUM_ROOMTYPES);
    return theFragRooms[roomtype](rand_choice(theFragRooms[roomtype].entries()));
}

void
MAP::init()
{
    DIRECTORY		dir;
    const char		*f;
    BUF			buf;

    dir.opendir("../rooms");

    while ((f = dir.readdir()))
    {
	buf.strcpy(f);
	if (buf.extensionMatch("map"))
	{
	    ROOMTYPE_NAMES		roomtype;

	    FOREACH_ROOMTYPE(roomtype)
	    {
		if (buf.startsWith(glb_roomtypedefs[roomtype].prefix))
		{
		    BUF		fname;
		    fname.sprintf("../rooms/%s", f);
		    theFragRooms[roomtype].append(new FRAGMENT(fname.buffer()));
		}
	    }
#if 1
	    // Let all unmatched become generic rooms
	    if (roomtype == NUM_ROOMTYPES)
	    {
		buf.sprintf("../rooms/%s", f);
		theFragRooms[ROOMTYPE_NONE].append(new FRAGMENT(buf.buffer()));
	    }
#endif
	}
    }

    // Create our generators
    ROOMTYPE_NAMES		roomtype;
    FOREACH_ROOMTYPE(roomtype)
    {
	if (roomtype == ROOMTYPE_PREBUILTSLICE)
	{
	    theFragRooms[roomtype].append(new FRAGMENT(100, 100));
	}
	else if (glb_roomtypedefs[roomtype].usegenerator)
	{
#if 0
	    if (roomtype == ROOMTYPE_CORRIDOR)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(17, 13));
		theFragRooms[roomtype].append(new FRAGMENT(16, 15));
		theFragRooms[roomtype].append(new FRAGMENT(15, 17));
	    }
	    else
	    {
		theFragRooms[roomtype].append(new FRAGMENT(13, 13));
		theFragRooms[roomtype].append(new FRAGMENT(12, 11));
		theFragRooms[roomtype].append(new FRAGMENT(11, 12));
	    }
#else
	    if (roomtype == ROOMTYPE_BIGROOM)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(60, 30));
	    }
	    else if (roomtype == ROOMTYPE_MAZE)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(45, 30));
	    }
	    else if (roomtype == ROOMTYPE_MAGE)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(22, 22));
	    }
	    else if (roomtype == ROOMTYPE_LABYRINTH)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(50, 50));
	    }
	    else if (roomtype == ROOMTYPE_PALACE)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(32, 32));
	    }
	    else if (roomtype == ROOMTYPE_CAVEROOMS)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(32, 32));
	    }
	    else if (roomtype == ROOMTYPE_ISLAND)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(1000, 1000));
	    }
	    else if (roomtype == ROOMTYPE_WILDERNESS)
	    {
		theFragRooms[roomtype].append(new FRAGMENT(100, 100));
	    }
	    else
	    {
		theFragRooms[roomtype].append(new FRAGMENT(60, 30));
	    }
#endif
	}
    }

    if (!theFragRooms[ROOMTYPE_NONE].entries())
    {
	cerr << "No .map files found in ../rooms" << endl;
	exit(-1);
    }
}

// Tracks what percentage we are done the item cache.
extern volatile int glbItemCache;

void
MAP::cacheItemPos()
{
    ITEMLIST		list;

#if 0
    getAllItems(list);

    for (int i = 0; i < list.entries(); i++)
    {
	glbItemCache = (int) (100 * (i / (float)list.entries()));
	buildDistMap(list(i)->pos());
    }

#endif
    glbItemCache = -1;
}

void
MAP::rebuildOnFire()
{
    setAllFlags(MAPFLAG_ONFIRE, false);
    processAllItems([&](ITEM *item)
    {
	if (!item->pos().valid()) return;

	if (item->getDefinition() == ITEM_CAMPFIRE)
	{
	    item->pos().markOnFire(true);
	}
    });
}

void
MAP::rebuildLighting()
{
    setAllFlags(MAPFLAG_TEMPLIT, false);

    // We stop showing camp fires quickly or we overwhelm...
    int		firecount = 0;

    processAllItems([&](ITEM *item)
    {
	if (!item->pos().valid()) return;

	if (item->getDefinition() == ITEM_CAMPFIRE)
	{
	    firecount++;
	    if (firecount > 5)
	    {
		// Only light if the fire isn't already lit,
		// this will then flicker the boundaries... a feature!
		// Note isLit will not light in daytime, but that
		// include twilight.
		if (rand_chance(firecount*10))
		{
		    if (item->pos().isDirectlyLit())
			return;
		}
	    }
	}

	int	light = item->lightEmission();
	if (light)
	    item->pos().emitLight(light);
    });
    for (auto && mob  : myLiveMobs)
    {
	if (!mob) continue;
	if (!mob->pos().valid()) continue;

	int	light = mob->lightEmission();

	if (light)
	    mob->pos().emitLight(light);
    }
}

void
MAP::rebuildFOV()
{
    // Track any fires.
    rebuildOnFire();
    // If we have a sleeeping avatar we don't rebuild the fov
    if (avatar() && avatar()->isWaiting() && myFOVCache)
    {
	return;
    }

    rebuildLighting();

    setAllFlags(MAPFLAG_FOV, false);
    setAllFlags(MAPFLAG_FOVCACHE, false);

    myMobsInFOV.clear();

    if (!avatar())
	return;

    bool	isblind = avatar()->isBlind();

    const int FOV_RAD = 26;

    myFOVCache = std::make_shared<SCRPOS>(avatar()->meditatePos(), FOV_RAD, FOV_RAD);
    POS			p;

    TCODMap		tcodmap(FOV_RAD*2+1, FOV_RAD*2+1);
    int			x, y;

    for (y = 0; y < FOV_RAD*2+1; y++)
    {
	for (x = 0; x < FOV_RAD*2+1; x++)
	{
	    p = myFOVCache->lookup(this, x-FOV_RAD, y - FOV_RAD);
	    p.setFlag(MAPFLAG_FOVCACHE, true);
	    // Force our own square to be visible.
	    if (x == FOV_RAD && y == FOV_RAD)
	    {
		tcodmap.setProperties(x, y, true, true);
	    }
	    else if (ABS(x-FOV_RAD) < 2 && ABS(y-FOV_RAD) < 2)
	    {
		if (p.defn().semitransparent)
		    tcodmap.setProperties(x, y, true, true);
		else
		    tcodmap.setProperties(x, y, p.defn().istransparent, p.isPassable());
	    }
	    else
	    {
		tcodmap.setProperties(x, y, p.defn().istransparent, p.isPassable());
	    }
	}
    }

    tcodmap.computeFov(FOV_RAD, FOV_RAD);

    bool		anyhostiles = false;

    for (y = 0; y < FOV_RAD*2+1; y++)
    {
	for (x = 0; x < FOV_RAD*2+1; x++)
	{
	    p = myFOVCache->lookup(this, x-FOV_RAD, y - FOV_RAD);
	    // We might see this square in more than one way.
	    // We don't want to mark ourselves invisible if
	    // there is someway that did see us.
	    // This will result in too large an FOV, but the "free" squares
	    // are those that you could deduce from what you already 
	    // can see, so I hope to be benign?
	    if (tcodmap.isInFov(x, y))
	    {
		p.setFlag(MAPFLAG_FOV, true);
		bool	isclose = false;
		// Only mark self if you are blind
		if (isblind)
		{
		    if (x == FOV_RAD && y == FOV_RAD)
		    {
			p.setFlag(MAPFLAG_MAPPED, true);
		    }
		}
		else if (p.isLit())
		    p.setFlag(MAPFLAG_MAPPED, true);
		else
		{
		    // You can figure out immediate surroundings.
		    if (MAX(ABS(x- FOV_RAD), ABS(y-FOV_RAD)) <= 1)
		    {
			isclose = true;
			p.setFlag(MAPFLAG_MAPPED, true);
		    }
		}

		if (p.mob())
		{
		    if ((isclose || p.isLit()) && !p.mob()->isFriends(avatar()))
		    {
			anyhostiles = true;
		    }
		    // This is slightly wrong as we could double
		    // count, but I'd argue that the rat seeing
		    // its portal rat honestly thinks it is an ally!
		    myMobsInFOV.indexAnywhere(p.mob()->getDefinition())++;
		}
	    }
	}
    }

    if (anyhostiles)
    {
	avatar()->stopWaiting("Sensing a foe, %S <end> %r rest.");
    }
}

void
MAP::clearFOV()
{
    myFOVCache.reset();
}

void
MAP::mergeFlags(MAPFLAG_NAMES flag, MAP *src)
{
    if (!src) return;

    for (int i = 0; i < myRooms.entries(); i++)
    {
	if (i >= src->myRooms.entries())
	    return;
	myRooms(i)->mergeFlags(flag, src->myRooms(i));
    }
}

bool
MAP::computeDelta(POS a, POS b, int &dx, int &dy)
{
    int		x1, y1, x2, y2;

    if (!myFOVCache)
	return false;

    // This early exits the linear search in myFOVCache find.
    if (!a.isFOVCache() || !b.isFOVCache())
	return false;

    if (!myFOVCache->find(a, x1, y1))
	return false;
    if (!myFOVCache->find(b, x2, y2))
	return false;

    dx = x2 - x1;
    dy = y2 - y1;

    POS		ap, at;

    // These are all in the coordinates of whoever built the scrpos,
    // ie, scrpos(0,0)
    //
    // This is a very confusing situation.
    // I think we have:
    // a - our source pos
    // @ - center of the FOV, myFOVCache->lookup(0,0)
    // a' - our source pos after walking from @, ie myFOVCache->lookup(x1,y1)
    //
    // dx, dy are in world space of @.  We want world space of a.
    // call foo.W the toWorld and foo.L the toLocal.
    // First apply the twist that @ got going to a'.
    // a'.W(@.L())
    // Next apply the transform into a.
    // a.W(a'.L())
    // Next we see a' cancels, leaving
    // a.W(@.L())
    //
    // Okay, that is bogus.  It is obvious it can't work as it
    // does not depend on a', and a' is the only way to include
    // the portal twist invoked in the walk!  I think I have the
    // twist backwards, ie, @.W(a'.L()), which means the compaction
    // doesn't occur.

    //
    // Try again.
    // @.L is needed to get into map space.
    // Then apply twist of @ -> a'   This is in Local coords!
    // So we just apply a.W
    // a.W(@.L(a'.W(@.L())))

    // Hmm.. 
    // ap encodes how to go from @ based world coords into
    // local a room map coords.  Thus ap.L gets us right there,
    // with only an a.W needed.

    ap = myFOVCache->lookup(this, x1, y1);
    // at = myFOVCache->lookup(0, 0);

    ap.rotateToLocal(dx, dy);
    a.rotateToWorld(dx, dy);

    return true;
}

void
MAP::buildReflexivePortal(POS pos, int dir)
{
    dir = (dir + 2);

    dir += pos.angle();

    dir %= 4;
    
    POS		backpos;

    ROOM	*home = findAtlasRoom(0, 0);

    if (home)
    {
	backpos = home->findUserProtoPortal(dir);
	buildUserPortal(backpos, 1, dir, 0);
    }
}

void
MAP::buildUserPortal(POS pos, int pnum, int dir, int avatardir)
{
    if (!pos.valid())
	return;

    if (!pnum)
    {
	buildReflexivePortal(pos, dir + avatardir);
    }

    if (myUserPortal[pnum].valid())
    {
	if (myUserPortal[0].valid() && myUserPortal[1].valid())
	{
	    // Both existing portals are wiped.
	    // Note the reversal of directins here!  This is because we
	    // have the coordinates of the landing zone and the portal
	    // only exists in the source room!
	    myUserPortal[1].room()->removePortal(myUserPortal[0]);
	    myUserPortal[0].room()->removePortal(myUserPortal[1]);
	}

	// Restore the wall, possibly trapping people :>
	if (myUserPortal[pnum].roomType() == ROOMTYPE_VILLAGE)
	    myUserPortal[pnum].setTile(TILE_USERPROTOPORTAL);
	else
	    myUserPortal[pnum].setTile(myUserPortal[pnum].roomDefn().wall_tile);
	myUserPortal[pnum] = POS();
    }

    // Construct our new portal...
    pos.setTile(pnum ? TILE_ORANGEPORTAL : TILE_BLUEPORTAL);
    pos.setFlag(MAPFLAG_PORTAL, true);
    myUserPortal[pnum] = pos;
    myUserPortalDir[pnum] = dir;

    // Build the link
    if (myUserPortal[0].valid() && myUserPortal[1].valid())
    {
	ROOM::buildPortal(myUserPortal[0], myUserPortalDir[0],
			  myUserPortal[1], myUserPortalDir[1],
			    false);	
    }
}

ITEM *
MAP::findItem(int uid) const
{
    if (uid < 0 || uid >= myItems.entries())
	return nullptr;

    return myItems(uid);
}

MOB *
MAP::findMob(int uid) const
{
    if (uid < 0 || uid >= myLiveMobs.entries())
	return 0;

    return myLiveMobs(uid);
}

MOB *
MAP::findMobByType(MOB_NAMES type) const
{
    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (myLiveMobs(i) && myLiveMobs(i)->getDefinition() == type)
	{
	    return myLiveMobs(i);
	}
    }

    return 0;
}


MOB *
MAP::findMobByLoc(ROOM *room, int x, int y) const
{
    if (!room)
	return nullptr;
    int roomid = room->getId();

    MOB	*swallowed = nullptr;

    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (!myLiveMobs(i))
	    continue;
	if (myLiveMobs(i)->pos().roomId() == roomid &&
	    myLiveMobs(i)->pos().myX == x &&
	    myLiveMobs(i)->pos().myY == y)
	{
	    if (myLiveMobs(i)->isSwallowed())
		swallowed = myLiveMobs(i);
	    else
		return myLiveMobs(i);
	}
    }

    return swallowed;
}

int
MAP::findAllMobsByLoc(MOBLIST &list, ROOM *room, int x, int y) const
{
    if (!room)
	return list.entries();
    int roomid = room->getId();

    for (int i = 0; i < myLiveMobs.entries(); i++)
    {
	if (!myLiveMobs(i))
	    continue;
	if (myLiveMobs(i)->pos().roomId() == roomid &&
	    myLiveMobs(i)->pos().myX == x &&
	    myLiveMobs(i)->pos().myY == y)
	{
	    list.append(myLiveMobs(i));
	}
    }

    return list.entries();
}

ITEM *
MAP::findItemByLoc(ROOM *room, int x, int y) const
{
    if (!room)
	return nullptr;
    int roomid = room->getId();

    ITEM	*bestitem = nullptr;

    processAllItems([&](ITEM *candidate)
    {
	if (candidate->pos().roomId() == roomid &&
	    candidate->pos().myX == x &&
	    candidate->pos().myY == y)
	{
	    if (!bestitem)
		bestitem = candidate;
	    else
	    {
		int cprior = candidate->defn().stackpriority;
		int bprior = bestitem->defn().stackpriority;
		if (cprior > bprior)
		    bestitem = candidate;
		else if (cprior < bprior)
		{ /* reject */ }
		else if (candidate->dropTime() > bestitem->dropTime())
		    bestitem = candidate;
	    }
	}
    });

    return bestitem;
}

int
MAP::findAllItemsByLoc(ITEMLIST &list, ROOM *room, int x, int y) const
{
    if (!room)
	return list.entries();
    int roomid = room->getId();

    for (int i = 0; i < myItems.entries(); i++)
    {
	if (!myItems(i))
	    continue;
	if (myItems(i)->pos().roomId() == roomid &&
	    myItems(i)->pos().myX == x &&
	    myItems(i)->pos().myY == y)
	{
	    list.append(myItems(i));
	}
    }

    return list.entries();
}
POS
MAP::findRoomOfType(ROOMTYPE_NAMES roomtype) const
{
    POS		result;
    int		nfound = 0;

    for (int i = 0; i < myRooms.entries(); i++)
    {
	if (myRooms(i)->type() == roomtype)
	{
	    nfound++;
	    if (!rand_choice(nfound))
	    {
		result = myRooms(i)->findCentralPos();
	    }
	}
    }
    return result;
}

POS
MAP::astralLoc() const
{
    ROOM *astral = findAtlasRoom(-1, -1);

    if (!astral)
	return POS();

    POS astralpos = astral->findCentralPos();

    return astral->spiralSearch(astralpos, 0, true);
}

ROOM *
MAP::findAtlasRoom(int x, int y) const
{
    for (int i = 0; i < myRooms.entries(); i++)
    {
	if (myRooms(i)->atlasX() == x && myRooms(i)->atlasY() == y)
	{
	    return myRooms(i);
	}
    }
    return 0;
}

void
MAP::save(ostream &os) const
{
    s32			val;
    int			i;

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

    val = myMobLetterTimeStamp;
    os.write((const char *)&val, sizeof(int));
    for (int i = 0; i < 26; i++)
    {
	val = myMobLastSeen[i];
	os.write((const char *)&val, sizeof(int));
    }

    val = GAMEDEF::getNumItem();
    os.write((const char *)&val, sizeof(s32));

    for (ITEM_NAMES item = ITEM_NONE; item < GAMEDEF::getNumItem();
	 item = (ITEM_NAMES)(item+1))
    {
	val = myItemIded.safeIndexAnywhere(item);
	os.write((const char *) &val, sizeof(s32));
	val = myItemMagicClass.safeIndexAnywhere(item);
	os.write((const char *) &val, sizeof(s32));
    }

    for (i = 0; i < 2; i++)
    {
	myUserPortal[i].save(os);
	val = myUserPortalDir[i];
	os.write((const char *) &val, sizeof(s32));
    }

    val = myRooms.entries();
    os.write((const char *) &val, sizeof(s32));
    for (i = 0; i < myRooms.entries(); i++)
    {
	myRooms(i)->save(os);
    }

    val = myItems.entries();
    os.write((const char *) &val, sizeof(s32));
    int		itemnumber = 0;
    for (itemnumber = 0; itemnumber < myItems.entries(); itemnumber++)
    {
	if (myItems(i))
	    break;
    }
    // Number of zero items:
    val = itemnumber;
    os.write((const char *) &val, sizeof(s32));
    // Now non zero.
    for (i = itemnumber; i < myItems.entries(); i++)
    {
	if (myItems(i))
	{
	    val = 1;
	    os.write((const char *) &val, sizeof(s32));
	    myItems(i)->save(os);
	}
	else
	{
	    val = 0;
	    os.write((const char *) &val, sizeof(s32));
	}
    }

    val = myLiveMobs.entries();
    os.write((const char *) &val, sizeof(s32));
    for (i = 0; i < myLiveMobs.entries(); i++)
    {

	if (!myLiveMobs(i))
	{
	    val = 0;
	    os.write((const char *) &val, sizeof(s32));
	}
	else
	{
	    val = 1;
	    os.write((const char *) &val, sizeof(s32));
	    myLiveMobs(i)->save(os);
	}
    }
}

MAP::MAP(istream &is)
{
    s32			val;
    int			i, n;

    myUniqueId = glbMapId.add(1);
    myFOVCache.reset();
    myAllowDelays = false;
    myVerse = nullptr;

    for (i = 0; i < DISTMAP_CACHE; i++)
    {
	myDistMapWeight[i] = 0.0;
	myDistMapCache[i].setMap(this);
    }

    is.read((char *) &val, sizeof(s32));
    mySpeed.setTime(val);

    is.read((char *) &val, sizeof(int));
    myMobLetterTimeStamp = val;
    for (int i = 0; i < 26; i++)
    {
	is.read((char *) &val, sizeof(int));
	myMobLastSeen[i] = val;
    }

    s32		total;
    is.read((char *) &total, sizeof(s32));

    for (int i = 0; i < total; i++)
    {
	is.read((char *) &val, sizeof(s32));
	myItemIded.indexAnywhere(i) = val ? true : false;
	is.read((char *) &val, sizeof(s32));
	myItemMagicClass.indexAnywhere(i) = val;
    }

    for (i = 0; i < 2; i++)
    {
	myUserPortal[i].load(is);
	myUserPortal[i].setMap(this);
	is.read((char *) &val, sizeof(s32));
	myUserPortalDir[i] = val;
    }

    is.read((char *) &val, sizeof(s32));
    n = val;
    for (i = 0; i < n; i++)
    {
	myRooms.append(ROOM::load(is));
	myRooms(i)->setMap(this);
    }

    is.read((char *) &val, sizeof(s32));
    n = val;
    is.read((char *) &val, sizeof(s32));
    int zeroprefix = val;
    for (i = 0; i < zeroprefix; i++)
	myItems.append(nullptr);
    for (i = zeroprefix; i < n; i++)
    {
	is.read((char *) &val, sizeof(s32));
	if (val)
	{
	    myItems.append(ITEM::load(this, is));
	    myItems.top()->setMap(this);
	}
	else
	    myItems.append(nullptr);
    }

    is.read((char *) &val, sizeof(s32));
    n = val;
    for (i = 0; i < n; i++)
    {
	is.read((char *) &val, sizeof(s32));
	if (val)
	{
	    myLiveMobs.append(MOB::load(this, is));
	    myLiveMobs.top()->setMap(this);
	}
	else
	    myLiveMobs.append(nullptr);
    }

    myAvatar = findAvatar();
}

int
MAP::getTime() const
{
    return mySpeed.time();
}

PHASE_NAMES
MAP::getPhase() const
{
    return mySpeed.phase();
}

int
MAP::getDaylight() const
{
    return mySpeed.daylight();
}

ATTR_NAMES
MAP::depthColor(int depth, bool invert)
{
    ATTR_NAMES		result = ATTR_NORMAL;

    if (depth < 5)
    {
	result = ATTR_WHITE;
    }
    else if (depth < 10)
    {
	result = ATTR_GREY;
    }
    else if (depth < 15)
    {
	result = ATTR_DKGREY;
    }
    else
    {
	result = ATTR_LIGHTBLACK;
    }

    return result;
}

static ATOMIC_INT32 glbVerseId;

MULTIVERSE::MULTIVERSE()
{
    myUniqueId = glbVerseId.add(1);
    myPhase = PHASE_NORMAL;
}

MULTIVERSE::~MULTIVERSE()
{
}

bool
MULTIVERSE::load()
{
    bool	 ok = false;

    // This disables loading, useful when the data format is changing.
    // return false;

    BUF		path;
    path.sprintf("../save/%s.sav", glbWorldName);

    // Did I mention my hatred of streams?
    {
#ifdef WIN32
	ifstream	is(path.buffer(), ios::in | ios::binary);
#else
	ifstream	is(path.buffer());
#endif

	if (!is)
	    return false;

	ITEM::loadGlobal(is);
	MOB::loadGlobal(is);

	myMap = std::make_shared<MAP>(is);
	myMap->setVerse(this);
	myPhase = myMap->getPhase();

	ok = true;
    }

    // Scope out the stream so we can have it unlocked to delete.
    MYunlink(path.buffer());

    return ok;
}

void
MULTIVERSE::save() const
{
    // Did I mention my hatred of streams?
    // Decades later it still burns bright.
    BUF		path;
    path.sprintf("../save/%s.sav", glbWorldName);
#ifdef WIN32
    ofstream	os(path.buffer(), ios::out | ios::binary);
#else
    ofstream	os(path.buffer());
#endif

    ITEM::saveGlobal(os);
    MOB::saveGlobal(os);

    J_ASSERT(map());
    map()->save(os);
}

float
MULTIVERSE::mapDensityRaw(POS pos) const
{
    if (!myMapsFromPos.size())
    {
	// No density engaged, so zero.
	return 0.0f;
    }

    if (!myMapsFromPos.count(pos))
	return 0.0f;

    int nummaps = 0;
    for (auto && kv : myMapsFromPos)
    {
	nummaps += kv.second.entries();
    }
    int nummap = myMapsFromPos.at(pos).entries();

    return (nummap) / (float)(nummaps);
}

float
MULTIVERSE::mapDensity(POS pos) const
{
    if (!myMapsFromPos.size())
    {
	// No density engaged, so zero.
	return 0.0f;
    }

    if (!myMapsFromPos.count(pos))
	return 0.0f;

    int nummap = myMapsFromPos.at(pos).entries();

    return (nummap) / (float)(glbConfig->myMaxMapsPerPos);
}

float
MULTIVERSE::mobDensity(POS pos) const
{
    if (!myMapsFromPos.size())
    {
	// No density engaged, so zero.
	return 0.0f;
    }

    int		nummap = 0, nummob = 0;
    for (auto && kv : myMapsFromPos)
    {
	for (auto && map : kv.second)
	{
	    nummap++;
	    pos.setMap(map.get());
	    if (pos.isFOV())
	    {
		MOB *mob = pos.mob();
		if (mob && mob != map->avatar())
		    nummob++;
	    }
	}
    }
    // Should be based on number of maps, but in practice they smear
    // so tighten it back...
    return (nummob) / (float)(glbConfig->myMaxMapsPerPos);
}

float
MULTIVERSE::mobDensityRaw(POS pos) const
{
    if (!myMapsFromPos.size())
    {
	// No density engaged, so zero.
	return 0.0f;
    }

    int		nummap = 0, nummob = 0;
    for (auto && kv : myMapsFromPos)
    {
	for (auto && map : kv.second)
	{
	    nummap++;
	    pos.setMap(map.get());
	    if (pos.isFOV())
	    {
		MOB *mob = pos.mob();
		if (mob && mob != map->avatar())
		    nummob++;
	    }
	}
    }
    return (nummob) / (float)(nummap);
}

float
MULTIVERSE::mobDensityGiven(POS pos, POS given) const
{
    if (!myMapsFromPos.count(given))
	return 0.0f;

    auto && maplist = myMapsFromPos.at(given);

    int		nummap = 0, nummob = 0;

    for (auto && map : maplist)
    {
	nummap++;
	pos.setMap(map.get());
	if (pos.isFOV())
	{
	    MOB *mob = pos.mob();
	    if (mob && mob != map->avatar())
		nummob++;
	}
    }
    return (nummob) / (float)(nummap);
}

void
MULTIVERSE::avatarHealth(POS pos, PTRLIST<float> &healths) const
{
    healths.resize(NUM_HEALTHLEVELS);
    HEALTHLEVEL_NAMES health = HEALTHLEVEL_DEAD;
    FOREACH_HEALTHLEVEL(health)
    {
	healths(health) = 0;
    }

    if (!myMapsFromPos.count(pos))
	return;

    auto && maplist = myMapsFromPos.at(pos);

    int		nummap = 0, nummob = 0;

    for (auto && map : maplist)
    {
	health = HEALTHLEVEL_DEAD;
	if (map->avatar())
	    health = map->avatar()->healthStatus();
	healths(health)++;
	nummap++;
    }
    FOREACH_HEALTHLEVEL(health)
    {
	healths(health) /= nummap;
    }
}


void			 
MULTIVERSE::uniqueMap()
{ 
    if (myMap.use_count() > 1)
    {
	myMap = std::make_shared<MAP>(*myMap);
	myMap->setVerse(this);
    }
    // This becomes a new world.
    myUniqueId = glbVerseId.add(1);
}

void
MULTIVERSE::appendMap(POS pos, MAP_PTR map, int maxmapperpos)
{
    int  &nummaps = myMapsAtPos[pos];
    auto &maplist = myMapsFromPos[pos];

    myPhase = map->getPhase();

    nummaps++;
    if (nummaps > maxmapperpos)
    {
	int	maploc = rand_choice(nummaps);
	if (maploc < maxmapperpos)
	{
	    // replace.  I think this will be too much risk
	    // for existing maps, but have not done the math :>
	    // This trick works for a single item pool, so I'm
	    // guessing it generalizes??
	    //
	    // So with 64 in, each current map had a 100% chance
	    // After this there is a 1/65 they got evicted, so they
	    // each have 64/65 of being there, and the new map has
	    // 64/65 of selecting an existing slot.
	    //
	    // So I think this works...
	    maplist.set(maploc, map);
	}
    }
    else
    {
	maplist.append(map);
    }
}

void
MULTIVERSE::cullWorlds(int maxworldperpos)
{
    for (auto && kv : myMapsFromPos)
    {
	while (kv.second.entries() > maxworldperpos)
	{
	    int		cull = rand_choice(kv.second.entries());
	    kv.second.swapEntries(cull, kv.second.entries()-1);
	    kv.second.pop();
	}
    }
}

void
MULTIVERSE::focusWorld(int x, int y, int roomid)
{
    if (!map()) return;

    POS	p = map()->buildPos(x, y, roomid);
    
    // Failure to find will restore old map.
    MAP_PTR newmap = myMap;

    if (!myMapsFromPos.count(p))
    {
	// Nothing to focus on, abandon.  Should be prevented earlier!
    }
    else
    {
	auto &maplist = myMapsFromPos.at(p);
	// Collapse to a random map..
	if (maplist.entries())
	{
	    newmap = maplist(rand_choice(maplist.entries()));
	}
    }

    if (newmap != myMap)
    {
	// Focus successful, merge in all other mapped flags!
	for (auto && kv : myMapsFromPos)
	{
	    for (auto && map : kv.second)
	    {
		newmap->mergeFlags(MAPFLAG_MAPPED, map.get());
	    }
	}
    }

    myMapsFromPos.clear();
    myMapsAtPos.clear();

    myMap = newmap;
    myPhase = myMap->getPhase();
}
