/
com/planet_ink/coffee_mud/Abilities/Common/
com/planet_ink/coffee_mud/Abilities/Diseases/
com/planet_ink/coffee_mud/Abilities/Druid/
com/planet_ink/coffee_mud/Abilities/Fighter/
com/planet_ink/coffee_mud/Abilities/Languages/
com/planet_ink/coffee_mud/Abilities/Misc/
com/planet_ink/coffee_mud/Abilities/Prayers/
com/planet_ink/coffee_mud/Abilities/Properties/
com/planet_ink/coffee_mud/Abilities/Skills/
com/planet_ink/coffee_mud/Abilities/Songs/
com/planet_ink/coffee_mud/Abilities/Spells/
com/planet_ink/coffee_mud/Abilities/Thief/
com/planet_ink/coffee_mud/Abilities/Traps/
com/planet_ink/coffee_mud/Behaviors/
com/planet_ink/coffee_mud/CharClasses/interfaces/
com/planet_ink/coffee_mud/Commands/
com/planet_ink/coffee_mud/Commands/interfaces/
com/planet_ink/coffee_mud/Common/
com/planet_ink/coffee_mud/Common/interfaces/
com/planet_ink/coffee_mud/Exits/interfaces/
com/planet_ink/coffee_mud/Items/Armor/
com/planet_ink/coffee_mud/Items/Basic/
com/planet_ink/coffee_mud/Items/CompTech/
com/planet_ink/coffee_mud/Items/MiscMagic/
com/planet_ink/coffee_mud/Items/Weapons/
com/planet_ink/coffee_mud/Items/interfaces/
com/planet_ink/coffee_mud/Libraries/
com/planet_ink/coffee_mud/Libraries/interfaces/
com/planet_ink/coffee_mud/Locales/
com/planet_ink/coffee_mud/MOBS/
com/planet_ink/coffee_mud/Races/
com/planet_ink/coffee_mud/Races/interfaces/
com/planet_ink/coffee_mud/WebMacros/
com/planet_ink/coffee_mud/WebMacros/interfaces/
com/planet_ink/coffee_mud/core/
com/planet_ink/coffee_mud/core/collections/
com/planet_ink/coffee_mud/core/interfaces/
com/planet_ink/coffee_mud/core/intermud/
com/planet_ink/coffee_mud/core/intermud/i3/
com/planet_ink/coffee_web/server/
com/planet_ink/siplet/applet/
lib/
resources/factions/
resources/fakedb/
resources/progs/autoplayer/
resources/quests/holidays/
web/
web/admin.templates/
web/admin/grinder/
web/admin/images/
web/clan.templates/
web/pub.templates/
web/pub/images/mxp/
web/pub/sounds/
web/pub/textedit/
package com.planet_ink.coffee_mud.Libraries;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.ExpertiseLibrary.ExpertiseDefinition;
import com.planet_ink.coffee_mud.Abilities.interfaces.*;
import com.planet_ink.coffee_mud.Areas.interfaces.*;
import com.planet_ink.coffee_mud.Behaviors.interfaces.*;
import com.planet_ink.coffee_mud.CharClasses.interfaces.*;
import com.planet_ink.coffee_mud.Commands.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.AccountStats.PrideStat;
import com.planet_ink.coffee_mud.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.util.*;


