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

#include "item.h"
#include "gamedef.h"
#include "mob.h"

#include <assert.h>

#include <iostream>
using namespace std;

#include "grammar.h"
#include "text.h"
#include "engine.h"
#include "msg.h"
#include "thesaurus.h"
#include "level.h"

int glbUniqueId = 1;

static PTRLIST<bool> glbItemIded;
static PTRLIST<int> glbItemMagicClass;

struct COLOURPAIR
{
    ATTR_NAMES	attr;
    const char *name;
};

static const COLOURPAIR glbColors[] =
{
    { ATTR_WHITE, "white" },
    { ATTR_GOLD, "gold" },
    { ATTR_PINK, "pink" },
    { ATTR_PURPLE, "purple" },
    { ATTR_LIGHTBLACK, "midnight" },
    { ATTR_ORANGE, "orange" },
    { ATTR_LIGHTBROWN, "tan" },
    { ATTR_BROWN, "brown" },
    { ATTR_RED, "red" },
    { ATTR_DKRED, "blood" },
    { ATTR_GREEN, "green" },
    { ATTR_DKGREEN, "olive" },
    { ATTR_BLUE, "blue" },
    { ATTR_TEAL, "teal" },
    { ATTR_CYAN, "cyan" },
    { ATTR_GREY, "grey" },
    { ATTR_FIRE, "glowing" },
    { ATTR_NONE, 0 }
};

MATERIAL_NAMES
item_randmaterial(int depth, bool gilt)
{
    MATERIAL_NAMES		choice = MATERIAL_NONE;
    int				nvalid = 0;

    for (MATERIAL_NAMES material = MATERIAL_NONE; material < (MATERIAL_NAMES) GAMEDEF::getNumMaterial(); material = (MATERIAL_NAMES) (material+1))
    {
	if (material == MATERIAL_NONE)
	    continue;
	if (gilt && !GAMEDEF::materialdef(material)->gildable)
	    continue;
	if (GAMEDEF::materialdef(material)->depth <= depth)
	{
	    nvalid++;
	    if (!rand_choice(nvalid))
	    {
		choice = material;
	    }
	}
    }

    return choice;
}

int 
glb_allocUID()
{
    return glbUniqueId++;
}

void
glb_reportUID(int uid)
{
    if (uid >= glbUniqueId)
	glbUniqueId = uid+1;
}


//
// Item Definitions
//

ITEM::ITEM()
{
    myDefinition = (ITEM_NAMES) 0;
    myCount = 1;
    myTimer = -1;
    myUID = INVALID_UID;
    myBroken = false;
    myEquipped = false;
    myWielded = false;
    myMobType = MOB_NONE;
    myElement = ELEMENT_NONE;
    myMaterial = myGiltMaterial = MATERIAL_NONE;
    myBonus = 0;
    myBonusKnown = false;
    myDuped = false;
    myStartingItem = false;
    myLetter = 0;

    WORLD_NAMES	world;
    FOREACH_WORLD(world)
    {
	myWorldStash[world] = nullptr;
    }
}

ITEM::~ITEM()
{
    pos().removeItem(this);

    WORLD_NAMES	world;
    FOREACH_WORLD(world)
    {
	delete myWorldStash[world];
    }
}

void
ITEM::move(POS pos)
{
    myPos.removeItem(this);
    myPos = pos;
    myPos.addItem(this);
}

void
ITEM::clearAllPos()
{
    myPos = POS();
}

ITEM *
ITEM::copy() const
{
    ITEM *item;

    item = new ITEM();

    *item = *this;

    // Copy world items.
    WORLD_NAMES world;
    FOREACH_WORLD(world)
    {
	if (item->myWorldStash[world])
	    item->myWorldStash[world] = myWorldStash[world]->copy();
    }

    return item;
}

ITEM *
ITEM::createCopy() const
{
    ITEM *item;

    item = copy();

    item->myUID = glb_allocUID();

    return item;
}

void
ITEM::transferToWorld(WORLD_NAMES curworld, MAP *dest)
{
    WORLD_NAMES		newworld = dest->world();

    POS	p = pos();
    if (p.valid())
    {
        J_ASSERT(p.world() == curworld);
    }
    POS	npos = dest->findBijectivePos(p);
    p.removeItem(this);

    myPos = npos;

    // Update any internal map pointers:
    setMap(dest);

    // If world changed and polymorphic, polymorph!
    if (curworld != newworld &&
	myWorldStash[curworld] &&
	myWorldStash[newworld])
    {
	stashInto(myWorldStash[curworld]);
	myWorldStash[newworld]->stashInto(this);
    }

    // We can't just do setPos because that will do moveMob, but
    // we must actually add/remove.
    if (npos.tile() == TILE_ALTAR)
    {
        // Duplicate...
        POSLIST altars;
        npos.getAllRoomTiles(TILE_ALTAR, altars);
        for (auto && altar : altars)
        {
            // ignore canon mapping
            if (altar == npos)
                continue;

            ITEM *dup = createCopy();
            dup->markDuped(true);
            dup->myPos = altar;
            altar.addItem(dup);
        }
    }
    // Add to the new map
    // This may delete ourself!
    npos.addItem(this);
}

