/
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/Specializations/
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/
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/BasicTech/
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.Abilities.Common;
import com.planet_ink.coffee_mud.Abilities.StdAbility;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.CMSecurity.SecFlag;
import com.planet_ink.coffee_mud.core.collections.*;
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.Session.InputCallback;
import com.planet_ink.coffee_mud.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.MOB.Attrib;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.io.IOException;
import java.util.*;
/*
   Copyright 2017-2019 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 Studying extends CommonSkill implements AbilityContainer
{
	@Override
	public String ID()
	{
		return "Studying";
	}

	private final static String	localizedName	= CMLib.lang().L("Studying");

	@Override
	public String name()
	{
		return localizedName;
	}

	private static final String[]	triggerStrings	= I(new String[] { "STUDY", "STUDYING" });

	@Override
	public String[] triggerStrings()
	{
		return triggerStrings;
	}

	@Override
	public long flags()
	{
		return super.flags() | Ability.FLAG_NOUNINVOKING;
	}

	protected static enum perLevelLimits
	{
		COMMON(1, 6, 1, ACODE_COMMON_SKILL, ACODE_LANGUAGE),
		SKILL(1, 6, 2, ACODE_SKILL, ACODE_THIEF_SKILL),
		SONG(1, 6, 3, ACODE_SONG, -1),
		SPELL(1, 6, 4, ACODE_SPELL, -1),
		CHANT(1, 6, 5, ACODE_CHANT, -1),
		PRAYER(1, 6, 6, ACODE_PRAYER, -1)
		;

		private int	type1		= -1;
		private int	type2		= -1;
		private int	num			= 1;
		private int	perLevels	= 1;
		private int	aboveLevel	= 1;

		private perLevelLimits(final int num, final int perLevels, final int aboveLevel, final int type1, final int type2)
		{
			this.num=num;
			this.perLevels=perLevels;
			this.aboveLevel=aboveLevel;
			this.type1=type1;
			this.type2=type2;
		}

		public boolean doesRuleApplyTo(final Ability A)
		{
			return (A!=null)
				&& (((A.classificationCode()&Ability.ALL_ACODES)==type1)
					||((A.classificationCode()&Ability.ALL_ACODES)==type2));
		}

		public boolean doesRuleApplyTo(final int abilityCode)
		{
			return ((abilityCode==type1) || (abilityCode==type2));
		}

		public int numAllowed(final int classLevel)
		{
			if(classLevel < aboveLevel)
				return 0;
			return num + (num * (int)Math.round(Math.floor((classLevel-aboveLevel) / perLevels)));
		}
	}

	protected perLevelLimits getSupportedSkillType()
	{
		return null;
	}

	@Override
	public boolean isAutoInvoked()
	{
		return true;
	}

	@Override
	public boolean canBeUninvoked()
	{
		return !isAnAutoEffect;
	}

	/*
	We could also make this 6 different abilities, Common Skill Studying, Skills
	Studying, Songs Studying, Chants Studying, Spells Studying, and Prayers Studying if you would prefer
	granting each ability at the lowest level above (1,2,3,4,5,6).
	 */

	protected Physical			teacherP			= null;
	protected Ability			teachingA			= null;
	protected volatile boolean	distributed			= false;
	protected boolean			successfullyTaught	= false;
	protected List<Ability>		skillList			= new LinkedList<Ability>();

	@Override
	public void setMiscText(final String newMiscText)
	{
		super.setMiscText(newMiscText);
		distributed = false;
	}

	@Override
	public String displayText()
	{
		if((teacherP == null)||(teachingA==null))
			return L("(Scholarly)"); // prevents it from being uninvokeable through autoaffects
		else
		{
			final Ability teachingA = this.teachingA;
			if((teachingA != null)
			&&(affected instanceof MOB))
				return L("You are being taught @x2 by @x1.",teacherP.name((MOB)affected),teachingA.name());
			return L("You are being taught something by someone!");
		}
	}

	@Override
	protected int canAffectCode()
	{
		return CAN_MOBS;
	}

	@Override
	protected int canTargetCode()
	{
		return CAN_MOBS;
	}

	@Override
	public int abstractQuality()
	{
		return Ability.QUALITY_OK_OTHERS;
	}

	@Override
	public int classificationCode()
	{
		return Ability.ACODE_COMMON_SKILL | Ability.DOMAIN_EDUCATIONLORE;
	}

	@Override
	public void executeMsg(final Environmental myHost, final CMMsg msg)
	{
		super.executeMsg(myHost,msg);
		if((msg.source() == affected)
		&&(!canBeUninvoked())
		&&(msg.tool() instanceof Ability))
		{
			final MOB mob=msg.source();
			if(msg.tool().ID().equals("Spell_Scribe")
			||msg.tool().ID().equals("Spell_EnchantWand")
			||msg.tool().ID().equals("Spell_MagicItem")
			||msg.tool().ID().equals("Spell_StoreSpell")
			||msg.tool().ID().equals("Spell_WardArea"))
			{
				final Ability A=mob.fetchAbility(msg.tool().text());
				if((A!=null)&&(!A.isSavable()))
					forget(mob,A.ID());
			}
			final Ability A=mob.fetchAbility(msg.tool().ID());
			if((A!=null)&&(!A.isSavable())
			&&((A.classificationCode()&Ability.ALL_ACODES)==Ability.ACODE_SPELL))
				forget(mob,A.ID());
		}
	}

	protected boolean forget(final MOB mob, final String abilityID)
	{
		if(mob == null)
			return false;
		final Studying studA=(Studying)mob.fetchAbility(ID());
		final Studying effA=(Studying)mob.fetchEffect(ID());
		if((studA != null) && (effA != null))
		{
			final List<String> strList = CMParms.parseSemicolons(studA.text(), true);
			boolean removed=false;
			for(int i=0;i<strList.size();i++)
			{
				if(strList.get(i).startsWith(abilityID+","))
				{
					strList.remove(i);
					removed = true;
					break;
				}
			}
			if(removed)
			{
				final String text=CMParms.combineWith(strList,';');
				effA.setMiscText(text);
				studA.setMiscText(text);
				for(final Ability A : effA.skillList)
				{
					if(A.ID().equalsIgnoreCase(abilityID))
						mob.delAbility(A);
				}
				if(studA.skillList!=null)
				{
					for(final Ability A : studA.skillList)
					{
						if(A.ID().equalsIgnoreCase(abilityID))
							mob.delAbility(A);
					}
				}
				return true;
			}
		}
		return false;
	}

	@Override
	public boolean autoInvocation(final MOB mob, final boolean force)
	{
		if(isAutoInvoked())
		{
			if(CMSecurity.isAllowedEverywhere(mob, SecFlag.ALLSKILLS))
				return false;
		}
		return super.autoInvocation(mob, force);
	}

	@Override
	public boolean okMessage(final Environmental myHost, final CMMsg msg)
	{
		if((msg.source()==affected)
		&&(msg.targetMinor()!=CMMsg.TYP_TEACH)
		&&(msg.tool() instanceof Ability)
		&&(skillList.contains(msg.tool())))
		{
			msg.source().tell(L("You don't know how to do that in practice."));
			return false;
		}
		else
		if((msg.target()==affected)
		&&(msg.targetMinor()==CMMsg.TYP_TEACH)
		&&(msg.tool() instanceof Ability))
			forget((MOB)msg.target(),msg.tool().ID());
		else
		if((msg.tool() instanceof Ability)
		&&(msg.source()==affected)
		&&(msg.targetMinor()==CMMsg.TYP_WROTE)
		&&(msg.targetMessage().length()>0)
		&&(msg.tool().ID().equals("Dissertating")))
		{
			forget(msg.source(),msg.targetMessage());
		}

		return super.okMessage(myHost,msg);
	}

	@Override
	public boolean tick(final Tickable ticking, final int tickID)
	{
		if((affected instanceof MOB)
		&&(tickID==Tickable.TICKID_MOB))
		{
			final MOB mob=(MOB)affected;
			if((!distributed)
			&&(affected instanceof MOB)
			&&(isNowAnAutoEffect()))
			{
				boolean doWorkOn = false;
				synchronized(skillList)
				{
					if(!distributed)
					{
						distributed=true;
						doWorkOn=true;
					}
				}
				if(doWorkOn)
				{
					distributeSkills(mob);
				}
			}
			if((this.teacherP != null)
			&&(this.teachingA!=null)
			&&(this.affected instanceof MOB))
			{
				final Physical teacher=this.teacherP;
				if((teacher == null)
				||(teacher == affected)
				||(mob.location()!=CMLib.map().roomLocation(teacher))
				||((teacher instanceof MOB)&&(((MOB)teacher).isInCombat()))
				||(mob.isInCombat())
				||(mob.location()!=activityRoom)
				||((teacher instanceof MOB)&&(!CMLib.flags().isAliveAwakeMobileUnbound((MOB)teacher,true)))
				||(!CMLib.flags().isAliveAwakeMobileUnbound(mob, true)))
				{
					aborted=true;
					unInvoke();
					return true;
				}
				if((--tickDown)<=0)
				{
					tickDown=-1;
					unInvoke();
					return true;
				}
				else
					tickUp++;
				super.tick(ticking, tickID);
			}
		}
		return true;
	}

	@Override
	public void unInvoke()
	{
		if((this.teacherP != null)
		&&(this.teachingA!=null)
		&&(this.activityRoom!=null)
		&&(this.affected instanceof MOB))
		{
			final MOB mob=(MOB)this.affected;
			final Ability teachingA=this.teachingA;
			this.teachingA=null;
			this.activityRoom=null;
			this.tickDown=Integer.MAX_VALUE/2;
			final MOB teacherM;
			if(this.teacherP instanceof MOB)
				teacherM=(MOB)this.teacherP;
			else
			{
				teacherM=CMClass.getFactoryMOB(teacherP.name(), 30, mob.location());
				teacherM.setAttribute(Attrib.NOTEACH, false);
				final Ability teacherMA=CMClass.getAbility(teachingA.ID());
				teacherMA.setProficiency(100);
				teacherM.addAbility(teacherMA);
			}
			final String teachingAName=(teachingA!=null)?teachingA.name():L("something");
			String doneMsgStr;
			if(aborted)
				doneMsgStr=L("<S-NAME> stop(s) learning @x1 from <T-NAME>.",teachingAName);
			else
				doneMsgStr=L("<S-NAME> <S-IS-ARE> done learning @x1 from <T-NAME>.",teachingAName);
			try
			{
				if((teachingA != null)
				&&(mob.location()!=null)
				&&(!aborted)
				&&(CMLib.map().roomLocation(teacherP)==mob.location()))
				{
					if(this.successfullyTaught)
					{
						final Ability A=mob.fetchAbility(ID());
						final Ability fA=mob.fetchEffect(ID());
						final Ability mTeachingA=teacherM.fetchAbility(teachingA.ID());
						if((A==null)||(fA==null)||(mTeachingA==null)||(!A.isSavable())||(!fA.isNowAnAutoEffect()))
							aborted=true;
						else
						{
							final StringBuilder str=new StringBuilder(A.text());
							if(str.length()>0)
								str.append(';');
							final int prof = mTeachingA.proficiency() + (5 * super.expertise(mob, mTeachingA, ExpertiseLibrary.Flag.LEVEL));
							str.append(mTeachingA.ID()).append(',').append(prof);
							fA.setMiscText(str.toString()); // and this should do it.
							A.setMiscText(str.toString()); // and this should be savable
						}
					}
					else
						mob.location().show(teacherM,mob,getActivityMessageType(),L("<S-NAME> fail(s) to teach <T-NAME> @x1.",teachingA.name()));
					// let super announce it
				}
			}
			finally
			{
				mob.location().show(mob,teacherM,getActivityMessageType(),doneMsgStr);
				if((teacherP instanceof Item)&&(!teacherM.isPlayer()))
					teacherM.destroy();
				this.teacherP=null;
			}
			helping=false;
			helpingAbility=null;
		}
	}

	public void distributeSkills(final MOB mob)
	{
		if(skillList.size() > 0)
		{
			for(final Ability a : skillList)
			{
				final Ability A=mob.fetchAbility(a.ID());
				if((A!=null)&&(!A.isSavable()))
				{
					mob.delAbility(A);
					final Ability fA=mob.fetchEffect(A.ID());
					if(fA!=null)
					{
						fA.unInvoke();
						mob.delEffect(fA);
					}
				}
			}
			skillList.clear();
		}
		for(final String as : CMParms.parseSemicolons(text(), false))
		{
			final List<String> idProf = CMParms.parseCommas(as, true);
			if(idProf.size()>1)
			{
				final Ability A=CMClass.getAbility(idProf.get(0));
				if(A!=null)
				{
					A.setSavable(false);
					A.setProficiency(CMath.s_int(idProf.get(1)));
					mob.addAbility(A);
					skillList.add(A);
				}
			}
		}
	}

	@Override
	public void unlearn(final MOB student)
	{
		if(student == null)
			return;
		final Studying studyA=(Studying)student.fetchEffect(ID());
		if(studyA!=null)
			studyA.delAllAbilities();
		delAllAbilities();
		super.unlearn(student);
	}

	public void confirmSkills(final MOB mob)
	{
		final Studying studyA=(Studying)mob.fetchEffect(ID());
		if((studyA!=null)
		&&(studyA.distributed))
		{
			boolean broken=false;
			for(final Ability A : skillList)
			{
				if(mob.fetchAbility(A.ID())==null)
					broken=true;
			}
			if(broken)
				studyA.distributeSkills(mob);
		}
	}

	@Override
	public boolean invoke(final MOB mob, final List<String> commands, final Physical givenTarget, final boolean auto, final int asLevel)
	{
		boolean quiet=false;
		if((commands.size()==1)&&(commands.get(0).equalsIgnoreCase("quiet")))
			quiet=commands.remove(0).equalsIgnoreCase("quiet");

		if(commands.size()==0)
		{
			confirmSkills(mob);
			if(!quiet)
			{
				mob.tell(L("You've been taught: "));
				final List<List<String>> taughts = CMParms.parseDoubleDelimited(text(), ';', ',');
				final StringBuilder str=new StringBuilder("");
				for(final List<String> l : taughts)
				{
					if(l.size()>1)
					{
						final Ability A=mob.fetchAbility(l.get(0));
						final Ability eA=mob.fetchEffect(l.get(0));
						final int prof=CMath.s_int(l.get(1));
						if((A!=null)&&(!A.isSavable()))
						{
							A.setProficiency(prof);
							if(eA!=null)
							{
								eA.unInvoke();
								mob.delEffect(eA);
							}
							str.append(CMStrings.padRight(L(Ability.ACODE_DESCS[A.classificationCode()&Ability.ALL_ACODES]), 12)+": "+A.Name()+"\n\r");
						}
					}
				}
				str.append("\n\rYou may learn ");
				for(int i=0;i<Ability.ACODE_DESCS.length;i++)
				{
					perLevelLimits limitObj = null;
					for(final perLevelLimits l : perLevelLimits.values())
					{
						if(l.doesRuleApplyTo(i))
							limitObj = l;
					}
					if(limitObj == null)
						continue;
					if((getSupportedSkillType()!=null) && (getSupportedSkillType()!=limitObj))
						continue;
					final int classLevel = CMLib.ableMapper().qualifyingClassLevel(mob, this);
					final int numAllowed = limitObj.numAllowed(classLevel);
					int numHas = 0;
					for(final List<String> l : taughts)
					{
						if(l.size()>0)
						{
							final Ability A1=CMClass.getAbility(l.get(0));
							if(limitObj.doesRuleApplyTo(A1))
								numHas++;
						}
					}
					str.append(numAllowed-numHas).append(" more ").append(CMLib.english().makePlural(Ability.ACODE_DESCS[i].toLowerCase())).append(", ");
				}
				final String fstr=str.toString();
				if(fstr.endsWith(", "))
					mob.tell(fstr.substring(0,fstr.length()-2)+".");
				else
					mob.tell(fstr);
			}
			return true;
		}
		if((commands.size()>0)&&(commands.size()<3))
		{
			final String combStr=CMParms.combine(commands);
			final List<List<String>> taughts = CMParms.parseDoubleDelimited(text(), ';', ',');
			for(int i=0;i<Ability.ACODE_DESCS.length;i++)
			{
				perLevelLimits limitObj = null;
				for(final perLevelLimits l : perLevelLimits.values())
				{
					if(l.doesRuleApplyTo(i))
						limitObj = l;
				}
				if(limitObj == null)
					continue;
				if((getSupportedSkillType()!=null) && (getSupportedSkillType()!=limitObj))
					continue;
				if(Ability.ACODE_DESCS[i].equalsIgnoreCase(combStr)
				||CMLib.english().makePlural(Ability.ACODE_DESCS[i].toLowerCase()).equalsIgnoreCase(combStr))
				{
					final int classLevel = CMLib.ableMapper().qualifyingClassLevel(mob, this);
					final int numAllowed = limitObj.numAllowed(classLevel);
					int numHas = 0;
					for(final List<String> l : taughts)
					{
						if(l.size()>0)
						{
							final Ability A1=CMClass.getAbility(l.get(0));
							if(limitObj.doesRuleApplyTo(A1))
								numHas++;
						}
					}
					final StringBuilder str=new StringBuilder("You may learn ");
					str.append(numAllowed-numHas).append(" more ").append(CMLib.english().makePlural(Ability.ACODE_DESCS[i].toLowerCase())).append(". ");
					str.append("\n\rAvailable ").append(CMLib.english().makePlural(Ability.ACODE_DESCS[i].toLowerCase())).append(" include: ");
					final List<String> all=new ArrayList<String>(100);
					for(final Enumeration<Ability> a=CMClass.abilities();a.hasMoreElements();)
					{
						final Ability A=a.nextElement();
						if((A!=null)
						&&((A.classificationCode()&Ability.ALL_ACODES)==i))
						{
							final int lowestQualifyingLevel = CMLib.ableMapper().lowestQualifyingLevel(A.ID());
							if((lowestQualifyingLevel <= classLevel)
							&&(lowestQualifyingLevel >= 1)
							&&(mob.fetchAbility(A.ID())==null))
							{
								all.add(A.name());
							}
						}
					}
					str.append(CMLib.english().toEnglishStringList(all));
					mob.tell(str.toString());
					return true;
				}
			}
		}
		if(commands.get(0).equalsIgnoreCase("FORGET") && (commands.size()>1))
		{
			commands.remove(0);
			final String name=CMParms.combine(commands);
			if(name.trim().length()==0)
				mob.tell(L("Forget what?",name));
			else
			{
				Ability A=(Ability)CMLib.english().fetchEnvironmental(skillList, name, true);
				if(A==null)
					A=(Ability)CMLib.english().fetchEnvironmental(skillList, name, false);
				if(A==null)
					A=CMClass.findAbility(name);
				if(A!=null)
				{
					if(forget(mob,A.ID()))
						mob.tell(L("You have forgotten @x1.",A.name()));
					else
						mob.tell(L("You haven't studied @x1.",A.name()));
				}
				else
					mob.tell(L("You never studied @x1.",name));
			}
			return true;
		}
		if(commands.size()<2)
		{
			mob.tell(L("Have who teach you what?"));
			return false;
		}
		final List<String> name = new XVector<String>(commands.remove(0));
		final String skillName = CMParms.combine(commands);
		final Physical target = super.getAnyTarget(mob, new XVector<String>(name), givenTarget, Wearable.FILTER_UNWORNONLY,false,true);
		final Ability A;
		final int teacherClassLevel;
		final int teacherQualifyingLevel;
		if(target instanceof Item)
		{
			if(!(target instanceof SpellHolder))
			{
				commonTell(mob,L("You aren't going to learn much from @x1.",target.Name()));
				return false;
			}
			if(target.ID().indexOf("issertation")<0)
			{
				commonTell(mob,L("You don't know how to learn from @x1.",target.Name()));
				return false;
			}
			final SpellHolder aC=(SpellHolder)target;
			Ability possA=(Ability)CMLib.english().fetchEnvironmental(aC.getSpells(),skillName,true);
			if(possA==null)
				possA=(Ability)CMLib.english().fetchEnvironmental(aC.getSpells(),skillName,false);
			if(possA==null)
			{
				commonTell(mob,L("@x1 doesn't seem to be about '@x2'.",target.Name(),skillName));
				return false;
			}
			A=possA;
			final int lowestQualifyingLevel = CMLib.ableMapper().lowestQualifyingLevel(A.ID());
			teacherClassLevel = (target.phyStats().level() > lowestQualifyingLevel) ? target.phyStats().level() : lowestQualifyingLevel;
			teacherQualifyingLevel = lowestQualifyingLevel;
		}
		else
		if(target instanceof MOB)
		{
			final MOB targetM=(MOB)target;
			if(target == mob)
			{
				mob.tell(L("You can't teach yourself."));
				return false;
			}
			if((targetM.isAttributeSet(MOB.Attrib.NOTEACH))
			&&((!targetM.isMonster())||(!targetM.willFollowOrdersOf(mob))))
			{
				mob.tell(L("@x1 is not accepting students right now.",target.name(mob)));
				return false;
			}
			A=CMClass.findAbility(skillName, targetM);
			if(A==null)
			{
				mob.tell(L("@x1 doesn't know '@x2'.",targetM.name(mob),skillName));
				return false;
			}
			teacherClassLevel = CMLib.ableMapper().qualifyingClassLevel(targetM, A);
			teacherQualifyingLevel = CMLib.ableMapper().qualifyingLevel(targetM, A);
		}
		else
		{
			if(target != null)
				commonTell(mob,L("You can't learn anything from '@x1'.",target.Name()));
			return false;
		}
		if(mob.fetchAbility(A.ID())!=null)
		{
			mob.tell(L("You already know @x1.",A.name()));
			return false;
		}
		final int lowestQualifyingLevel = CMLib.ableMapper().lowestQualifyingLevel(A.ID());
		final int classLevel = CMLib.ableMapper().qualifyingClassLevel(mob, this);
		if((classLevel >=0)
		&&(!auto)
		&&(classLevel < lowestQualifyingLevel))
		{
			mob.tell(L("You aren't qualified to be taught @x1.",A.Name()));
			return false;
		}
		if((teacherClassLevel <0)
		&&(!auto))
		{
			mob.tell(L("@x1 isn't qualified to teach @x2.",target.name(mob),A.Name()));
			return false;
		}
		perLevelLimits limitObj = null;
		for(final perLevelLimits l : perLevelLimits.values())
		{
			if(l.doesRuleApplyTo(A))
				limitObj = l;
		}
		if(limitObj == null)
		{
			mob.tell(L("You can not study that sort of skill."));
			return false;
		}
		if((getSupportedSkillType()!=null) && (getSupportedSkillType()!=limitObj))
		{
			mob.tell(L("You can not study that sort of skill with this one."));
			return false;
		}
		final int numAllowed = limitObj.numAllowed(classLevel);
		int numHas = 0;
		final List<List<String>> taughts = CMParms.parseDoubleDelimited(text(), ';', ',');
		for(final List<String> l : taughts)
		{
			if(l.size()>0)
			{
				final Ability A1=CMClass.getAbility(l.get(0));
				if(limitObj.doesRuleApplyTo(A1))
					numHas++;
			}
		}
		if(numHas >= numAllowed)
		{
			mob.tell(L("You may not study any more @x1 at this time.",CMLib.english().makePlural(Ability.ACODE_DESCS[A.classificationCode()&Ability.ALL_ACODES])));
			return false;
		}

		final Studying thisOne=(Studying)mob.fetchEffect(ID());
		if((thisOne.teacherP!=null)
		&&(thisOne.teachingA!=null)
		&&(thisOne.activityRoom!=null))
		{
			mob.tell(L("You are already @x1.",thisOne.verb));
			return false;
		}
		thisOne.aborted=false;
		if(!super.invoke(mob, commands, givenTarget, auto, asLevel))
			return false;

		final double quickPct = getXTIMELevel(mob) * 0.05;
		final int teachTicks = (int)(((teacherQualifyingLevel * 60000L)
							- (10000L * (teacherClassLevel-teacherQualifyingLevel))
							- (15000L * super.getXLEVELLevel(mob))) / CMProps.getTickMillis());
		final int duration=teachTicks-(int)Math.round(CMath.mul(teachTicks, quickPct));
		final long minutes = (duration * CMProps.getTickMillis() / 60000L);
		final long seconds = (duration * CMProps.getTickMillis() / 1000L);

		/*
			Training time should be (Skill's qualifying level by the teaching character in minutes
			minus 10 seconds per level the teacher has over that, minus 15 seconds per expertise the scholar has, with
			a minimum of 1 minute)
		*/
		successfullyTaught = super.proficiencyCheck(mob, 0, auto);
		{
			final Session sess = (target instanceof MOB)?((MOB)target).session() : null;
			if(target instanceof MOB)
				thisOne.verb=L("learning @x2 from @x1",target.name(),A.name());
			else
				thisOne.verb=L("studying @x2 from @x1",target.name(),A.name());
			thisOne.displayText=L("You are @x1",verb);
			final Physical P=target;
			final Room mobroom=mob.location();
			final boolean success=this.successfullyTaught;
			thisOne.activityRoom=mobroom;
			thisOne.teachingA=null;
			thisOne.teacherP=null;
			final Runnable R=new Runnable()
			{
				final MOB		M	= mob;
				final Ability	tA	= A;
				final Physical	tP	= P;
				final Studying	oA	= thisOne;
				final boolean	ss	= success;
				//final Room		mR	= mobroom;
				@Override
				public void run()
				{
					final String str=L("<S-NAME> start(s) learning @x1 from <T-NAME>.",tA.Name());
					final CMMsg msg=CMClass.getMsg(M,tP,oA,CMMsg.MSG_NOISYMOVEMENT|(auto?CMMsg.MASK_ALWAYS:0),str);
					final Room R=M.location();
					if(R!=null)
					{
						if(R.okMessage(M,msg))
						{
							R.send(M, msg);
							oA.teachingA=tA;
							oA.teacherP=tP;
							oA.successfullyTaught=ss;
							//oA.activityRoom=mR;
							int ticks=duration;
							if(ticks < 1)
								ticks = 1;
							ticks = getBeneficialTickdownTime(mob,mob,ticks,asLevel);
							oA.tickDown=ticks;
						}
					}
				}

			};
			if((!(target instanceof MOB)) || ((MOB)target).isMonster() || (sess==null))
				R.run();
			else
			{
				mob.tell(L("If @x1 agrees to teach you, you will begin studying together.",target.name(mob)));
				sess.prompt(new InputCallback(InputCallback.Type.CONFIRM,"N",0)
				{
					@Override
					public void showPrompt()
					{
						String timeStr;
						if(seconds<=0)
							timeStr = CMLib.lang().L("no time at all",""+seconds);
						else
						if(minutes<2)
							timeStr = CMLib.lang().L("around @x1 seconds",""+seconds);
						else
							timeStr = CMLib.lang().L("around @x1 minutes",""+minutes);
						if(P instanceof MOB)
							sess.promptPrint(L("\n\r@x1 wants you to try to teach @x2 about @x3. It will take @x4.  Is that OK (y/N)? ",
									mob.name((MOB)P), ((MOB)P).charStats().himher(), A.name(), timeStr));
					}

					@Override
					public void timedOut()
					{
					}

					@Override
					public void callBack()
					{
						if (this.input.equals("Y"))
						{
							try
							{
								R.run();
							}
							catch (final Exception e)
							{
							}
						}
					}
				});
			}
		}

		return true;
	}

	@Override
	public void addAbility(final Ability to)
	{
		throw new java.lang.UnsupportedOperationException();
	}

	@Override
	public void delAbility(final Ability to)
	{
		if(to==null)
			return;
		final List<String> strList = CMParms.parseSemicolons(text(), true);
		boolean removed=false;
		for(int i=0;i<strList.size();i++)
		{
			if(strList.get(i).startsWith(to.ID()+","))
			{
				strList.remove(i);
				removed = true;
				break;
			}
		}
		if(removed)
		{
			final String text=CMParms.combineWith(strList,';');
			for(final Ability A : skillList)
			{
				if(A.ID().equalsIgnoreCase(A.ID()))
				{
					skillList.remove(A);
				}
			}
			setMiscText(text);
		}
	}

	@Override
	public int numAbilities()
	{
		return skillList.size();
	}

	@Override
	public Ability fetchAbility(final int index)
	{
		return skillList.get(index);
	}

	@Override
	public Ability fetchAbility(final String ID)
	{
		for(final Ability A : skillList)
		{
			if(A.ID().equalsIgnoreCase(ID))
				return A;
		}
		return null;
	}

	@Override
	public Ability fetchRandomAbility()
	{
		if(numAbilities()==0)
			return null;
		return fetchAbility(CMLib.dice().roll(1, numAbilities(), -1));
	}

	@Override
	public Enumeration<Ability> abilities()
	{
		return new IteratorEnumeration<Ability>(skillList.iterator());
	}

	@Override
	public void delAllAbilities()
	{
		final XVector<Ability> all=new XVector<Ability>(skillList);
		for(final Ability A : all)
			delAbility(A);
	}

	@Override
	public int numAllAbilities()
	{
		return numAbilities();
	}

	@Override
	public Enumeration<Ability> allAbilities()
	{
		return abilities();
	}
}