/*
 * 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"

static ATOMIC_INT32 glbItemId;

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 glbItemId.add(1);
}

void
glb_reportUID(int uid)
{
    if (uid >= glbItemId)
	glbItemId.maximum(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;
    myDiscarded = false;
    myStartingItem = false;
    myLetter = 0;
}

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

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;

    return item;
}

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

    item = copy();

    item->myUID = glb_allocUID();

    return item;
}

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->myDiscarded = myDiscarded;
    dst->mySeed = mySeed;
    dst->myBonus = myBonus;
    // No letter
    dst->myMaterial = myMaterial;
    dst->myGiltMaterial = myGiltMaterial;
}

ITEM *
ITEM::createMacGuffin(const MAP *map)
{
    ITEM		*i;

    i = ITEM::create(map, ITEM_MACGUFFIN, 1);
    return i;
}

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

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

    i = new ITEM();

    i->myMap = const_cast<MAP *>(map);
    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(const MAP *map, ITEMCLASS_NAMES itemclass, int depth)
{
    ITEM_NAMES item = chooseItemClass(map, itemclass, depth);
    if (item == ITEM_NONE) return nullptr;
    return create(map, item, depth);
}

ITEM *
ITEM::createRandomBalanced(const MAP *map, int depth)
{
    ITEM_NAMES		basetype = chooseRandomBalanced(map, depth);

    if (basetype == ITEM_NONE) return nullptr;

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

    return item;
}

ITEM_NAMES
ITEM::chooseRandomBalanced(const MAP *map, int depth)
{
    // Decide on type
    ITEMCLASS_NAMES	choices[] =
    {
	ITEMCLASS_MELEEWEAPON,
	ITEMCLASS_RANGEWEAPON,
	ITEMCLASS_AMMO,
	ITEMCLASS_POTION,
	ITEMCLASS_ARMOUR,
	ITEMCLASS_SPELLBOOK,
	ITEMCLASS_SCROLL,
	ITEMCLASS_WAND,
	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(map, itemclass, depth);
}

ITEM_NAMES
ITEM::chooseAnyItemClass(MAP *map, ITEMCLASS_NAMES itemclass)
{
    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 ungenerated.
	if (!ITEM::getDepth(map, i))
	    continue;

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

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

    return itemname;
}

ITEM_NAMES
ITEM::chooseItemClass(const MAP *map, ITEMCLASS_NAMES itemclass, int depth)
{
    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(map, i))
	    continue;

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

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

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

    return itemname;
}

ITEM *
ITEM::createRandom(const MAP *map, int depth)
{
    ITEM_NAMES		item;

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

ITEM_NAMES
ITEM::itemFromHash(const MAP *map, 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(map, 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(map, i) > depth)
	    rarity = 0;

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

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

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

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

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

WAND_NAMES
ITEM::asWand(const MAP *map, ITEM_NAMES item)
{
    if (defn(item).itemclass == ITEMCLASS_WAND)
        return (WAND_NAMES) getMagicClass(map, item);
    return WAND_NONE;
}

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

ITEM_NAMES
ITEM::lookupPotion(const MAP *map, POTION_NAMES magicclass)
{
    return item_searchbyclass(map, ITEMCLASS_POTION, magicclass);
}

ITEM_NAMES
ITEM::lookupRing(const MAP *map, RING_NAMES magicclass)
{
    return item_searchbyclass(map, ITEMCLASS_RING, magicclass);
}

ITEM_NAMES
ITEM::lookupSpell(const MAP *map, SPELL_NAMES magicclass)
{
    return item_searchbyclass(map, ITEMCLASS_SPELLBOOK, magicclass);
}

ITEM_NAMES
ITEM::lookupScroll(const MAP *map, SCROLL_NAMES magicclass)
{
    return item_searchbyclass(map, ITEMCLASS_SCROLL, magicclass);
}

ITEM_NAMES
ITEM::lookupWand(const MAP *map, WAND_NAMES magicclass)
{
    return item_searchbyclass(map, ITEMCLASS_WAND, magicclass);
}

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

int
ITEM::getMagicClass(const MAP *map, ITEM_NAMES item)
{
    J_ASSERT(map);
    if (!map) return 0;
    return map->itemMagicClass(item);
}


bool
ITEM::isMagicClassKnown() const
{
    J_ASSERT(myMap);
    if (!myMap) return false;
    return myMap->itemIded(getDefinition());
}

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

    J_ASSERT(myMap);
    if (!myMap) return;

    if (!myMap->itemIded(getDefinition()))
    {
	BUF		origname;
	origname = getSingleArticleName();
	myMap->setItemIded(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;

	// Special case boiled eggs.
	if (getDefinition() == ITEM_EGG_BOILED)
	{
	    buf.sprintf("boiled %s egg", mobname);
	}
	else
	{
	    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 (isWand())
    {
	if (isMagicClassKnown())
	{
	    buf.reference(GAMEDEF::wanddef(asWand())->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() || isWand()))
    {
	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))
	{
	    return resurrect(owner);
	}
    }
    if (getDefinition() == ITEM_EGG &&
	mobType() != MOB_NONE)
    {
	// It is rather important to roll once per stack here,
	// but no point to do more than that, 
	float	singlechance = 0.001f;
	float	failchance = 1 - singlechance;

	failchance = powf(failchance, (float)getStackCount());

	// A more mean person would roll once per stack... :>
	if (rand_prob(1 - failchance))
	{
	    return resurrect(owner);
	}
    }
	
    return false;
}

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

    return defn(item).depth;
}

void
ITEM::initSystem()
{
}

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

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));
    val = myBroken;
    os.write((const char *) &val, sizeof(s32));
    val = myEquipped;
    os.write((const char *) &val, sizeof(s32));
    val = myWielded;
    os.write((const char *) &val, sizeof(s32));
    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 = myDiscarded;
    os.write((const char *) &val, sizeof(s32));
    val = myLetter;
    os.write((const char *) &val, sizeof(s32));
}

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 *)&val, sizeof(s32));
    i->myBroken = val;
    is.read((char *)&val, sizeof(s32));
    i->myEquipped = val;
    is.read((char *)&val, sizeof(s32));
    i->myWielded = val;
    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->myDiscarded = val;
    is.read((char *)&val, sizeof(s32));
    i->myLetter = val;

    return i;
}

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

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::isWand() const
{
    if (defn().itemclass == ITEMCLASS_WAND)
	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;
}

void
ITEM::markDuped(bool dupestate)
{
    // All polymorphed variants are also duped.
    myDuped = 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(myMap, 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(myMap, 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::mend(MOB *owner)
{
    POS		p = pos();
    if (owner) p = owner->pos();

    if (!pos().valid())
    {
	J_ASSERT(!"Cannot mend astral items yet!");
	return false;
    }

    bool		mend = false;

    if (isBroken())
    {
	// Sorry, you can't mend wands of restoration.
	if (asWand() == WAND_RESTORATION)
	{
	    // Ah, you say, but you forgot a loophole!
	    // You can polymorph other wands into restoration
	    // wands, and use them to fix broken wands, making
	    // an infinite wand engine!
	    //
	    // That's right, smarty-pants.  And while you are busy
	    // poly-piling the world burns.
	    return false;
	}
	p.formatAndReport("%S <mend>.", this);
	setBroken(false);
	return true;
    }
    return false;
}

bool
ITEM::cook()
{
    bool	effect = false;
    if (getDefinition() == ITEM_MEATCHUNK)
    {
        setDefinition(ITEM_COOKED_MEATCHUNK);
	effect = true;
    }
    if (getDefinition() == ITEM_EGG)
    {
        setDefinition(ITEM_EGG_BOILED);
	effect = true;
    }
    return effect;
}

bool
ITEM::polymorph(bool silent)
{
    bool		effect = false;

    // Quest items can't transform
    if (defn().quest)
    {
	if (!silent) msg_format("%S <shudder>.", this);
	return true;
    }

    if (getDefinition() == ITEM_EGG ||
	getDefinition() == ITEM_CORPSE)
    {
	// Spin the mob type.
	if (mobType() != MOB_NONE)
	{
	    MOB_NAMES newmob = MOB::chooseAnyNPCType();
	    if (newmob != mobType())
	    {
		if (!silent) msg_format("%S <transform>.", this);
		// It is possible we want to be more precise when
		// it comes to eggs?  Like pick anything that
		// is an eggtype?
		setMobType(MOB::chooseAnyNPCTypeWithCorpse());
		if (!silent) msg_format("%P becomes %S.", this);
		return true;
	    }
	    // No visible effect.
	    return false;
	}
	// Unnamed will become another food item...
    }

    if (itemClass() != ITEMCLASS_NONE)
    {
	ITEM_NAMES	newitem = chooseAnyItemClass(itemClass());

	if (newitem != getDefinition())
	{
	    if (!silent) msg_format("%S <transform>.", this);
	    setDefinition(newitem);
	    return true;
	}
	// no effect.
	return false;
    }
    return effect;
}

bool
ITEM::canResurrect() const
{
    if (getDefinition() != ITEM_CORPSE &&
	getDefinition() != ITEM_EGG)
	return false;

    // No-types can't zombify
    if (mobType() == MOB_NONE)
	return false;

    return true;
}

bool
ITEM::resurrect(MOB *owner)
{
    J_ASSERT(pos().valid() || owner);
    if (!pos().valid() && !owner)
	return false;

    if (!canResurrect())
	return false;

    POS		np;
    if (owner)
	np = owner->pos().randAdjacent();
    else
	np = pos();
    if (np.valid() && !np.mob())
    {
	MOB		*reborn = MOB::create(np.map(), mobType(), false);

	reborn->move(np);
	reborn->skipNextTurn();
	if (owner)
	{
	    owner->formatAndReport("%O escapes %r backpack!", reborn);
	}
	else
	{
	    if (getDefinition() == ITEM_CORPSE)
		reborn->formatAndReport("%S <rise> from the dead!");
	    else if (getDefinition() == ITEM_EGG)
		reborn->formatAndReport("%S <hatch> from the egg!");
	    else
	    {
		J_ASSERT(!"Unhandled mob source...");
		reborn->formatAndReport("%S <rise> from the dust!");
	    }
	}
	if (getStackCount() > 1)
	    setStackCount(getStackCount()-1);
	else
	    return true;
    }
    return false;
}

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(myMap, 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(myMap, 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(myMap, 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(myMap, 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(myMap, getDefinition());
}

EFFECT_NAMES
ITEM::potionEffect(const MAP *map, ITEM_NAMES item)
{
    J_ASSERT(map);
    if (!map) return EFFECT_NONE;

    if (defn(item).itemclass != ITEMCLASS_POTION)
	return EFFECT_NONE;

    POTION_NAMES potion = asPotion(map, item);

    return GAMEDEF::potiondef(potion)->effect;
}