void
ITEM::stashInto(ITEM *dst)
{
    dst->myDefinition = myDefinition;
    dst->myElement = myElement;
    dst->myMobType = myMobType;
    // No pos
    dst->myCount = myCount;
    dst->myTimer = myTimer;
    // No uid
    dst->myBroken = myBroken;
    dst->myEquipped = myEquipped;
    dst->myWielded = myWielded;
    dst->myBonusKnown = myBonusKnown;
    dst->myDuped = myDuped;
    dst->myStartingItem = myStartingItem;
    dst->mySeed = mySeed;
    dst->myBonus = myBonus;
    // No letter
    dst->myMaterial = myMaterial;
    dst->myGiltMaterial = myGiltMaterial;
}

void
ITEM::fixWorldState()
{
    WORLD_NAMES world;
    FOREACH_WORLD(world)
    {
	delete myWorldStash[world];
	myWorldStash[world] = nullptr;
    }
}

ITEM *
ITEM::createMacGuffin(WORLD_NAMES baseworld)
{
    ITEM		*i;

    i = ITEM::create(glb_worlddefs[baseworld].macguffin, 1);
    WORLD_NAMES 	world;
    FOREACH_WORLD(world)
    {
	i->myWorldStash[world] = ITEM::create(glb_worlddefs[world].macguffin, 1);
    }
    return i;
}

ITEM *
ITEM::create(ITEM_NAMES item, int depth)
{
    ITEM	*i;

    J_ASSERT(item >= ITEM_NONE && item < GAMEDEF::getNumItem());

    i = new ITEM();

    i->myDefinition = item;
    i->myTimer = i->defn().timer;
    i->setStackCount(i->defn().startstack.roll());

    i->myUID = glb_allocUID();
    i->mySeed = rand_int();

    if (i->defn().startsbroken)
	i->myBroken = true;

    if (i->defn().needsmaterial)
    {
	i->myMaterial = item_randmaterial(depth, false);
	if (i->defn().gildable)
	{
	    if (!rand_choice(3))
	    {
		i->myGiltMaterial = item_randmaterial(depth, true);
	    }
	}
    }

    if (i->isRing() || i->isRanged() || i->isWeapon() || i->isArmour())
    {
	int itemlevel = 0;
	while (rand_chance(10))
	    itemlevel++;
	while (rand_chance(5))
	    itemlevel--;
	i->setBonus(itemlevel);
    }
    else
    {
	// no secret for these items and it ensures they stack after
	// identification.
	i->markBonusKnown();
    }

    return i;
}

ITEM *
ITEM::createItemClass(ITEMCLASS_NAMES itemclass, int depth, WORLD_NAMES world)
{
    ITEM_NAMES item = chooseItemClass(itemclass, depth, world);
    if (item == ITEM_NONE) return nullptr;
    return create(item, depth);
}

ITEM *
ITEM::createMultiWorldRandomBalanced(int depth, WORLD_NAMES curworld)
{
    ITEM_NAMES		basetype = chooseRandomBalanced(depth, curworld);

    if (basetype == ITEM_NONE) return nullptr;

    ITEM		*item = create(basetype, depth);
    if (!item)
	return item;

    WORLD_NAMES world;
    FOREACH_WORLD(world)
    {
	if (world != curworld)
	{
	    ITEM_NAMES itemtype = chooseRandomBalanced(depth, world);
	    if (itemtype != ITEM_NONE)
		item->myWorldStash[world] = create(itemtype, world);
	}
	else
	{
	    // This will be wiped on first stash so is irrelevant.
	    item->myWorldStash[world] = create(basetype);
	}

	// If it failed, fail.
	if (!item->myWorldStash[world])
	{
	    delete item;
	    return nullptr;
	}
    }

    return item;
}

ITEM_NAMES
ITEM::chooseRandomBalanced(int depth, WORLD_NAMES world)
{
    // Decide on type
    ITEMCLASS_NAMES	choices[] =
    {
	ITEMCLASS_MELEEWEAPON,
	ITEMCLASS_RANGEWEAPON,
	ITEMCLASS_AMMO,
	ITEMCLASS_POTION,
	ITEMCLASS_ARMOUR,
	ITEMCLASS_SPELLBOOK,
	ITEMCLASS_SCROLL,
	ITEMCLASS_RING,
	ITEMCLASS_FOOD,
	ITEMCLASS_TOOL,
	ITEMCLASS_FURNITURE,
        ITEMCLASS_TREASURE,
    };

    ITEMCLASS_NAMES	itemclass = choices[0];
    int choice = 0;
    for (int i = 0; i < sizeof(choices)/sizeof(ITEMCLASS_NAMES); i++)
    {
        int rarity = GAMEDEF::itemclassdef(choices[i])->rarity;
	if (rand_choice(choice + rarity) < rarity)
	{
	    itemclass = choices[i];
	}
	choice += rarity;
    }

    return chooseItemClass(itemclass, depth, world);
}


ITEM_NAMES
ITEM::chooseItemClass(ITEMCLASS_NAMES itemclass, int depth, WORLD_NAMES world)
{
    int		matchingdepth = 0;
    int choice = 0;
    ITEM_NAMES	itemname = ITEM_NONE;

    for (ITEM_NAMES i = ITEM_NONE; i < (ITEM_NAMES) GAMEDEF::getNumItem(); i = (ITEM_NAMES) (i+1))
    {
	// Ignore mismatching.
	if (!ITEM::getDepth(i))
	    continue;

	if (defn(i).itemclass != itemclass)
	    continue;

	if (!MAP::worldListContains(defn(i).worlds, world))
	    continue;

	if (ITEM::getDepth(i) <= depth)
	{
	    if (ITEM::getDepth(i) == depth)
		matchingdepth++;

	    if (rand_choice(choice + defn(i).rarity) < defn(i).rarity)
		itemname = i;
	    choice += defn(i).rarity;
	}
    }

    return itemname;
}