/*
   Copyright 2006-2016 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
public class CoffeeLevels extends StdLibrary implements ExpLevelLibrary
{
	@Override public String ID(){return "CoffeeLevels";}

	public int getManaBonusNextLevel(MOB mob)
	{
		final CharClass charClass = mob.baseCharStats().getCurrentClass();
		final double[] variables={
				mob.phyStats().level(),
				mob.charStats().getStat(CharStats.STAT_WISDOM),
				CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_WISDOM)),
				mob.charStats().getStat(CharStats.STAT_INTELLIGENCE),
				CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_INTELLIGENCE)),
				mob.charStats().getStat(charClass.getAttackAttribute()),
				CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(charClass.getAttackAttribute())),
				mob.charStats().getStat(CharStats.STAT_CHARISMA),
				mob.charStats().getStat(CharStats.STAT_CONSTITUTION)
			};
		return (int)Math.round(CMath.parseMathExpression(charClass.getManaFormula(), variables));
	}

	@Override
	public int getLevelMana(MOB mob)
	{
		return CMProps.getIntVar(CMProps.Int.STARTMANA)+
			((mob.basePhyStats().level()-1)*getManaBonusNextLevel(mob));
	}

	public int getAttackBonusNextLevel(MOB mob)
	{
		final CharClass charClass = mob.baseCharStats().getCurrentClass();
		final int rawAttStat = mob.charStats().getStat(charClass.getAttackAttribute());
		int attStat= rawAttStat;
		final int maxAttStat=(CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)
					 +mob.charStats().getStat(CharStats.CODES.toMAXBASE(charClass.getAttackAttribute())));
		if(attStat>=maxAttStat)
			attStat=maxAttStat;
		int attGain=(int)Math.floor(CMath.div(attStat,18.0))+charClass.getBonusAttackLevel();
		if(attStat>=25)
			attGain+=2;
		else
		if(attStat>=22)
			attGain+=1;
		return attGain;
	}

	@Override
	public int getLevelAttack(MOB mob)
	{
		return ((mob.basePhyStats().level()-1)*getAttackBonusNextLevel(mob)) + mob.basePhyStats().level();
	}

	@Override
	public int getLevelMOBArmor(MOB mob)
	{
		return 100-(int)Math.round(CMath.mul(mob.basePhyStats().level(),3.0));
	}

	@Override
	public int getLevelMOBDamage(MOB mob)
	{
		return (mob.basePhyStats().level());
	}

	@Override
	public double getLevelMOBSpeed(MOB mob)
	{
		return 1.0+Math.floor(CMath.div(mob.basePhyStats().level(),30.0));
	}

	public int getMoveBonusNextLevel(MOB mob)
	{
		final CharClass charClass = mob.baseCharStats().getCurrentClass();
		final double[] variables={
			mob.phyStats().level(),
			mob.charStats().getStat(CharStats.STAT_STRENGTH),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_STRENGTH)),
			mob.charStats().getStat(CharStats.STAT_DEXTERITY),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_DEXTERITY)),
			mob.charStats().getStat(CharStats.STAT_CONSTITUTION),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_CONSTITUTION)),
			mob.charStats().getStat(CharStats.STAT_WISDOM),
			mob.charStats().getStat(CharStats.STAT_INTELLIGENCE)
		};
		return (int)Math.round(CMath.parseMathExpression(charClass.getMovementFormula(), variables));
	}

	@Override
	public int getLevelMove(MOB mob)
	{
		int move=CMProps.getIntVar(CMProps.Int.STARTMOVE);
		if(mob.basePhyStats().level()>1)
			move+=(mob.basePhyStats().level()-1) * getMoveBonusNextLevel(mob);
		return move;
	}

	public int getPlayerHPBonusNextLevel(MOB mob)
	{
		final CharClass charClass = mob.baseCharStats().getCurrentClass();
		final double[] variables={
			mob.phyStats().level(),
			mob.charStats().getStat(CharStats.STAT_STRENGTH),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_STRENGTH)),
			mob.charStats().getStat(CharStats.STAT_DEXTERITY),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_DEXTERITY)),
			mob.charStats().getStat(CharStats.STAT_CONSTITUTION),
			CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)+mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_CONSTITUTION)),
			mob.charStats().getStat(CharStats.STAT_WISDOM),
			mob.charStats().getStat(CharStats.STAT_INTELLIGENCE)
		};
		final int newHitPointGain=(int)Math.round(CMath.parseMathExpression(charClass.getHitPointsFormula(), variables));
		if(newHitPointGain<=0)
		{
			if(mob.charStats().getStat(CharStats.STAT_CONSTITUTION)>=1)
				return 1;
			return 0;
		}
		return newHitPointGain;
	}

	@Override
	public int getPlayerHitPoints(MOB mob)
	{
		final int hp=CMProps.getIntVar(CMProps.Int.STARTHP);
		return hp+((mob.phyStats().level()-1)*getPlayerHPBonusNextLevel(mob));
	}

	@Override
	public MOB fillOutMOB(CharClass C, int level)
	{
		final MOB mob=CMClass.getFactoryMOB();
		mob.baseCharStats().setCurrentClass(C);
		mob.charStats().setCurrentClass(C);
		mob.baseCharStats().setCurrentClassLevel(level);
		mob.charStats().setCurrentClassLevel(level);
		mob.basePhyStats().setLevel(level);
		mob.phyStats().setLevel(level);
		fillOutMOB(mob,level);
		return mob;
	}

	public boolean isFilledOutMOB(MOB mob)
	{
		if(!mob.isMonster())
			return false;
		final PhyStats mobP=mob.basePhyStats();

		final MOB filledM=fillOutMOB((MOB)null,mobP.level());
		final PhyStats filP=filledM.basePhyStats();
		if((mobP.speed()==filP.speed())
		&&(mobP.armor()==filP.armor())
		&&(mobP.damage()==filP.damage())
		&&(mobP.attackAdjustment()==filP.attackAdjustment()))
		{
			filledM.destroy();
			return true;
		}
		filledM.destroy();
		return false;
	}

	@Override
	public MOB fillOutMOB(MOB mob, int level)
	{
		if(mob==null)
			mob=CMClass.getFactoryMOB();
		if(!mob.isMonster())
			return mob;

		long rejuv=CMProps.getTicksPerMinute()+CMProps.getTicksPerMinute()+(level*CMProps.getTicksPerMinute()/2);
		if(rejuv>(CMProps.getTicksPerMinute()*20))
			rejuv=(CMProps.getTicksPerMinute()*20);
		mob.basePhyStats().setLevel(level);
		mob.basePhyStats().setRejuv((int)rejuv);
		mob.basePhyStats().setSpeed(getLevelMOBSpeed(mob));
		mob.basePhyStats().setArmor(getLevelMOBArmor(mob));
		mob.basePhyStats().setDamage(getLevelMOBDamage(mob));
		mob.basePhyStats().setAttackAdjustment(getLevelAttack(mob));
		mob.setMoney(CMLib.dice().roll(1,level,0)+CMLib.dice().roll(1,10,0));
		mob.baseState().setHitPoints(CMLib.dice().rollHP(mob.basePhyStats().level(),mob.basePhyStats().ability()));
		mob.baseState().setMana(getLevelMana(mob));
		mob.baseState().setMovement(getLevelMove(mob));
		if(mob.getWimpHitPoint()>0)
			mob.setWimpHitPoint((int)Math.round(CMath.mul(mob.curState().getHitPoints(),.10)));
		mob.setExperience(CMLib.leveler().getLevelExperience(mob.phyStats().level()));
		return mob;
	}
	
	@Override
	public double[] getLevelMoneyRange(MOB mob)
	{
		return new double[]{2,mob.basePhyStats().level()+10};
	}

	@Override
	public StringBuffer baseLevelAdjuster(MOB mob, int adjuster)
	{
		mob.basePhyStats().setLevel(mob.basePhyStats().level()+adjuster);
		final CharClass curClass=mob.baseCharStats().getCurrentClass();
		mob.baseCharStats().setClassLevel(curClass,mob.baseCharStats().getClassLevel(curClass)+adjuster);
		final int classLevel=mob.baseCharStats().getClassLevel(mob.baseCharStats().getCurrentClass());
		int gained=mob.getExperience()-mob.getExpNextLevel();
		if(gained<50)
			gained=50;

		final StringBuffer theNews=new StringBuffer("");

		mob.recoverCharStats();
		mob.recoverPhyStats();
		theNews.append("^HYou are now a "+mob.charStats().displayClassLevel(mob,false)+".^N\n\r");

		final int newHitPointGain = getPlayerHPBonusNextLevel(mob) * adjuster;
		if(mob.getWimpHitPoint() > 0)
		{
			double wimpPct = CMath.div(mob.getWimpHitPoint(), mob.baseState().getHitPoints());
			mob.setWimpHitPoint((int)Math.round(CMath.ceiling(CMath.mul(mob.baseState().getHitPoints()+newHitPointGain,wimpPct))));
		}
		mob.baseState().setHitPoints(mob.baseState().getHitPoints()+newHitPointGain);
		if(mob.baseState().getHitPoints()<20)
			mob.baseState().setHitPoints(20);
		mob.curState().setHitPoints(mob.curState().getHitPoints()+newHitPointGain);
		theNews.append("^NYou have gained ^H"+newHitPointGain+"^? hit " +
			(newHitPointGain!=1?"points":"point") + ", ^H");

		final int mvGain = getMoveBonusNextLevel(mob) * adjuster;
		mob.baseState().setMovement(mob.baseState().getMovement()+mvGain);
		mob.curState().setMovement(mob.curState().getMovement()+mvGain);
		theNews.append(mvGain+"^N move " + (mvGain!=1?"points":"point") + ", ^H");

		final int attGain=getAttackBonusNextLevel(mob) * adjuster;
		mob.basePhyStats().setAttackAdjustment(mob.basePhyStats().attackAdjustment()+attGain);
		mob.phyStats().setAttackAdjustment(mob.phyStats().attackAdjustment()+attGain);
		if(attGain>0)
			theNews.append(attGain+"^N attack " + (attGain!=1?"points":"point") + ", ^H");

		final int manaGain = getManaBonusNextLevel(mob) * adjuster;
		mob.baseState().setMana(mob.baseState().getMana()+manaGain);
		theNews.append(manaGain+"^N " + (manaGain!=1?"points":"point") + " of mana,");


		if(curClass.getLevelsPerBonusDamage()!=0)
		{
			if((adjuster<0)&&(((classLevel+1)%curClass.getLevelsPerBonusDamage())==0))
				mob.basePhyStats().setDamage(mob.basePhyStats().damage()-1);
			else
			if((adjuster>0)&&((classLevel%curClass.getLevelsPerBonusDamage())==0))
				mob.basePhyStats().setDamage(mob.basePhyStats().damage()+1);
		}
		mob.recoverMaxState();
		return theNews;
	}

	@Override
	public void unLevel(MOB mob)
	{
		if((mob.basePhyStats().level()<2)
		||(CMSecurity.isDisabled(CMSecurity.DisFlag.LEVELS))
		||(mob.charStats().getCurrentClass().leveless())
		||(mob.charStats().getMyRace().leveless()))
			return;
		mob.tell(L("^ZYou have ****LOST A LEVEL****^.^N\n\r\n\r@x1",CMLib.protocol().msp("doh.wav",60)));
		if(!mob.isMonster())
		{
			final List<String> channels=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.LOSTLEVELS);
			if(!CMLib.flags().isCloaked(mob))
			for(int i=0;i<channels.size();i++)
				CMLib.commands().postChannel(channels.get(i),mob.clans(),L("@x1 has just lost a level.",mob.Name()),true);
		}

		final CharClass curClass=mob.baseCharStats().getCurrentClass();
		final int oldClassLevel=mob.baseCharStats().getClassLevel(curClass);
		baseLevelAdjuster(mob,-1);
		int prac2Stat=mob.charStats().getStat(CharStats.STAT_WISDOM);
		final int maxPrac2Stat=(CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)
					 +mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_WISDOM)));
		if(prac2Stat>maxPrac2Stat)
			prac2Stat=maxPrac2Stat;
		int practiceGain=(int)Math.floor(CMath.div(prac2Stat,6.0))+curClass.getBonusPracLevel();
		if(practiceGain<=0)
			practiceGain=1;
		mob.setPractices(mob.getPractices()-practiceGain);
		int trainGain=0;
		if(trainGain<=0)
			trainGain=1;
		mob.setTrains(mob.getTrains()-trainGain);

		mob.recoverPhyStats();
		mob.recoverCharStats();
		mob.recoverMaxState();
		mob.tell(L("^HYou are now a level @x1 @x2^N.\n\r",""+mob.charStats().getClassLevel(mob.charStats().getCurrentClass()),mob.charStats().getCurrentClass().name(mob.charStats().getCurrentClassLevel())));
		curClass.unLevel(mob);
		Ability A=null;
		final Vector<Ability> lose=new Vector<Ability>();
		for(int a=0;a<mob.numAbilities();a++)
		{
			A=mob.fetchAbility(a);
			if((CMLib.ableMapper().getQualifyingLevel(curClass.ID(),false,A.ID())==oldClassLevel)
			&&(CMLib.ableMapper().getDefaultGain(curClass.ID(),false,A.ID()))
			&&(CMLib.ableMapper().classOnly(mob,curClass.ID(),A.ID())))
				lose.addElement(A);
		}
		for(int l=0;l<lose.size();l++)
		{
			A=lose.elementAt(l);
			mob.delAbility(A);
			mob.tell(L("^HYou have forgotten @x1.^N.\n\r",A.name()));
			A=mob.fetchEffect(A.ID());
			if((A!=null)&&(A.isNowAnAutoEffect()))
			{
				A.unInvoke();
				mob.delEffect(A);
			}
		}
		fixMobStatsIfNecessary(mob,-1);
		CMLib.achievements().possiblyBumpAchievement(mob, AchievementLibrary.Event.LEVELSGAINED, -1, mob);
	}

	@Override
	public void loseExperience(MOB mob, int amount)
	{
		if((mob==null)||(mob.soulMate()!=null))
			return;
		if(Log.combatChannelOn())
		{
			final String room=CMLib.map().getExtendedRoomID((mob.location()!=null)?mob.location():null);
			final String mobName=mob.Name();
			Log.killsOut("-EXP",room+":"+mobName+":"+amount);
		}
		if((mob.getLiegeID().length()>0)&&(amount>2)&&(!mob.isMonster()))
		{
			final MOB sire=CMLib.players().getPlayer(mob.getLiegeID());
			if((sire!=null)&&(CMLib.flags().isInTheGame(sire,true)))
			{
				final int sireShare=(int)Math.round(CMath.div(amount,10.0));
				amount-=sireShare;
				if(postExperience(sire,null,"",-sireShare,true))
					sire.tell(L("^N^!You lose ^H@x1^N^! experience points from @x2.^N",""+sireShare,mob.Name()));
			}
		}
		if((amount>2)&&(!mob.isMonster()))
		{
			for(final Pair<Clan,Integer> p : mob.clans())
			{
				final Clan C=p.first;
				if(C.getTaxes()>0.0)
				{
					final int clanshare=(int)Math.round(CMath.mul(amount,C.getTaxes()));
					if(clanshare>0)
					{
						amount-=clanshare;
						C.adjExp(clanshare*-1);
						C.update();
					}
				}
			}
		}
		mob.setExperience(mob.getExperience()-amount);
		int neededLowest=getLevelExperience(mob.basePhyStats().level()-2);
		if((mob.getExperience()<neededLowest)
		&&(mob.basePhyStats().level()>1))
		{
			unLevel(mob);
			neededLowest=getLevelExperience(mob.basePhyStats().level()-2);
		}
	}

	@Override
	public boolean postExperience(MOB mob,MOB victim,String homage,int amount,boolean quiet)
	{
		if((mob==null)
		||(CMSecurity.isDisabled(CMSecurity.DisFlag.EXPERIENCE))
		||mob.charStats().getCurrentClass().expless()
		||mob.charStats().getMyRace().expless())
			return false;
		final CMMsg msg=CMClass.getMsg(mob,victim,null,CMMsg.MASK_ALWAYS|CMMsg.TYP_EXPCHANGE,null,CMMsg.NO_EFFECT,homage,CMMsg.NO_EFFECT,""+quiet);
		msg.setValue(amount);
		final Room R=mob.location();
		if(R!=null)
		{
			if(R.okMessage(mob,msg))
				R.send(mob,msg);
			else
				return false;
		}
		else
		if(amount>=0)
			gainExperience(mob,victim,homage,amount,quiet);
		else
			loseExperience(mob,-amount);
		return true;
	}

	@Override
	public int getLevelExperience(int level)
	{
		if(level<0)
			return 0;
		final int[] levelingChart = CMProps.getListFileIntList(CMProps.ListFile.EXP_CHART);
		if(level<levelingChart.length)
			return levelingChart[level];
		final int lastDiff=levelingChart[levelingChart.length-1] - levelingChart[levelingChart.length-2];
		return levelingChart[levelingChart.length-1] + ((1+(level-levelingChart.length)) * lastDiff);
	}

	@Override
	public int getLevelExperienceJustThisLevel(int level)
	{
		if(level<0)
			return 0;
		final int[] levelingChart = CMProps.getListFileIntList(CMProps.ListFile.EXP_CHART);
		if(level==0)
			return levelingChart[0];
		else
		if(level<levelingChart.length)
			return levelingChart[level]-levelingChart[level-1];
		final int lastDiff=levelingChart[levelingChart.length-1] - levelingChart[levelingChart.length-2];
		return ((1+(level-levelingChart.length)) * lastDiff);
	}

	@Override
	public void level(MOB mob)
	{
		if((CMSecurity.isDisabled(CMSecurity.DisFlag.LEVELS))
		||(mob.charStats().getCurrentClass().leveless())
		||(mob.charStats().isLevelCapped(mob.charStats().getCurrentClass()))
		||(mob.charStats().getMyRace().leveless()))
			return;
		final Room room=mob.location();
		final CMMsg msg=CMClass.getMsg(mob,CMMsg.MSG_LEVEL,null,mob.basePhyStats().level()+1);
		if(!CMLib.map().sendGlobalMessage(mob,CMMsg.TYP_LEVEL,msg))
			return;
		if(room!=null)
		{
			if(!room.okMessage(mob,msg))
				return;
			room.send(mob,msg);
		}

		if(mob.getGroupMembers(new HashSet<MOB>()).size()>1)
		{
			final Command C=CMClass.getCommand("GTell");
			try
			{
				if(C!=null)
					C.execute(mob,new XVector<String>("GTELL",",<S-HAS-HAVE> gained a level."),MUDCmdProcessor.METAFLAG_FORCED);
			}catch(final Exception e){}
		}
		final StringBuffer theNews=new StringBuffer("^xYou have L E V E L E D ! ! ! ! ! ^.^N\n\r\n\r"+CMLib.protocol().msp("level_gain.wav",60));
		CharClass curClass=mob.baseCharStats().getCurrentClass();
		theNews.append(baseLevelAdjuster(mob,1));
		if(mob.playerStats()!=null)
		{
			mob.playerStats().setLeveledDateTime(mob.basePhyStats().level(),mob.getAgeMinutes(),room);
			final List<String> channels=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.DETAILEDLEVELS);
			final List<String> channels2=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.LEVELS);
			if(!CMLib.flags().isCloaked(mob))
			for(int i=0;i<channels.size();i++)
				CMLib.commands().postChannel(channels.get(i),mob.clans(),L("@x1 has just gained a level at @x2.",mob.Name(),CMLib.map().getDescriptiveExtendedRoomID(room)),true);
			if(!CMLib.flags().isCloaked(mob))
			for(int i=0;i<channels2.size();i++)
				CMLib.commands().postChannel(channels2.get(i),mob.clans(),L("@x1 has just gained a level.",mob.Name()),true);
			if(mob.soulMate()==null)
				CMLib.coffeeTables().bump(mob,CoffeeTableRow.STAT_LEVELSGAINED);
		}

		int prac2Stat=mob.charStats().getStat(CharStats.STAT_WISDOM);
		final int maxPrac2Stat=(CMProps.getIntVar(CMProps.Int.BASEMAXSTAT)
					 +mob.charStats().getStat(CharStats.CODES.toMAXBASE(CharStats.STAT_WISDOM)));
		if(prac2Stat>maxPrac2Stat)
			prac2Stat=maxPrac2Stat;
		int practiceGain=(int)Math.floor(CMath.div(prac2Stat,6.0))+curClass.getBonusPracLevel();
		if(practiceGain<=0)
			practiceGain=1;
		mob.setPractices(mob.getPractices()+practiceGain);
		theNews.append(" ^H" + practiceGain+"^N practice " +
			( practiceGain != 1? "points" : "point" ) + ", ");

		int trainGain=1;
		if(trainGain<=0)
			trainGain=1;
		mob.setTrains(mob.getTrains()+trainGain);
		theNews.append("and ^H"+trainGain+"^N training "+ (trainGain != 1? "sessions" : "session" )+".\n\r^N");

		mob.tell(theNews.toString());
		curClass=mob.baseCharStats().getCurrentClass();
		final Set<String> oldAbilities=new HashSet<String>();
		for(final Enumeration<Ability> a=mob.allAbilities();a.hasMoreElements();)
		{
			final Ability A=a.nextElement();
			if(A!=null)
				oldAbilities.add(A.ID());
		}
		final Map<String,Integer> oldExpertises=new TreeMap<String,Integer>();
		for(final Enumeration<String> e=mob.expertises();e.hasMoreElements();)
		{
			Pair<String,Integer> pair = mob.fetchExpertise(e.nextElement());
			if(pair != null)
			{
				if((!oldExpertises.containsKey(pair.first))
				||(oldExpertises.get(pair.first).intValue() < pair.second.intValue()))
					oldExpertises.put(pair.first, pair.second);
			}
		}

		curClass.grantAbilities(mob,false);
		CMLib.achievements().grantAbilitiesAndExpertises(mob);

		// check for autoinvoking abilities
		for(int a=0;a<mob.numAbilities();a++)
		{
			final Ability A=mob.fetchAbility(a);
			if((A!=null)
			&&(CMLib.ableMapper().qualifiesByLevel(mob,A)))
			{
				final Ability eA=mob.fetchEffect(A.ID());
				if((eA==null)||(!eA.isNowAnAutoEffect()))
					A.autoInvocation(mob, false);
			}
		}

		final List<String> newAbilityIDs=new Vector<String>();
		for(final Enumeration<Ability> a=mob.allAbilities();a.hasMoreElements();)
		{
			final Ability A=a.nextElement();
			if((A!=null)&&(!oldAbilities.contains(A.ID())))
				newAbilityIDs.add(A.ID());
		}

		for(int a=0;a<newAbilityIDs.size();a++)
		{
			if(!oldAbilities.contains(newAbilityIDs.get(a)))
			{
				final Ability A=mob.fetchAbility(newAbilityIDs.get(a));
				if(A!=null)
				{
					final String type=Ability.ACODE_DESCS[(A.classificationCode()&Ability.ALL_ACODES)].toLowerCase();
					mob.tell(L("^NYou have learned the @x1 ^H@x2^?.^N",type,A.name()));
				}
			}
		}

		for(final Enumeration<String> e=mob.expertises();e.hasMoreElements();)
		{
			final Pair<String,Integer> pair = mob.fetchExpertise(e.nextElement());
			if(pair != null)
			{
				ExpertiseDefinition def=CMLib.expertises().findDefinition(pair.first+pair.second.toString(),true);
				if(def == null)
					def=CMLib.expertises().findDefinition(pair.first+pair.second.toString(),false);
				if(def == null)
					 def=CMLib.expertises().findDefinition(pair.first,true);
				if(def == null)
					 def=CMLib.expertises().findDefinition(pair.first,false);
				if(def != null)
				{
					if((!oldExpertises.containsKey(pair.first))
					||(oldExpertises.get(pair.first).intValue() < pair.second.intValue()))
						mob.tell(L("^NYou have learned ^H@x1^?.^N",def.name()));
				}
			}
		}
		
		fixMobStatsIfNecessary(mob,1);

		// wrap it all up
		mob.recoverPhyStats();
		mob.recoverCharStats();
		mob.recoverMaxState();

		curClass.level(mob,newAbilityIDs);
		mob.charStats().getMyRace().level(mob,newAbilityIDs);
		
		CMLib.achievements().possiblyBumpAchievement(mob, AchievementLibrary.Event.LEVELSGAINED, 1, mob);
	}

	protected boolean fixMobStatsIfNecessary(MOB mob, int direction)
	{
		if((mob.playerStats()==null)&&(mob.baseCharStats().getCurrentClass().name().equals("mob"))) // mob leveling
		{
			mob.basePhyStats().setSpeed(getLevelMOBSpeed(mob));
			mob.basePhyStats().setArmor(getLevelMOBArmor(mob));
			mob.basePhyStats().setDamage(getLevelMOBDamage(mob));
			mob.basePhyStats().setAttackAdjustment(getLevelAttack(mob));
			mob.baseState().setHitPoints(mob.baseState().getHitPoints()+((mob.basePhyStats().ability()/2)*direction));
			mob.baseState().setMana(getLevelMana(mob));
			mob.baseState().setMovement(getLevelMove(mob));
			if(mob.getWimpHitPoint()>0)
				mob.setWimpHitPoint((int)Math.round(CMath.mul(mob.curState().getHitPoints(),.10)));
			return true;
		}
		return false;
	}

	@Override
	public int adjustedExperience(MOB mob, MOB victim, int amount)
	{
		int highestLevelPC = 0;
		final Room R=mob.location();
		if(R!=null)
			for(int m=0;m<R.numInhabitants();m++)
			{
				final MOB M=R.fetchInhabitant(m);
				if((M!=null)&&(M!=mob)&&(M!=victim)&&(!M.isMonster())&&(M.phyStats().level()>highestLevelPC))
					highestLevelPC = M.phyStats().level();
			}

		final Set<MOB> group=mob.getGroupMembers(new HashSet<MOB>());
		CharClass charClass=null;
		Race charRace=null;

		for (final MOB allyMOB : group)
		{
			charClass = allyMOB.charStats().getCurrentClass();
			charRace = allyMOB.charStats().getMyRace();
			if(charClass != null)
				amount = charClass.adjustExperienceGain(allyMOB, mob, victim, amount);
			if(charRace != null)
				amount = charRace.adjustExperienceGain(allyMOB, mob, victim, amount);
		}

		if(victim!=null)
		{
			final double levelLimit=CMProps.getIntVar(CMProps.Int.EXPRATE);
			final double levelDiff=victim.phyStats().level()-mob.phyStats().level();

			if(levelDiff<(-levelLimit) )
				amount=0;
			else
			if((levelLimit>0)&&((highestLevelPC - mob.phyStats().level())<=levelLimit))
			{
				double levelFactor=levelDiff / levelLimit;
				if( levelFactor > levelLimit )
					levelFactor = levelLimit;
				amount+=(int)Math.round(levelFactor *  amount);
			}
		}

		return amount;
	}

	@Override
	public void gainExperience(MOB mob, MOB victim, String homageMessage, int amount, boolean quiet)
	{
		if(mob==null)
			return;
		if((Log.combatChannelOn())
		&&((mob.location()!=null)
			||((victim!=null)&&(victim.location()!=null))))
		{
			final String room=CMLib.map().getExtendedRoomID((mob.location()!=null)?mob.location():(victim==null)?null:victim.location());
			final String mobName=mob.Name();
			final String vicName=(victim!=null)?victim.Name():"null";
			Log.killsOut("+EXP",room+":"+mobName+":"+vicName+":"+amount+":"+homageMessage);
		}

		amount=adjustedExperience(mob,victim,amount);

		if(mob.phyStats().level()>=CMProps.getIntVar(CMProps.Int.MINCLANLEVEL))
		{
			for(final Pair<Clan,Integer> p : mob.clans())
				if(amount>2)
					amount=p.first.applyExpMods(amount);
		}

		if((mob.getLiegeID().length()>0)&&(amount>2))
		{
			final MOB sire=CMLib.players().getLoadPlayer(mob.getLiegeID());
			if(sire!=null)
			{
				int sireShare=(int)Math.round(CMath.div(amount,10.0));
				if(sireShare<=0)
					sireShare=1;
				amount-=sireShare;
				CMLib.leveler().postExperience(sire,null," from "+mob.name(sire),sireShare,quiet);
			}
		}

		CMLib.players().bumpPrideStat(mob,PrideStat.EXPERIENCE_GAINED, amount);
		mob.setExperience(mob.getExperience()+amount);
		if(homageMessage==null)
			homageMessage="";
		if(!quiet)
		{
			if(amount>1)
				mob.tell(L("^N^!You gain ^H@x1^N^! experience points@x2.^N",""+amount,homageMessage));
			else
			if(amount>0)
				mob.tell(L("^N^!You gain ^H@x1^N^! experience point@x2.^N",""+amount,homageMessage));
		}

		if((mob.getExperience()>=mob.getExpNextLevel())
		&&(mob.getExpNeededLevel()<Integer.MAX_VALUE))
			level(mob);
	}

	@Override
	public void handleExperienceChange(CMMsg msg)
	{
		final MOB mob=msg.source();
		if(!CMSecurity.isDisabled(CMSecurity.DisFlag.EXPERIENCE)
		&&!mob.charStats().getCurrentClass().expless()
		&&!mob.charStats().getMyRace().expless())
		{
			MOB expFromKilledmob=null;
			if(msg.target() instanceof MOB)
				expFromKilledmob=(MOB)msg.target();

			if(msg.value()>=0)
				gainExperience(mob,
							   expFromKilledmob,
							   msg.targetMessage(),
							   msg.value(),
							   CMath.s_bool(msg.othersMessage()));
			else
				loseExperience(mob,-msg.value());
		}
	}

}