ITEM *
ITEM::createRandom(int depth)
{
    ITEM_NAMES		item;

    item = itemFromHash(rand_choice(65536*32767), depth);
    if (item == ITEM_NONE)
	return 0;
    return create(item, depth);
}

ITEM_NAMES
ITEM::itemFromHash(unsigned int hash, int depth)
{
    int		totalrarity, rarity;
    ITEM_NAMES	i;

    // Find total rarity of items
    totalrarity = 0;
    for (i = ITEM_NONE; i < GAMEDEF::getNumItem(); i = (ITEM_NAMES)(i+1))
    {
	rarity = defn(i).rarity;
	if (ITEM::getDepth(i) > depth)
	    rarity = 0;
	totalrarity += rarity;
    }

    hash %= totalrarity;
    for (i = ITEM_NONE; i < GAMEDEF::getNumItem(); i = (ITEM_NAMES)(i+1))
    {
	rarity = defn(i).rarity;
	if (ITEM::getDepth(i) > depth)
	    rarity = 0;

	if (hash > (unsigned) rarity)
	    hash -= rarity;
	else
	    break;
    }
    return i;
}

static ITEM_NAMES 
item_searchbyclass(ITEMCLASS_NAMES itemclass, int magicclass)
{
    ITEM_NAMES	item;
    FOREACH_ITEM(item)
    {
	if (ITEM::defn(item).itemclass == itemclass &&
	    ITEM::getMagicClass(item) == magicclass)
	    return item;
    }
    return ITEM_NONE;
}

POTION_NAMES
ITEM::asPotion(ITEM_NAMES item)
{
    if (defn(item).itemclass == ITEMCLASS_POTION)
        return (POTION_NAMES) getMagicClass(item);
    return POTION_NONE;
}

SPELL_NAMES
ITEM::asSpell(ITEM_NAMES item)
{
    if (defn(item).itemclass == ITEMCLASS_SPELLBOOK)
        return (SPELL_NAMES) getMagicClass(item);
    return SPELL_NONE;
}

SCROLL_NAMES
ITEM::asScroll(ITEM_NAMES item)
{
    if (defn(item).itemclass == ITEMCLASS_SCROLL)
        return (SCROLL_NAMES) getMagicClass(item);
    return SCROLL_NONE;
}

RING_NAMES
ITEM::asRing(ITEM_NAMES item)
{
    if (defn(item).itemclass == ITEMCLASS_RING)
        return (RING_NAMES) getMagicClass(item);
    return RING_NONE;
}

ITEM_NAMES
ITEM::lookupPotion(POTION_NAMES magicclass)
{
    return item_searchbyclass(ITEMCLASS_POTION, magicclass);
}

ITEM_NAMES
ITEM::lookupRing(RING_NAMES magicclass)
{
    return item_searchbyclass(ITEMCLASS_RING, magicclass);
}

ITEM_NAMES
ITEM::lookupSpell(SPELL_NAMES magicclass)
{
    return item_searchbyclass(ITEMCLASS_SPELLBOOK, magicclass);
}

ITEM_NAMES
ITEM::lookupScroll(SCROLL_NAMES magicclass)
{
    return item_searchbyclass(ITEMCLASS_SCROLL, magicclass);
}

int
ITEM::getMagicClass() const
{
    return getMagicClass(getDefinition());
}

int
ITEM::getMagicClass(ITEM_NAMES item)
{
    return glbItemMagicClass.safeIndexAnywhere(item);
}


bool
ITEM::isMagicClassKnown() const
{
    return glbItemIded.safeIndexAnywhere(getDefinition());
}

void
ITEM::markMagicClassKnown(bool silent)
{
    // Only mark stuff that has two layers..
    if (!isPotion() && !isSpellbook() && !isScroll() && !isRing())
	return;

    if (!glbItemIded.indexAnywhere(getDefinition()))
    {
	BUF		origname;
	origname = getSingleArticleName();
	glbItemIded.indexAnywhere(getDefinition()) = true;
	BUF		msgtext, newname;
	newname = getSingleArticleName();
	msgtext.sprintf("You discover that %s is %s.", origname.buffer(), newname.buffer());
        if (!silent)
            msg_report(msgtext);
    }
}

VERB_PERSON
ITEM::getPerson() const
{
    if (getStackCount() > 1)
	return VERB_THEY;
    // See if raw name is plural, like "robes".
    if (gram_isnameplural(getRawName()))
	return VERB_THEY;

    return VERB_IT;
}

BUF
ITEM::getSingleName() const
{
    BUF		buf, basename;

    basename = getRawName();

    if (mobType() != MOB_NONE)
    {
	const char *mobname = GAMEDEF::mobdef(mobType())->name;
	// Special case your body.
	//if (!strcmp(mobname, "you"))
	//    mobname = "your";
	if (!strcmp(mobname, "you"))
	    mobname = glb_roledefs[GAMEDEF::mobdef(mobType())->role].name;

	buf.sprintf("%s %s", mobname, basename.buffer());
	basename = buf;
    }
    if (element() != ELEMENT_NONE)
    {
	const char *elementname = GAMEDEF::elementdef(element())->name;
	buf.sprintf("%s %s", basename.buffer(), elementname);
	basename = buf;
    }
    if (defn().thesaurus)
    {
	basename = thesaurus_expand(mySeed, getDefinition(), material(), giltMaterial());
    }
    else
    {
	if (material() != MATERIAL_NONE)
	{
	    const char *materialname = GAMEDEF::materialdef(material())->adjname;
	    buf.sprintf("%s %s", materialname, basename.buffer());
	    basename = buf;
	}
	if (giltMaterial() != MATERIAL_NONE)
	{
	    const char *materialname = GAMEDEF::materialdef(giltMaterial())->name;
	    buf.sprintf("%s-chased %s", materialname, basename.buffer());
	    basename = buf;
	}
    }

    if (getTimer() >= 0)
    {
	// A timed item...  We are off by one in the time we expire
	// so thus the extra 1 in the print out.
	buf.sprintf("%s (%d)", basename.buffer(), getTimer()+1);
    }
    else
    {
	// A normal item.
	buf = basename;
    }
    if (getBonus() && isBonusKnown())
    {
	BUF		bonus;
	bonus.sprintf("%+d %s", getBonus(), buf.buffer());
	buf.strcpy(bonus);
    }
    if (isBroken())
    {
	BUF		broken;
	broken.sprintf("broken %s", buf.buffer());
	buf.strcpy(broken);
    }

    return buf;
}

BUF
ITEM::getName() const
{
    BUF		buf = getSingleName();
    return gram_createcount(buf, myCount, false);
}

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

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

    return result;
}

BUF
ITEM::getSingleArticleName() const
{
    BUF		name = getSingleName();
    BUF		result;

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

    return result;
}

BUF
ITEM::getRawName() const
{
    BUF		buf;

    buf.reference(defn().name);

    if (getDefinition() == ITEM_CORPSE)
    {
	// This is so we can have a different text message when looking
	// at friendly corpses, etc.
	// But maids are technically friendly but if you kill them you
	// are thinking otherwise, so restrict to your party members.
	if (MOB::defn(mobType()).isavatarform)
	    buf.reference("body");
    }

    if (isSpellbook())
    {
	if (isMagicClassKnown())
	{
	    buf.sprintf("spellbook of %s", GAMEDEF::spelldef(asSpell())->name);
	}
    }
    if (isScroll())
    {
	if (isMagicClassKnown())
	{
	    buf.reference(GAMEDEF::scrolldef(asScroll())->name);
	}
    }
    if (isPotion())
    {
	if (isMagicClassKnown())
	{
	    buf.reference(GAMEDEF::potiondef(asPotion())->name);
	}
    }
    if (isRing())
    {
	if (isMagicClassKnown())
	{
	    buf.reference(GAMEDEF::ringdef(asRing())->name);
	}
    }

    return buf;
}

BUF
ITEM::buildList(const ITEMLIST &items, bool *isplural)
{
    BUF         list;

    if (items.entries() > 1 && isplural)
        *isplural = true;
    for (int i = 0; i < items.entries(); i++)
    {
        // foo
        // foo and bar
        // foo, bar, and quux
        if (i)
        {
            if (items.entries() > 2)
                list.strcat(",");
            list.strcat(" ");
        }
        if (i && i == items.entries()-1)
            list.strcat("and ");
        BUF     name = items(i)->getArticleName();
        if (isplural)
            *isplural |= gram_isnameplural(name);
        list.strcat(name);
    }
    return list;
}

const char *
item_BuildChanceString(int chance)
{
    switch (chance)
    {
	case -3:
	    return "It is exceptionally awkward to aim.  ";
	    break;
	case -2:
	    return "It is very awkward to aim.  ";
	    break;
	case -1:
	    return "It is awkward to aim.  ";
	    break;
	case 0:
	    return "";
	    break;
	case 1:
	    return "It is easy to aim.  ";
	    break;
	case 2:
	    return "It is very easy to aim.  ";
	    break;
	case 3:
	    return "It is exceptionally easy to aim.  ";
	    break;
    }

    if (chance < 0)
	return "It is almost impossible to aim right.";

    return "It seems to seek its target of its own will.";
}

BUF
ITEM::getDetailedDescription() const
{
    BUF		result, buf;

    result.reference("");

    if (getMeleeAttack() != ATTACK_NONE)
    {
	buf.sprintf("You can %s with %s%s for %s %s damage.  ",
		attackdefn().verb,
		gram_getarticle(getName()),
		getName().buffer(),
		attackdefn().damage.format(getKnownBonus()).buffer(),
		GAMEDEF::damageclassdef(attackdefn().damageclass)->name);
	result.strcat(buf);

	result.strcat(item_BuildChanceString(attackdefn().chancebonus+getKnownBonus()));
    }
    if (defn().throwable)
    {
	result.strcat("You can throw this effectively.  ");
    }
    if (isRanged())
    {
	buf.sprintf("You can %s at a range %d with this for %s %s damage.  ",
		rangedefn().verb,
		getRangeRange(),
		rangedefn().damage.format(getKnownBonus()).buffer(),
		GAMEDEF::damageclassdef(rangedefn().damageclass)->name);
	result.strcat(buf);
	if (defn().ammo != ITEM_NONE)
	{
	    buf.sprintf("It uses %s for ammo.  ",
		    GAMEDEF::itemdef(defn().ammo)->name);
	    result.strcat(buf);
	}
	result.strcat(item_BuildChanceString(rangedefn().chancebonus+getKnownBonus()));
    }
    if (result.isstring())
	result.strcat("\n");
    if (isArmour())
    {
	// We are careful to hide bonus here unless ided!
	buf.sprintf("It provides %+d blunt rotection, "
		    "%+d slash protection, "
		    "%+d pierce protection, "
		    "%+d splash protection, "
		    "and "
		    "%+d dodge bonus.\n",
		    getBaseArmourClass(DAMAGECLASS_BLUNT) + getKnownBonus(),
		    getBaseArmourClass(DAMAGECLASS_SLASH) + getKnownBonus(),
		    getBaseArmourClass(DAMAGECLASS_PIERCE) + getKnownBonus(),
		    getBaseArmourClass(DAMAGECLASS_SPLASH) + getKnownBonus(),
		    getBaseArmourClass(DAMAGECLASS_DODGE) + getKnownBonus());
	result.strcat(buf);
    }

    if (isSpellbook() && isMagicClassKnown())
    {
	// Find corresponding spell
	SPELL_NAMES spell = asSpell();
	BUF		buf = MOB::buildSpellDescr(spell);
	result.strcat(buf);
    }

    if (isRing() && isMagicClassKnown())
    {
	RING_NAMES	ring = asRing();

	if (GAMEDEF::ringdef(ring)->resist != ELEMENT_NONE)
	{
	    BUF		capelement;
	    capelement = gram_capitalize(GAMEDEF::elementdef(GAMEDEF::ringdef(ring)->resist)->name);
	    int		ramt = GAMEDEF::ringdef(ring)->resist_amt;
	    ramt += getKnownBonus();
	    if (ramt < 0)
		buf.sprintf("%s Vulnerable: %d\n", capelement.buffer(), -ramt);
	    else
		buf.sprintf("%s Resist: %d\n", capelement.buffer(), ramt);
	    result.strcat(buf);
	}
	if (GAMEDEF::ringdef(ring)->deflect != 0)
	{
	    int		amt = GAMEDEF::ringdef(ring)->deflect + getKnownBonus();
	    buf.sprintf("Deflection: %d\n", amt);
	    result.strcat(buf);
	}
    }
    return result;
}

BUF
ITEM::getLongDescription() const
{
    BUF		descr, detail;

    descr = gram_capitalize(getName());
    detail = getDetailedDescription();
    descr.append('\n');
    if (detail.isstring())
    {
	descr.append('\n');
	descr.strcat(detail);
    }
    const char *elementname = GAMEDEF::elementdef(element())->name;
    text_registervariable("ELEMENT", elementname);

    detail = text_lookup("item", getRawName());
    if (detail.isstring() && !detail.startsWith("Missing text entry: "))
    {
	descr.append('\n');
	descr.strcat(detail);
    }
    else
    {
	descr.append('\n');
    }

    // Append the non-magic version
    if (isMagicClassKnown() && (isPotion() || isRing() || isSpellbook() || isScroll()))
    {
	descr.strcat("Base Type: ");
	detail = gram_capitalize(defn().name);
	detail.append('\n');
	descr.strcat(detail);

	detail = text_lookup("item", defn().name);
	if (detail.isstring() && !detail.startsWith("Missing text entry: "))
	{
	    descr.append('\n');
	    descr.strcat(detail);
	}
	else
	{
	    descr.append('\n');
	}
    }

    return descr;
}

int
ITEM::breakChance(MOB_NAMES victim) const
{
    int		chance = 0;

    // Can't get blood from a stone.
    if (isBroken())
	return 0;

    // Only can break weapons.
    chance = defn().breakchance;

    // If we are enchanted positively, we must first fail the
    // bonus effect...
    if (getBonus() > 0)
    {
	if (rand_choice(getBonus()))
	    chance = 0;
    }

    return chance;
}

int
ITEM::getTotalArmourClass() const
{
    int		total = 0;
    DAMAGECLASS_NAMES damageclass;
    FOREACH_DAMAGECLASS(damageclass)
    {
	total += getArmourClass(damageclass);
    }
    return total;
}

int
ITEM::getBaseArmourClass(DAMAGECLASS_NAMES damageclass) const
{
    if (!isArmour())
	return 0;

    switch (damageclass)
    {
	case DAMAGECLASS_BLUNT:
	    return defn().ac_blunt;
	case DAMAGECLASS_SLASH:
	    return defn().ac_slash;
	case DAMAGECLASS_PIERCE:
	    return defn().ac_pierce;
	case DAMAGECLASS_SPLASH:
	    return defn().ac_splash;
	case NUM_DAMAGECLASSS:
	case DAMAGECLASS_BYPASS:
	    return 0;
    }

    return 0;
}

ATTACK_NAMES
ITEM::getRangeAttack() const
{
    if (defn().range_attack == ATTACK_NONE)
	return ATTACK_NONE;

    if (isBroken())
    {
	// Bad melee attack.
	return ATTACK_BROKEN;
    }

    return (ATTACK_NAMES) defn().range_attack;
}

ATTACK_NAMES
ITEM::getMeleeAttack() const
{
    if (defn().melee_attack == ATTACK_NONE)
	return ATTACK_NONE;

    if (isBroken())
    {
	// Bad melee attack.
	return ATTACK_BROKEN;
    }

    return (ATTACK_NAMES) defn().melee_attack;
}

int
ITEM::getFoodVal() const
{
    if (getDefinition() != ITEM_CORPSE)
	return defn().foodval;
    // Note this must match the value of meatchunks!
    return glb_sizedefs[size()].weight * 20;
}

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

    if (material() != MATERIAL_NONE)
    {
	attr = (ATTR_NAMES) GAMEDEF::materialdef(material())->attr;
    }
}

bool
ITEM::canStackWith(const ITEM *stack) const
{
    if (getDefinition() != stack->getDefinition())
	return false;

    if (material() != stack->material())
	return false;
    if (giltMaterial() != stack->giltMaterial())
	return false;
    if (element() != stack->element())
	return false;
    if (getBonus() != stack->getBonus())
	return false;
    if (isBonusKnown() != stack->isBonusKnown())
	return false;
    
    if (mobType() != stack->mobType())
	return false;

    if (isBroken() != stack->isBroken())
	return false;

    // Disable stacking of held/worn items.
    if (isEquipped() || stack->isEquipped())
	return false;
    if (isWielded() || stack->isWielded())
	return false;

    // Disable stacking of weapons/books/armour as we want
    // to be able to delete them easily
    if (defn().unstackable)
    {
        // Allow stacking of duped items even if they are normally
        // not stackable as they can compress.
        if (isDuped() || stack->isDuped())
        {
        }
        else
        {
            return false;
        }
    }

    // No reason why not...
    return true;
}

void
ITEM::combineItem(const ITEM *item)
{
    // If either is duped, the combination is a no-op,
    // ie, the source will be effectively lost.
    if (isDuped() || item->isDuped())
        return;

    // Untimed items stack.
    if (getTimer() < 0)
	myCount += item->getStackCount();
    // Timed items add up charges.
    if (getTimer() >= 0)
    {
	J_ASSERT(item->getTimer() >= 0);
	myTimer += item->getTimer();
    }

    J_ASSERT(myCount >= 0);
    if (myCount < 0)
	myCount = 0;
}

bool
ITEM::runHeartbeat(MOB *owner)
{
    if (myTimer >= 0)
    {
	if (!myTimer)
	    return true;
	myTimer--;
    }
    if (getDefinition() == ITEM_CORPSE &&
	GAMEDEF::mobdef(mobType())->isregenerate)
    {
	// A more mean person would roll once per stack... :>
	if (rand_chance(5))
	{
	    POS		np;
	    if (owner)
		np = owner->pos().randAdjacent();
	    else
		np = pos();
	    if (np.valid() && !np.mob())
	    {
		MOB		*reborn = MOB::create(mobType(), false, np.world());

		reborn->move(np);
		if (owner)
		{
		    owner->formatAndReport("%O escapes %r backpack!", reborn);
		}
		else
		{
		    reborn->formatAndReport("%S <rise> from the dead!");
		}
		if (getStackCount() > 1)
		    setStackCount(getStackCount()-1);
		else
		    return true;
	    }
	}
    }
	
    return false;
}

int
ITEM::getDepth(ITEM_NAMES item)
{
    switch (defn(item).itemclass)
    {
	case ITEMCLASS_SPELLBOOK:
	{
	    auto spell = asSpell(item);
	    return glb_spelldefs[spell].difficulty;
	}
    }

    return defn(item).depth;
}

void
ITEM::initSystem()
{
    glbItemIded.clear();
    glbItemMagicClass.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 (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;
	    }
	    glbItemMagicClass.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 (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;
	    }
	    glbItemMagicClass.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 (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;
	    }
	    glbItemMagicClass.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 (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;
	    }
	    glbItemMagicClass.indexAnywhere(scrollclasses[scroll]) = scroll;
	}
    }
}

void
ITEM::saveGlobal(ostream &os)
{
    s32		val;

    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 = glbItemIded.safeIndexAnywhere(item);
	os.write((const char *) &val, sizeof(s32));
	val = glbItemMagicClass.safeIndexAnywhere(item);
	os.write((const char *) &val, sizeof(s32));
    }
}

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

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

    myPos.save(os);

    os.write((const char *) &myCount, sizeof(int));
    os.write((const char *) &myTimer, sizeof(int));
    os.write((const char *) &myUID, sizeof(int));
    os.write((const char *) &myBroken, sizeof(bool));
    os.write((const char *) &myEquipped, sizeof(bool));
    os.write((const char *) &myWielded, sizeof(bool));
    val = myMobType;
    os.write((const char *) &val, sizeof(s32));
    val = myElement;
    os.write((const char *) &val, sizeof(s32));
    val = myMaterial;
    os.write((const char *) &val, sizeof(s32));
    val = myGiltMaterial;
    os.write((const char *) &val, sizeof(s32));
    val = mySeed;
    os.write((const char *) &val, sizeof(s32));
    val = myBonus;
    os.write((const char *) &val, sizeof(s32));
    val = myBonusKnown;
    os.write((const char *) &val, sizeof(s32));
    val = myDuped;
    os.write((const char *) &val, sizeof(s32));
    val = myStartingItem;
    os.write((const char *) &val, sizeof(s32));
    val = myLetter;
    os.write((const char *) &val, sizeof(s32));

    WORLD_NAMES		world;
    FOREACH_WORLD(world)
    {
	if (myWorldStash[world])
	{
	    c = 1;
	    os.write((const char *) &c, 1);
	    myWorldStash[world]->save(os);
	}
	else
	{
	    c = 0;
	    os.write((const char *) &c, 1);
	}
    }
}

ITEM *
ITEM::load(istream &is)
{
    s32		val;
    u8		c;
    ITEM	*i;

    i = new ITEM();

    is.read((char *)&val, sizeof(s32));
    i->myDefinition = (ITEM_NAMES) val;

    i->myPos.load(is);

    is.read((char *)&i->myCount, sizeof(int));
    is.read((char *)&i->myTimer, sizeof(int));

    is.read((char *)&i->myUID, sizeof(int));
    glb_reportUID(i->myUID);
    is.read((char *)&i->myBroken, sizeof(bool));
    is.read((char *)&i->myEquipped, sizeof(bool));
    is.read((char *)&i->myWielded, sizeof(bool));
    is.read((char *)&val, sizeof(s32));
    i->myMobType = (MOB_NAMES) val;
    is.read((char *)&val, sizeof(s32));
    i->myElement = (ELEMENT_NAMES) val;
    is.read((char *)&val, sizeof(s32));
    i->myMaterial = (MATERIAL_NAMES) val;
    is.read((char *)&val, sizeof(s32));
    i->myGiltMaterial = (MATERIAL_NAMES) val;
    is.read((char *)&val, sizeof(s32));
    i->mySeed = val;
    is.read((char *)&val, sizeof(s32));
    i->myBonus = val;
    is.read((char *)&val, sizeof(s32));
    i->myBonusKnown = val;
    is.read((char *)&val, sizeof(s32));
    i->myDuped = val;
    is.read((char *)&val, sizeof(s32));
    i->myStartingItem = val;
    is.read((char *)&val, sizeof(s32));
    i->myLetter = val;

    WORLD_NAMES		world;
    FOREACH_WORLD(world)
    {
	is.read((char *)&c, 1);
	if (c)
	{
	    c = 1;
	    i->myWorldStash[world] = ITEM::load(is);
	}
	else
	{
	    c = 0;
	    i->myWorldStash[world] = nullptr;
	}
    }
    return i;
}

void
ITEM::loadGlobal(istream &is)
{
    s32		val, total;

    is.read((char *) &total, sizeof(s32));

    for (int i = 0; i < total; i++)
    {
	is.read((char *) &val, sizeof(s32));
	glbItemIded.indexAnywhere(i) = val ? true : false;
	is.read((char *) &val, sizeof(s32));
	glbItemMagicClass.indexAnywhere(i) = val;
    }
}

bool
ITEM::isPotion() const
{
    if (defn().itemclass == ITEMCLASS_POTION)
	return true;
    return false;
}

bool
ITEM::isSpellbook() const
{
    if (defn().itemclass == ITEMCLASS_SPELLBOOK)
	return true;
    return false;
}

bool
ITEM::isScroll() const
{
    if (defn().itemclass == ITEMCLASS_SCROLL)
	return true;
    return false;
}

bool
ITEM::isRing() const
{
    if (defn().itemclass == ITEMCLASS_RING)
	return true;
    return false;
}

bool
ITEM::isFood() const
{
    if (defn().itemclass == ITEMCLASS_FOOD)
	return true;
    return false;
}

bool
ITEM::isTool() const
{
    if (defn().itemclass == ITEMCLASS_TOOL)
	return true;
    if (getDefinition() == ITEM_DAGGER)
	return true;
    return false;
}

bool
ITEM::isAmmo() const
{
    if (defn().itemclass == ITEMCLASS_AMMO)
	return true;
    return false;
}

bool
ITEM::isTreasure() const
{
    if (defn().itemclass == ITEMCLASS_TREASURE)
	return true;
    return false;
}

bool
ITEM::isFlag() const
{
    if (defn().isflag)
	return true;
    return false;
}

bool
ITEM::isDuped() const
{
    return myDuped;    
}

bool
ITEM::isStartingItem() const
{
    return myStartingItem;    
}

void
ITEM::markStartingItem(bool startstate)
{
    // All polymorphed variants are also duped.
    myStartingItem = startstate;
    WORLD_NAMES world;
    FOREACH_WORLD(world)
    {
        if (myWorldStash[world])
            myWorldStash[world]->markStartingItem(startstate);
    }
}

void
ITEM::markDuped(bool dupestate)
{
    // All polymorphed variants are also duped.
    myDuped = dupestate;
    WORLD_NAMES world;
    FOREACH_WORLD(world)
    {
        if (myWorldStash[world])
            myWorldStash[world]->markDuped(dupestate);
    }
}

bool
ITEM::enchant(int bonus)
{
    if (isBroken() && bonus > 0)
    {
	msg_format("%S <be> enveloped with a glow and <be> repaired.", this);
	setBroken(false);
    }
    int		newpower = getBonus()+bonus;
    if (newpower > 0)
    {
	// safe until 3...  +4 is 25% fail, and increasing...
	int danger = rand_choice(newpower);
	if (danger > 2)
	{
	    msg_format("%S <overload> and <disintegrate>.", this);
	    return false;
	}
    }
    if (newpower > 2)
	msg_format("%S <shudder> and <gain> power.", this);
    else
	msg_format("%S <gain> power.", this);

    setBonus(newpower);
    markBonusKnown();  // ideally modulated by fov :<

    return true;
}

bool
ITEM::waterSoak(POTION_NAMES liquid)
{
    if (isSpellbook() || isScroll())
    {
	if (liquid == POTION_WATER)
	{
	    msg_format("%S <be> soaked with water.", this);
	    fadeWriting();
	    return true;
	}
    }

    if (isPotion() && asPotion() == POTION_WATER)
    {
	if (liquid == POTION_WATER)
	{
	    // Water with water isn't interesting..
	}
	else
	{
	    msg_format("%S <be> tainted by the splashed liquid.", this);
	    setDefinition(lookupPotion(liquid));
	    // I'd like to return true as something happened,
	    // but then we'd identify the splasher!
	    // But we do now know that this is water.
	    markMagicClassKnown();
	}
    }
    else if (isPotion() && liquid == POTION_WATER)
    {
	msg_format("%S <be> soaked with water and is diluted.", this);
	setDefinition(lookupPotion(POTION_WATER));
	return true;
    }

    return false;
}

bool
ITEM::fadeWriting()
{
    if (isSpellbook())
    {
	msg_format("Writing on %S fades.", this);
	setDefinition(ITEM_SPELLBOOK_BLANK);
	return true;
    }
    if (isScroll())
    {
	msg_format("Writing on %S fades.", this);
	setDefinition(ITEM_SCROLL_BLANK);
	return true;
    }
    return false;
}

bool
ITEM::isWeapon() const
{
    if (defn().itemclass == ITEMCLASS_MELEEWEAPON)
	return true;
    return false;
}

bool
ITEM::isArmour() const
{
    if (defn().itemclass == ITEMCLASS_ARMOUR)
	return true;
    return false;
}

bool
ITEM::isRanged() const
{
    if (defn().itemclass == ITEMCLASS_RANGEWEAPON)
	return true;
    return false;
}

void
ITEM::getRangeStats(int &range, int &area) const
{
    range = defn().range_range;
    area = defn().range_area;
}

int
ITEM::getRangeRange() const
{
    // Compute our power, accuracy, and consistency.
    int		range, d;

    getRangeStats(range, d);

    return range;
}

int
ITEM::getRangeArea() const
{
    int		area, d;

    getRangeStats(d, area);

    return area;
}

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

    tmp.strcpy(gram_getarticle(getName()));
    tmp.strcat(getName());
    text_registervariable("A_ITEMNAME", tmp);
    text_registervariable("ITEMNAME", getName());
    text_registervariable("ITEMNAMES", gram_makepossessive(getName()));
    tmp.sprintf("%d", getDepth());
    text_registervariable("ITEMDEPTH", tmp);
    text_registervariable("ITEMCLASS", GAMEDEF::itemclassdef(defn().itemclass)->name);
}

SIZE_NAMES
ITEM::size() const
{
    if (getDefinition() != ITEM_CORPSE)
	return defn().size;

    if (mobType() == MOB_NONE)
	return SIZE_MEDIUM;

    return MOB::defn(mobType()).size;
}

bool
ITEM::shatter()
{
    if (!pos().valid())
    {
	J_ASSERT(!"Cannot shatter backpack items yet!");
	return false;
    }

    bool		destroy = false;
    const char 		*descr = "%S <shatter>.";
    POS			p = pos();
    ITEM		*newitem = nullptr;

    switch (defn().itemclass)
    {
	case ITEMCLASS_POTION:
	    destroy = true;
	    // Probably should also release vapour...
	    break;
	case ITEMCLASS_FURNITURE:
	    destroy = true;
	    switch (material())
	    {
		case MATERIAL_WOOD:
		    descr = "%S <shatter> into splinters.";
		    newitem = ITEM::create(ITEM_CLUB);
		    newitem->setStackCount(rand_range(1, (glb_sizedefs[size()].weight) / 2 + 1));
		    break;
		case MATERIAL_STONE:
		    descr = "%S <shatter> into pieces.";
		    newitem = ITEM::create(ITEM_ROCK);
		    newitem->setStackCount(rand_range(1, glb_sizedefs[size()].weight));
		    break;
		case MATERIAL_IRON:
		case MATERIAL_SILVER:
		    destroy = false;
		    break;
		case MATERIAL_GOLD:
		    descr = "%S <break> into pieces.";
		    newitem = ITEM::create(ITEM_COIN);
		    newitem->setStackCount(rand_range(3, 3 * glb_sizedefs[size()].weight));
		    break;
	    }
    }

    if (!destroy)
    {
	switch (getDefinition())
	{
	    case ITEM_BOULDER:
		descr = "%S <shatter> into pieces.";
		newitem = ITEM::create(ITEM_ROCK);
		newitem->setStackCount(rand_range(1, glb_sizedefs[size()].weight));
		destroy = true;
		break;
	}
    }

    if (destroy)
    {
	move(POS());
	msg_format(descr, this);
	if (newitem)
	    newitem->move(p);
	delete this;
	return destroy;
    } 
    else if (!isBroken() && rand_chance(breakChance()))
    {
	msg_format("%S <break>.", this);
	setBroken(true);
    }
    else
    {
	msg_format("%S <be> unaffected.", this);
    }
    if (newitem)
	p.addItem(newitem);
    return false;
}

EFFECT_NAMES
ITEM::corpseEffect() const
{
    if (getDefinition() != ITEM_CORPSE)
	return EFFECT_NONE;

    if (mobType() == MOB_NONE)
	return EFFECT_NONE;

    return MOB::defn(mobType()).corpse_effect;
}

EFFECT_NAMES
ITEM::potionEffect() const
{
    return potionEffect(getDefinition());
}

EFFECT_NAMES
ITEM::potionEffect(ITEM_NAMES item)
{
    if (defn(item).itemclass != ITEMCLASS_POTION)
	return EFFECT_NONE;
    POTION_NAMES potion = (POTION_NAMES) glbItemMagicClass.safeIndexAnywhere(item);

    return GAMEDEF::potiondef(glbItemMagicClass.safeIndexAnywhere(item))->effect;
}
