/
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.CMSecurity.DbgFlag;
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.Fighter;
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.Common.interfaces.CMMsg.View;
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.MOBS.interfaces.MOB.Attrib;
import com.planet_ink.coffee_mud.Races.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;

import java.util.*;

/*
   Copyright 2004-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 MUDFight extends StdLibrary implements CombatLibrary
{
	@Override public String ID(){return "MUDFight";}
	static final char[] PARENS={'(',')'};

	public String 		lastStr			= "";
	public long 		lastRes			= 0;
	public Object[][][] hitWordIndex	= null;
	public Object[][][] hitWordsChanged	= null;
	
	protected LinkedList<CMath.CompiledOperation>	attackAdjustmentFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	armorAdjustmentFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	attackerFudgeBonusFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	pvpAttackerFudgeBonusFormula	= null;
	protected LinkedList<CMath.CompiledOperation>	spellFudgeDamageFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	pvpSpellFudgeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	spellCritChanceFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	pvpSpellCritChanceFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	spellCritDmgFormula				= null;
	protected LinkedList<CMath.CompiledOperation>	pvpSpellCritDmgFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	targetedRangedDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	pvpTargetedRangedDamageFormula	= null;
	protected LinkedList<CMath.CompiledOperation>	rangedFudgeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	pvpRangedFudgeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	targetedMeleeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	pvpTargetedMeleeDamageFormula	= null;
	protected LinkedList<CMath.CompiledOperation>	meleeFudgeDamageFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	pvpMeleeFudgeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	staticRangedDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	staticMeleeDamageFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	weaponCritChanceFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	pvpWeaponCritChanceFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	weaponCritDmgFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	pvpWeaponCritDmgFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	stateHitPointRecoverFormula		= null;
	protected LinkedList<CMath.CompiledOperation>	stateManaRecoverFormula			= null;
	protected LinkedList<CMath.CompiledOperation>	stateMovesRecoverFormula		= null;

	private static final int ATTACK_ADJUSTMENT = 50;

	@Override
	public boolean activate()
	{
		attackAdjustmentFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_ATTACKADJUSTMENT));
		armorAdjustmentFormula= CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_ARMORADJUSTMENT));
		attackerFudgeBonusFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_ATTACKFUDGEBONUS));
		pvpAttackerFudgeBonusFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPATTACKFUDGEBONUS));
		spellCritChanceFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_CHANCESPELLCRIT));
		pvpSpellCritChanceFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPCHANCESPELLCRIT));
		spellCritDmgFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGESPELLCRIT));
		pvpSpellCritDmgFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGESPELLCRIT));
		targetedRangedDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGERANGEDTARGETED));
		pvpTargetedRangedDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGERANGEDTARGETED));
		targetedMeleeDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGEMELEETARGETED));
		pvpTargetedMeleeDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGEMELEETARGETED));
		staticRangedDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGERANGEDSTATIC));
		staticMeleeDamageFormula=CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGEMELEESTATIC));
		weaponCritChanceFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_CHANCEWEAPONCRIT));
		pvpWeaponCritChanceFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPCHANCEWEAPONCRIT));
		weaponCritDmgFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGEWEAPONCRIT));
		pvpWeaponCritDmgFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGEWEAPONCRIT));
		spellFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGESPELLFUDGE));
		pvpSpellFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGESPELLFUDGE));
		meleeFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGEMELEEFUDGE));
		pvpMeleeFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGEMELEEFUDGE));
		rangedFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_DAMAGERANGEDFUDGE));
		pvpRangedFudgeDamageFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_PVPDAMAGERANGEDFUDGE));

		stateHitPointRecoverFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_HITPOINTRECOVER));
		stateManaRecoverFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_MANARECOVER));
		stateMovesRecoverFormula = CMath.compileMathExpression(CMProps.getVar(CMProps.Str.FORMULA_MOVESRECOVER));
		
		if(serviceClient==null)
		{
			name="THCombat"+Thread.currentThread().getThreadGroup().getName().charAt(0);
			serviceClient=CMLib.threads().startTickDown(this, Tickable.TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK, CMProps.getTickMillis(), 3);
		}
		return true;
	}

	@Override
	public void propertiesLoaded()
	{
		activate();
	}

	@Override
	public Set<MOB> allPossibleCombatants(MOB mob, boolean includePlayers)
	{
		final SHashSet<MOB> h=new SHashSet<MOB>();
		final Room thisRoom=mob.location();
		if(thisRoom==null)
			return null;
		final Set<MOB> h1=mob.getGroupMembers(new HashSet<MOB>());
		for(int m=0;m<thisRoom.numInhabitants();m++)
		{
			final MOB inhab=thisRoom.fetchInhabitant(m);
			if((inhab!=null)
			&&(inhab!=mob)
			&&(!h1.contains(inhab))
			&&(CMLib.flags().isSeeable(inhab)||CMLib.flags().canMove(inhab))
			&&(CMLib.flags().isSeeable(inhab)||(!CMLib.flags().isCloaked(inhab)))
			&&((includePlayers)||(!mob.isMonster())||(!inhab.isMonster())))
				h.addUnsafe(inhab);
		}
		return h;
	}

	@Override
	public Set<MOB> properTargets(Ability A, MOB caster, boolean includePlayers)
	{
		Set<MOB> h=null;
		if(A.abstractQuality()!=Ability.QUALITY_MALICIOUS)
		{
			if(caster.Name().equalsIgnoreCase("somebody"))
				h=new SHashSet<MOB>();
			else
				h=caster.getGroupMembers(new SHashSet<MOB>());
			for (final MOB M : h)
			{
				if(M.location()!=caster.location())
					h.remove(M);
			}
		}
		else
		if(caster.isInCombat())
			h=allCombatants(caster);
		else
			h=allPossibleCombatants(caster,includePlayers);
		return h;
	}

	@Override
	public int adjustedAttackBonus(MOB mob, MOB target)
	{
		final int maxStr = mob.charStats().getMaxStat(CharStats.STAT_STRENGTH);
		int currStr = mob.charStats().getStat(CharStats.STAT_STRENGTH);
		int strBonus = 0;
		if(currStr > maxStr)
		{
			strBonus = currStr - maxStr;
			currStr = maxStr;
		}
		int baseStr = mob.baseCharStats().getStat(CharStats.STAT_STRENGTH);
		if(baseStr > maxStr)
			baseStr = maxStr;
		final double[] vars = {mob.phyStats().attackAdjustment(),
						 currStr,
						 baseStr,
						 strBonus,
						 (mob.curState().getHunger()<1)?1.0:0.0,
						 (mob.curState().getThirst()<1)?1.0:0.0,
						 (mob.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0
						};
		return (int)Math.round(CMath.parseMathExpression(attackAdjustmentFormula, vars, 0.0));
	}

	@Override
	public void postItemDamage(MOB mob, Item I, Environmental tool, int damageAmount, int messageType, String message)
	{
		if(mob==null)
			return ;
		final Room R=mob.location();
		if(R==null)
			return ;
		CMMsg msg=CMClass.getMsg(mob,I,tool,CMMsg.MASK_MALICIOUS|CMMsg.MASK_ALWAYS|messageType,message,CMMsg.MASK_MALICIOUS|CMMsg.MASK_ALWAYS|messageType,message,CMMsg.NO_EFFECT,null);
		if(R.okMessage(mob,msg))
		{
			R.send(mob,msg);
			if(msg.value()<=0)
			{
				if(I.subjectToWearAndTear())
				{
					I.setUsesRemaining(I.usesRemaining()-damageAmount);
					I.recoverPhyStats(); // important relation to setuses -- for brittle
					if(I.usesRemaining()<=0)
					{
						I.setUsesRemaining(100);
						I.unWear();
						msg=CMClass.getMsg(mob,null,I,CMMsg.MSG_OK_VISUAL,L("<O-NAME> is destroyed!"),null,L("<O-NAME> carried by <S-NAME> is destroyed!"));
						if(R.okMessage(mob,msg))
							R.send(mob,msg);
						I.destroy();
						mob.recoverPhyStats();
						mob.recoverCharStats();
						mob.recoverMaxState();
						R.recoverRoomStats();
					}
					else
					if(I.usesRemaining()<=10)
					{
						mob.tell(L("@x1 is looking really bad.",I.name(mob)));
					}
				}
				else
				if(damageAmount > 0)
				{
					I.unWear();
					msg=CMClass.getMsg(mob,null,I,CMMsg.MSG_OK_VISUAL,L("<O-NAME> is destroyed!"),null,L("<O-NAME> carried by <S-NAME> is destroyed!"));
					if(R.okMessage(mob,msg))
						R.send(mob,msg);
					I.destroy();
					mob.recoverPhyStats();
					mob.recoverCharStats();
					mob.recoverMaxState();
					R.recoverRoomStats();
				}
			}
		}
	}

	@Override
	public boolean doTurnBasedCombat(final MOB mob, final Room R, final Area A)
	{
		int index = R.getCombatTurnMobIndex();
		MOB M=null;
		if(mob.actions() < 1.0)
		{
			if((index >= R.numInhabitants())||((M=R.fetchInhabitant(index))==mob)||(M==null)||(!M.isInCombat()))
			{
				if((index<0)||(index>=R.numInhabitants()-1))
					index=-1;
				for(index++;index<R.numInhabitants();index++)
				{
					M=R.fetchInhabitant(index);
					if((M!=null)&&(M.isInCombat()))
					{
						M.setActions((M.actions() - Math.floor(M.actions())) + (CMLib.flags().isSitting(M) ? M.phyStats().speed() / 2.0 : M.phyStats().speed()));
						R.setCombatTurnMobIndex(index);
						break;
					}
				}
				return false;
			}
			else
			{
				return true;
			}
		}
		else
		if((index < R.numInhabitants())&&((M=R.fetchInhabitant(index))!=null)&&(M!=mob)&&(M.isInCombat()))
			mob.setActions(mob.actions()-Math.floor(mob.actions()));
		return false;
	}

	@Override
	public int adjustedArmor(MOB mob)
	{
		int currDex=mob.charStats().getStat(CharStats.STAT_DEXTERITY);
		final int maxDex = mob.charStats().getMaxStat(CharStats.STAT_DEXTERITY);
		int dexBonus = 0;
		if(currDex > maxDex)
		{
			dexBonus = currDex - maxDex;
			currDex = maxDex;
		}
		double baseDex=mob.baseCharStats().getStat(CharStats.STAT_DEXTERITY);
		if(baseDex > maxDex)
			baseDex = maxDex;

		final double[] vars = {
				mob.phyStats().armor(),
				currDex,
				baseDex,
				dexBonus,
				(mob.curState().getHunger()<1)?1.0:0.0,
				(mob.curState().getThirst()<1)?1.0:0.0,
				(mob.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
				CMLib.flags().isSitting(mob)?0.0:1.0,
				CMLib.flags().isSleeping(mob)?0.0:1.0
				};
		return (int)Math.round(CMath.parseMathExpression(armorAdjustmentFormula, vars, 0.0));
	}

	@Override
	public boolean rollToHit(MOB attacker, MOB defender)
	{
		if((attacker==null)||(defender==null))
			return false;
		final double vars[] = {
			attacker.phyStats().level(),
			defender.phyStats().level(),
			attacker.phyStats().level() > defender.phyStats().level() ? 1 : -1
		};
		final boolean isPVP=(attacker.isPlayer()&&defender.isPlayer());
		final int attackerFudgeBonusAmt = (int)Math.round(CMath.parseMathExpression(isPVP?pvpAttackerFudgeBonusFormula:attackerFudgeBonusFormula, vars, 0.0));
		return rollToHit(adjustedAttackBonus(attacker,defender),adjustedArmor(defender),attackerFudgeBonusAmt);
	}

	@Override
	public boolean rollToHit(int attack, int defence, int adjustment)
	{
		double myArmor= -((double)defence);
		if(myArmor==0)
			myArmor=1.0;
		else
		if(myArmor<0.0)
			myArmor=-CMath.div(1.0,myArmor);
		double hisAttack=attack;
		if(hisAttack==0.0)
			hisAttack=1.0;
		else
		if(hisAttack<0.0)
			hisAttack=-CMath.div(1.0,myArmor);
		return CMLib.dice().normalizeAndRollLess((int)Math.round(50.0*(hisAttack/myArmor)) + adjustment);
	}

	@Override
	public Set<MOB> allCombatants(MOB mob)
	{
		final SHashSet<MOB> h=new SHashSet<MOB>();
		final Room thisRoom=mob.location();
		if(thisRoom==null)
			return null;
		if(!mob.isInCombat())
			return null;

		Set<MOB> h1=null;
		if(mob.Name().equalsIgnoreCase("nobody"))
			h1=new HashSet<MOB>();
		else
			h1=mob.getGroupMembers(new HashSet<MOB>());
		for(int m=0;m<thisRoom.numInhabitants();m++)
		{
			final MOB inhab=thisRoom.fetchInhabitant(m);
			if((inhab!=null)
			&&(inhab!=mob)
			&&((inhab==mob.getVictim())
				||((inhab!=mob)
					&&(inhab.getVictim()!=mob.getVictim())
					&&(CMLib.flags().isSeeable(inhab)||(!CMLib.flags().isCloaked(inhab)))
					&&(CMLib.flags().canMove(inhab)||CMLib.flags().isSeeable(inhab))
					&&(!h1.contains(inhab)))))
				 h.addUnsafe(inhab);
		}
		return h;

	}

	@Override
	public void makePeaceInGroup(MOB mob)
	{
		final Set<MOB> myGroup=mob.getGroupMembers(new HashSet<MOB>());
		for (final MOB mob2 : myGroup)
		{
			if(mob2.isInCombat()&&(myGroup.contains(mob2.getVictim())))
				mob2.makePeace(true);
		}
	}

	@Override
	public void postPanic(MOB mob, CMMsg addHere)
	{
		if(mob==null)
			return;

		// make sure he's not already dead, or with a pending death.
		if(mob.amDead())
			return;
		if((addHere!=null)&&(addHere.trailerMsgs()!=null))
			for(final CMMsg msg : addHere.trailerMsgs())
				if((msg.source()==mob)
				&&((msg.sourceMinor()==CMMsg.TYP_PANIC))
				   ||(msg.sourceMinor()==CMMsg.TYP_DEATH))
					return;
		final CMMsg msg=CMClass.getMsg(mob,null,CMMsg.MSG_PANIC,null);
		if(addHere!=null)
			addHere.addTrailerMsg(msg);
		else
		if((mob.location()!=null)&&(mob.location().okMessage(mob,msg)))
			mob.location().send(mob,msg);
	}

	@Override
	public void postDeath(MOB killerM, MOB deadM, CMMsg addHere)
	{
		if(deadM==null)
			return;
		final Room deathRoom=deadM.location();
		if(deathRoom==null)
			return;

		// make sure he's not already dead, or with a pending death.
		if(deadM.amDead())
			return;
		if((addHere!=null)&&(addHere.trailerMsgs()!=null))
		{
			for(final CMMsg msg : addHere.trailerMsgs())
			{
				if((msg.source()==deadM)
				&&((msg.sourceMinor()==CMMsg.TYP_PANIC))
				   ||(msg.sourceMinor()==CMMsg.TYP_DEATH))
					return;
			}
		}

		final String msp=CMLib.protocol().msp("death"+CMLib.dice().roll(1,7,0)+".wav",50);
		CMMsg msg=null;
		if(isKnockedOutUponDeath(deadM,killerM))
		{
			msg=CMClass.getMsg(deadM,null,killerM,
					CMMsg.MSG_OK_VISUAL,L("^f^*^<FIGHT^>!!!!!!!!!YOU ARE DEFEATED!!!!!!!!!!^</FIGHT^>^?^.\n\r@x1",msp),
					CMMsg.MSG_OK_VISUAL,null,
					CMMsg.MSG_DEATH,L("^F^<FIGHT^><S-NAME> is DEFEATED!!!^</FIGHT^>^?\n\r@x1",msp));
		}
		else
		{
			msg=CMClass.getMsg(deadM,null,killerM,
				CMMsg.MSG_OK_VISUAL,L("^f^*^<FIGHT^>!!!!!!!!!!!!!!YOU ARE DEAD!!!!!!!!!!!!!!^</FIGHT^>^?^.\n\r@x1",msp),
				CMMsg.MSG_OK_VISUAL,null,
				CMMsg.MSG_DEATH,L("^F^<FIGHT^><S-NAME> is DEAD!!!^</FIGHT^>^?\n\r@x1",msp));
		}
		CMLib.map().sendGlobalMessage(deadM,CMMsg.TYP_DEATH, CMClass.getMsg(deadM,null,killerM, CMMsg.TYP_DEATH,null, CMMsg.TYP_DEATH,null, CMMsg.TYP_DEATH,null));
		final CMMsg msg2=CMClass.getMsg(deadM,null,killerM, CMMsg.MSG_DEATH,null, CMMsg.MSG_DEATH,null, CMMsg.MSG_DEATH,null);
		if(addHere!=null)
		{
			if(deathRoom.okMessage(deadM,msg2))
			{
				addHere.addTrailerMsg(msg);
				addHere.addTrailerMsg(msg2);
			}
		}
		else
		if(deathRoom.okMessage(deadM,msg))
		{
			deathRoom.send(deadM,msg);
			if(deathRoom.okMessage(deadM,msg2))
				deathRoom.send(deadM,msg2);
		}
	}

	@Override
	public boolean postAttack(MOB attacker, MOB target, Item weapon)
	{
		if((attacker==null)||(!attacker.mayPhysicallyAttack(target)))
			return false;
		if((weapon==null)
		&&(attacker.isAttributeSet(MOB.Attrib.AUTODRAW)))
		{
			CMLib.commands().postDraw(attacker,false,true);
			weapon=attacker.fetchWieldedItem();
		}
		final CMMsg msg=CMClass.getMsg(attacker,target,weapon,CMMsg.MSG_WEAPONATTACK,null);
		final Room R=target.location();
		if(R!=null)
		{
			if(R.okMessage(attacker,msg))
			{
				R.send(attacker,msg);
				return msg.value()>0;
			}
		}
		return false;
	}

	protected static boolean ownerSecurityCheck(final String ownerName, final MOB mob)
	{
		return (ownerName.length()>0)
			 &&(mob!=null)
			 &&((mob.Name().equals(ownerName))
				||(mob.getLiegeID().equals(ownerName)&mob.isMarriedToLiege())
				||(CMLib.clans().checkClanPrivilege(mob, ownerName, Clan.Function.PROPERTY_OWNER)));
	}

	@Override
	public boolean mayIAttack(final MOB mob, final Rideable attacker, final Rideable defender)
	{
		final String attackerOwnerName = (attacker instanceof PrivateProperty) ? ((PrivateProperty)attacker).getOwnerName() : "";  
		final String defenderOwnerName = (defender instanceof PrivateProperty) ? ((PrivateProperty)defender).getOwnerName() : "";  
		// is this how we determine npc ships?
		if((defenderOwnerName == null)||(defenderOwnerName.length()==0))
			return true;
		if(((attackerOwnerName == null)||(attackerOwnerName.length()==0)) 
		&& (mob.isMonster()))
			return true;
		if(CMSecurity.isASysOp(mob) && mob.isAttributeSet(Attrib.PLAYERKILL))
			return true;
		if(defender instanceof BoardableShip)
		{
			final Area otherArea = ((BoardableShip)defender).getShipArea();
			if(otherArea != null)
			{
				for(Enumeration<Room> r=otherArea.getProperMap();r.hasMoreElements();)
				{
					final Room R=r.nextElement();
					if(R!=null)
					{
						for(Enumeration<MOB> m=R.inhabitants();m.hasMoreElements();)
						{
							final MOB M=m.nextElement();
							if((M!=null)
							&&(ownerSecurityCheck(defenderOwnerName,M))
							&&(mob.mayIFight(M)))
							{
								return true;
							}
						}
					}
				}
			}
		}
		else
		{
			for(int i=0;i<defender.numRiders();i++)
			{
				Rider R=defender.fetchRider(i);
				if((R instanceof MOB)
				&&(mob.mayIFight((MOB)R)))
				{
					return true;
				}
			}
		}
		return false;
	}
	
	@Override
	public boolean postAttack(MOB attacker, Rideable attackingShip, Rideable target, Weapon weapon, boolean wasAHit)
	{
		if((attacker==null)||(!mayIAttack(attacker,attackingShip, target))||(weapon==null))
			return false;
		final CMMsg msg=CMClass.getMsg(attacker,target,weapon,CMMsg.MSG_WEAPONATTACK,null);
		final Room R=CMLib.map().roomLocation(target);
		if(R!=null)
		{
			msg.setValue(wasAHit?1:0);
			if(R.okMessage(attacker,msg))
			{
				R.send(attacker,msg);
				return wasAHit;
			}
		}
		return false;
	}

	@Override
	public boolean postHealing(MOB healer,
							   MOB target,
							   Ability tool,
							   int healing,
							   int messageCode,
							   String allDisplayMessage)
	{
		if(healer==null)
			healer=target;
		if((healer==null)||(target==null)||(target.location()==null))
			return false;
		final CMMsg msg=CMClass.getMsg(healer,target,tool,messageCode,CMMsg.MSG_HEALING,messageCode,allDisplayMessage);
		msg.setValue(healing);
		final Room R=target.location();
		if(R!=null)
		{
			if(R.okMessage(target,msg))
			{ 
				R.send(target,msg); 
				return true;
			}
		}
		return false;
	}

	@Override
	public String replaceDamageTag(String str, int damage, int damageType, View sourceTargetSTO)
	{
		if(str==null)
			return null;
		final int replace=str.indexOf("<DAMAGE");
		if(replace < 0)
			return str;
		if(str.length() < replace+9)
			return str;
		final boolean damages = (str.charAt(replace+7)=='S') && (str.charAt(replace+8)=='>');
		final String showDamage = CMProps.getVar(CMProps.Str.SHOWDAMAGE);
		final boolean showNumbers = showDamage.equalsIgnoreCase("YES")
								||((sourceTargetSTO==CMMsg.View.SOURCE)&&showDamage.equalsIgnoreCase("SOURCE"))
								||((sourceTargetSTO==CMMsg.View.TARGET)&&showDamage.equalsIgnoreCase("TARGET"));
		if(damages)
		{
			final String hitWord=CMStrings.deleteAllofAny(standardHitWord(damageType,damage),PARENS);
			if(!showNumbers)
				return str.substring(0,replace)+hitWord+str.substring(replace+9);
			return str.substring(0,replace)+hitWord+" ("+damage+")"+ str.substring(replace+9);
		}
		else
		if(str.charAt(replace+7)=='>')
		{
			if(!showNumbers)
				return str.substring(0,replace)+standardHitWord(damageType,damage)+str.substring(replace+8);
			return str.substring(0,replace)+standardHitWord(damageType,damage)+" ("+damage+")"+ str.substring(replace+8);
		}
		return str;
	}
	
	@Override
	public void forcePeaceAllFightingAgainst(final MOB mob, final Set<MOB> exceptionSet)
	{
		final Room R=mob.location();
		if(R==null)
			return;
		for(Enumeration<MOB> m=R.inhabitants();m.hasMoreElements();)
		{
			final MOB M=m.nextElement();
			if((M!=null)
			&&(M.getVictim()==mob)
			&&(M!=mob)
			&&((exceptionSet==null)||(!exceptionSet.contains(M))))
				M.setVictim(null);
		}
	}
	
	@Override
	public Set<MOB> getAllFightingAgainst(final MOB mob, Set<MOB> set)
	{
		if(set == null)
			set=new HashSet<MOB>(1);
		final Room R=mob.location();
		if(R==null)
			return set;
		for(Enumeration<MOB> m=R.inhabitants();m.hasMoreElements();)
		{
			final MOB M=m.nextElement();
			if((M!=null)&&(M.getVictim()==mob)&&(M!=mob))
				set.add(M);
		}
		return set;
	}
	
	@Override
	public void postDamage(MOB attacker, MOB target, Environmental weapon, int damage, int messageCode, int damageType, String allDisplayMessage)
	{
		if((attacker==null)||(target==null)||(target.location()==null))
			return;
		if(allDisplayMessage!=null)
			allDisplayMessage="^F^<FIGHT^>"+allDisplayMessage+"^</FIGHT^>^?";

		final int damageTypeMsg;
		if(attacker != target)
		{
			if((weapon instanceof Ability)
			&&(damage>0)
			&&(attacker.isMine(weapon)||(attacker.phyStats().level()>1))) // why >1? because quickly made fake-mobs tend to have lvl=1
				damage = modifySpellDamage(attacker, target, damage);
			damageTypeMsg = CMMsg.TYP_DAMAGE | (CMMsg.MASK_MALICIOUS & messageCode);
		}
		else
			damageTypeMsg = CMMsg.TYP_DAMAGE;

		final CMMsg msg=CMClass.getMsg(attacker,target,weapon,messageCode,damageTypeMsg,messageCode,allDisplayMessage);
		msg.setValue(damage);
		CMLib.color().fixSourceFightColor(msg);
		final Room R=target.location();
		if(R!=null)
		{
			if(R.okMessage(target,msg))
			{
				if(damageType>=0)
				{
					msg.modify(msg.source(),
							   msg.target(),
							   msg.tool(),
							   msg.sourceCode(),
							   replaceDamageTag(msg.sourceMessage(),msg.value(),damageType,CMMsg.View.SOURCE),
							   msg.targetCode(),
							   replaceDamageTag(msg.targetMessage(),msg.value(),damageType,CMMsg.View.TARGET),
							   msg.othersCode(),
							   replaceDamageTag(msg.othersMessage(),msg.value(),damageType,CMMsg.View.OTHERS));
				}
				R.send(target,msg);
			}
		}
	}

	public int modifySpellDamage(MOB attacker, MOB target, int baseDamage)
	{
		final int maxInt = attacker.charStats().getMaxStat(CharStats.STAT_INTELLIGENCE);
		int currInt = attacker.charStats().getStat(CharStats.STAT_INTELLIGENCE);
		int intBonus = 0;
		if(currInt > maxInt)
		{
			intBonus = currInt - maxInt;
			currInt = maxInt;
		}
		int baseInt = attacker.baseCharStats().getStat(CharStats.STAT_INTELLIGENCE);
		if(baseInt > maxInt)
			baseInt = maxInt;
		final double[] vars = {
				baseDamage,
				currInt,
				baseInt,
				intBonus,
				(attacker.curState().getHunger()<1)?1.0:0.0,
				(attacker.curState().getThirst()<1)?1.0:0.0,
				(attacker.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
				attacker.phyStats().level(),
				target.phyStats().level()
				};
		final boolean isPVP=(attacker.isPlayer()&&target.isPlayer());
		baseDamage = (int)Math.round(CMath.parseMathExpression(isPVP?pvpSpellFudgeDamageFormula:spellFudgeDamageFormula, vars, 0.0));
		vars[0]=baseDamage;
		final int spellCritChancePct = (int)Math.round(CMath.parseMathExpression(isPVP?pvpSpellCritChanceFormula:spellCritChanceFormula, vars, 0.0));
		if(CMLib.dice().rollPercentage()<spellCritChancePct)
		{
			final int spellCritDamageAmt = (int)Math.round(CMath.parseMathExpression(isPVP?pvpSpellCritDmgFormula:spellCritDmgFormula, vars, 0.0));
			baseDamage+=spellCritDamageAmt;
		}
		return baseDamage;
	}

	@Override
	public int adjustedDamage(MOB mob, Weapon weapon, MOB target, int bonusDamage, boolean allowCrits)
	{
		double damageAmount=0.0;
		Physical useDmg = null;
		boolean rangedAttack = false;
		if((weapon!=null)
		&&((weapon.weaponClassification()==Weapon.CLASS_RANGED)||(weapon.weaponClassification()==Weapon.CLASS_THROWN)))
		{
			useDmg = weapon;
			rangedAttack = true;
		}
		else
			useDmg = mob;
		final boolean isPVP=((target!=null)&&mob.isPlayer()&&target.isPlayer());
		if(target!=null)
		{
			final double[] vars = {
					useDmg.phyStats().damage()+bonusDamage,
					mob.charStats().getStat(CharStats.STAT_STRENGTH),
					mob.phyStats().level(),
					target.phyStats().level(),
					(mob.curState().getHunger()<1)?1.0:0.0,
					(mob.curState().getThirst()<1)?1.0:0.0,
					(mob.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
					CMLib.flags().canBeSeenBy(target,mob)?0:1,
					CMLib.flags().isSleeping(target)?1:0,
					CMLib.flags().isSitting(target)?1:0
			};
			if(rangedAttack)
				damageAmount = CMath.parseMathExpression(isPVP?pvpTargetedRangedDamageFormula:targetedRangedDamageFormula, vars, 0.0);
			else
				damageAmount = CMath.parseMathExpression(isPVP?pvpTargetedMeleeDamageFormula:targetedMeleeDamageFormula, vars, 0.0);
		}
		else
		{
			final double[] vars = {
					useDmg.phyStats().damage()+bonusDamage,
					mob.charStats().getStat(CharStats.STAT_STRENGTH),
					mob.phyStats().level(),
					0,
					(mob.curState().getHunger()<1)?1.0:0.0,
					(mob.curState().getThirst()<1)?1.0:0.0,
					(mob.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
					0,
					0,
					0
			};
			if((weapon!=null)&&((weapon.weaponClassification()==Weapon.CLASS_RANGED)||(weapon.weaponClassification()==Weapon.CLASS_THROWN)))
				damageAmount = CMath.parseMathExpression(staticRangedDamageFormula, vars, 0.0);
			else
				damageAmount = CMath.parseMathExpression(staticMeleeDamageFormula, vars, 0.0);
		}

		final int maxDex = mob.charStats().getMaxStat(CharStats.STAT_DEXTERITY);
		int currDex = mob.charStats().getStat(CharStats.STAT_DEXTERITY);
		int dexBonus = 0;
		if(currDex > maxDex)
		{
			dexBonus = currDex - maxDex;
			currDex = maxDex;
		}
		int baseDex = mob.baseCharStats().getStat(CharStats.STAT_DEXTERITY);
		if(baseDex > maxDex)
			baseDex = maxDex;

		final double[] vars = {
				damageAmount,
				currDex,
				baseDex,
				dexBonus,
				(mob.curState().getHunger()<1)?1.0:0.0,
				(mob.curState().getThirst()<1)?1.0:0.0,
				(mob.curState().getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
				mob.phyStats().level(),
				(target==null)?0:target.phyStats().level()
		};
		if(allowCrits)
		{
			final int weaponCritChancePct = (int)Math.round(CMath.parseMathExpression(isPVP?pvpWeaponCritChanceFormula:weaponCritChanceFormula, vars, 0.0));
			if(CMLib.dice().rollPercentage()<weaponCritChancePct)
			{
				final int weaponCritDmgAmt = (int)Math.round(CMath.parseMathExpression(isPVP?pvpWeaponCritDmgFormula:weaponCritDmgFormula, vars, 0.0));
				damageAmount += weaponCritDmgAmt;
			}
		}
		if(target != null)
		{
			vars[0] = damageAmount;
			if(rangedAttack)
				damageAmount = CMath.parseMathExpression(isPVP?pvpRangedFudgeDamageFormula:rangedFudgeDamageFormula, vars, 0.0);
			else
				damageAmount = CMath.parseMathExpression(isPVP?pvpMeleeFudgeDamageFormula:meleeFudgeDamageFormula, vars, 0.0);
		}
		return (int)Math.round(damageAmount);
	}


	@Override
	public void recoverTick(MOB mob)
	{
		if((mob!=null)
		&&(!mob.isInCombat())
		&&(!CMLib.flags().isClimbing(mob)))
		{
			final CharStats charStats=mob.charStats();
			final CharState curState=mob.curState();
			final CharState maxState=mob.maxState();
			if((curState.getHitPoints()<maxState.getHitPoints())
			||(curState.getMana()<maxState.getMana())
			||(curState.getMovement()<maxState.getMovement()))
			{
				final boolean isSleeping=(CMLib.flags().isSleeping(mob));
				final boolean isSittingOrRiding=(!isSleeping) && ((CMLib.flags().isSitting(mob))||(mob.riding()!=null));
				final boolean isFlying=(!isSleeping) && (!isSittingOrRiding) && CMLib.flags().isFlying(mob);
				final boolean isSwimming=(!isSleeping) && (!isSittingOrRiding) && (!isFlying) && CMLib.flags().isSwimming(mob);
				final double[] vals=new double[]{
					charStats.getStat(CharStats.STAT_CONSTITUTION),
					mob.phyStats().level(),
					(curState.getHunger()<1)?1.0:0.0,
					(curState.getThirst()<1)?1.0:0.0,
					(curState.getFatigue()>CharState.FATIGUED_MILLIS)?1.0:0.0,
					isSleeping?1.0:0.0,
					isSittingOrRiding?1.0:0.0,
					isFlying?1.0:0.0,
					isSwimming?1.0:0.0
				};
				final double hpGain = CMath.parseMathExpression(stateHitPointRecoverFormula, vals, 0.0);
	
				if((hpGain>0)&&(!CMLib.flags().isGolem(mob)))
					curState.adjHitPoints((int)Math.round(hpGain),maxState);
	
				vals[0]=((charStats.getStat(CharStats.STAT_INTELLIGENCE)+charStats.getStat(CharStats.STAT_WISDOM)));
				final double manaGain = CMath.parseMathExpression(stateManaRecoverFormula, vals, 0.0);
	
				if(manaGain>0)
					curState.adjMana((int)Math.round(manaGain),maxState);
	
				vals[0]=charStats.getStat(CharStats.STAT_STRENGTH);
				final double moveGain = CMath.parseMathExpression(this.stateMovesRecoverFormula, vals, 0.0);
	
				if(moveGain>0)
					curState.adjMovement((int)Math.round(moveGain),maxState);
			}
		}
	}

	@Override
	public void postWeaponDamage(MOB source, MOB target, Item item, int damageInt)
	{
		int damageType=Weapon.TYPE_BASHING;
		Weapon weapon=null;
		if(item instanceof Weapon)
		{
			weapon=(Weapon)item;
			damageType=weapon.weaponDamageType();
		}
		// calculate Base Damage (with Strength bonus)
		final String oldHitString="^F^<FIGHT^>"+((weapon!=null)?
							weapon.hitString(damageInt):
							standardHitString(Weapon.TYPE_NATURAL,Weapon.CLASS_BLUNT,damageInt,item.name()))+"^</FIGHT^>^?";
		final CMMsg msg=CMClass.getMsg(source,
								target,
								item,
								CMMsg.MSG_OK_VISUAL,
								CMMsg.MSG_DAMAGE,
								CMMsg.MSG_OK_VISUAL,
								oldHitString);
		CMLib.color().fixSourceFightColor(msg);

		msg.setValue(damageInt);
		// why was there no okaffect here?
		final Room room=source.location();
		if((room!=null)&&(room.okMessage(source,msg)))
		{
			if(msg.targetMinor()==CMMsg.TYP_DAMAGE)
			{
				damageInt=msg.value();
				msg.modify(msg.source(),
						   msg.target(),
						   msg.tool(),
						   msg.sourceCode(),
						   replaceDamageTag(msg.sourceMessage(),msg.value(),damageType,CMMsg.View.SOURCE),
						   msg.targetCode(),
						   replaceDamageTag(msg.targetMessage(),msg.value(),damageType,CMMsg.View.TARGET),
						   msg.othersCode(),
						   replaceDamageTag(msg.othersMessage(),msg.value(),damageType,CMMsg.View.OTHERS));
			}
			if((source.mayIFight(target))
			&&(source.location()==room)
			&&(target.location()==room))
				room.send(source,msg);
		}
	}

	@Override
	public void postWeaponAttackResult(MOB source, MOB target, Item item, boolean success)
	{
		if(source==null)
			return;
		if(!source.mayIFight(target))
			return;
		Weapon weapon=null;
		int damageInt = 0;
		if(item instanceof Weapon)
		{
			weapon=(Weapon)item;
			damageInt=adjustedDamage(source,weapon,target,0,true);
		}
		if(success)
			postWeaponDamage(source,target,item,damageInt);
		else
		{
			final String missString="^F^<FIGHT^>"+((weapon!=null)?
								weapon.missString():
								standardMissString(Weapon.TYPE_BASHING,Weapon.CLASS_BLUNT,item.name(),false))+"^</FIGHT^>^?";
			final CMMsg msg=CMClass.getMsg(source,
									target,
									weapon,
									CMMsg.MSG_NOISYMOVEMENT,
									missString);
			CMLib.color().fixSourceFightColor(msg);
			// why was there no okaffect here?
			final Room R=source.location();
			if(R!=null)
			if(R.okMessage(source,msg) && (!source.amDead()) && (!source.amDestroyed()))
				R.send(source,msg);
		}
	}

	@Override
	public void postWeaponAttackResult(MOB source, Rideable attacker, Rideable defender, Weapon weapon, boolean success)
	{
		if(source==null)
			return;
		if(!mayIAttack(source, attacker, defender))
			return;
		int damageInt=adjustedDamage(source,weapon,null,0,false);
		int damageType=Weapon.TYPE_BASHING;
		damageType=weapon.weaponDamageType();
		final Room room=CMLib.map().roomLocation(attacker);
		if(success)
		{
			// calculate Base Damage (with Strength bonus)
			final String oldHitString="^F^<FIGHT^>"+((weapon!=null)?
								weapon.hitString(damageInt):
								standardHitString(Weapon.TYPE_NATURAL,Weapon.CLASS_BLUNT,damageInt,weapon.name()))+"^</FIGHT^>^?";
			final CMMsg msg=CMClass.getMsg(source,
									defender,
									weapon,
									CMMsg.MSG_OK_VISUAL,
									CMMsg.MSG_DAMAGE,
									CMMsg.MSG_OK_VISUAL,
									oldHitString);
			CMLib.color().fixSourceFightColor(msg);

			msg.setValue(damageInt);
			// why was there no okaffect here?
			if((room!=null)&&(room.okMessage(source,msg)))
			{
				if(msg.targetMinor()==CMMsg.TYP_DAMAGE)
				{
					damageInt=msg.value();
					msg.modify(msg.source(),
							   msg.target(),
							   msg.tool(),
							   msg.sourceCode(),
							   replaceDamageTag(msg.sourceMessage(),msg.value(),damageType,CMMsg.View.SOURCE),
							   msg.targetCode(),
							   replaceDamageTag(msg.targetMessage(),msg.value(),damageType,CMMsg.View.TARGET),
							   msg.othersCode(),
							   replaceDamageTag(msg.othersMessage(),msg.value(),damageType,CMMsg.View.OTHERS));
				}
				if((mayIAttack(source,attacker,defender))
				&&(CMLib.map().roomLocation(attacker)==room)
				&&(CMLib.map().roomLocation(defender)==room))
					room.send(source,msg);
			}
		}
		else
		{
			final String missString="^F^<FIGHT^>"+((weapon!=null)?
								weapon.missString():
								standardMissString(Weapon.TYPE_BASHING,Weapon.CLASS_BLUNT,weapon.name(),false))+"^</FIGHT^>^?";
			final CMMsg msg=CMClass.getMsg(source,
											defender,
											weapon,
											CMMsg.MSG_NOISYMOVEMENT,
											missString);
			CMLib.color().fixSourceFightColor(msg);
			// why was there no okaffect here?
			if(room!=null)
			if(room.okMessage(source,msg))
				room.send(source,msg);
		}
	}

	protected void processFormation(List<MOB>[] done, MOB leader, int level)
	{
		for (final List<MOB> element : done)
		{
			if((element!=null)&&(element.contains(leader)))
				return;
		}
		if(level>=done.length)
			return;
		if(done[level]==null)
			done[level]=new Vector<MOB>();
		done[level].add(leader);
		for(int f=0;f<leader.numFollowers();f++)
		{
			final MOB M=leader.fetchFollower(f);
			if(M==null)
				continue;
			int range=leader.fetchFollowerOrder(M);
			if(range<0)
				range=0;
			processFormation(done,M,level+range);
		}
	}

	@Override
	public MOB getFollowedLeader(MOB mob)
	{
		MOB leader=mob;
		if(leader.amFollowing()!=null)
			leader=leader.amUltimatelyFollowing();
		return leader;
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<MOB>[] getFormation(MOB mob)
	{
		MOB leader=mob;
		if(leader.amFollowing()!=null)
			leader=leader.amUltimatelyFollowing();
		final Vector<MOB>[] done=new Vector[20];
		processFormation(done,leader,0);
		return done;
	}

	@Override
	public List<MOB> getFormationFollowed(MOB mob)
	{
		final List<MOB>[] form=getFormation(mob);
		for(int i=1;i<form.length;i++)
		{
			if((form[i]!=null)&&(form[i].contains(mob)))
			{
				i--;
				while(i>=0)
				{
					if((form[i]!=null)&&(form[i].size()>0))
						return form[i];
					i--;
				}
			}
		}
		return null;
	}

	@Override
	public int getFormationAbsOrder(MOB mob)
	{
		final List<MOB>[] form=getFormation(mob);
		for(int i=1;i<form.length;i++)
		{
			if((form[i]!=null)&&(form[i].contains(mob)))
				return i;
		}
		return 0;
	}

	public MOB getCombatDominentKiller(MOB killer, MOB killed)
	{
		if((!killer.isMonster())&&(killer.charStats()!=null))
			return killer;
		MOB M=killer;
		Set<MOB> checked=new HashSet<MOB>();
		checked.add(M);
		while(M.isMonster()
		&&(M.amFollowing()!=null)
		&&(!checked.contains(M.amFollowing())))
		{
			M=M.amFollowing();
			checked.add(M);
		}
		if((!M.isMonster())&&(M.charStats()!=null))
			return M;
		checked.clear();
		checked=killer.getGroupMembers(checked);
		for(final Iterator<MOB> m=checked.iterator(); m.hasNext(); )
		{
			M=m.next();
			if((!M.isMonster())&&(M.charStats()!=null))
				return M;
		}
		return killer;
	}

	@Override
	public CharClass getCombatDominantClass(MOB killer, MOB killed)
	{
		CharClass C=null;

		if((killer!=null)&&(killer.charStats()!=null))
		{
			C=killer.charStats().getCurrentClass();
			MOB M=killer;
			final HashSet<MOB> checked=new HashSet<MOB>();
			checked.add(M);
			while(M.isMonster()
			&&(M.amFollowing()!=null)
			&&(!checked.contains(M.amFollowing())))
			{
				M=M.amFollowing();
				checked.add(M);
			}
			if((!M.isMonster())&&(M.charStats()!=null))
				C=M.charStats().getCurrentClass();
		}
		else
			C=CMClass.getCharClass("StdCharClass");
		return C;
	}

	protected Set<MOB> getCombatBeneficiaries(MOB killer, MOB killed, Room deathRoom, Set<MOB> beneficiaries, CharClass combatCharClass)
	{
		final Set<MOB> followers=(killer!=null)?killer.getGroupMembers(new HashSet<MOB>()):(new SHashSet<MOB>());
		if(combatCharClass==null)
			combatCharClass=CMClass.getCharClass("StdCharClass");
		if(deathRoom!=null)
		{
			for(int m=0;m<deathRoom.numInhabitants();m++)
			{
				final MOB mob=deathRoom.fetchInhabitant(m);
				if((combatCharClass.isValidClassBeneficiary(killer,killed,mob,followers))
				&&(!beneficiaries.contains(mob)))
					beneficiaries.add(mob);
			}
		}
		if((killer!=null)&&(!beneficiaries.contains(killer))&&(killer!=killed)&&(CMLib.flags().isInTheGame(killer,true)))
			beneficiaries.add(killer);
		return beneficiaries;
	}

	@Override
	public Set<MOB> getCombatBeneficiaries(MOB killer, MOB killed, CharClass combatCharClass)
	{
		if((killer==null)||(killed==null))
			return new SHashSet<MOB>();
		final SHashSet<MOB> beneficiaries=new SHashSet<MOB>();
		Room R=killer.location();
		if(R!=null)
			getCombatBeneficiaries(killer,killed,R,beneficiaries,combatCharClass);
		R=killed.location();
		if((R!=null)&&(R!=killer.location()))
			getCombatBeneficiaries(killer,killed,R,beneficiaries,combatCharClass);
		return beneficiaries;
	}

	protected Set<MOB> getCombatDividers(MOB killer, MOB killed, Room deathRoom, Set<MOB> dividers, CharClass combatCharClass)
	{
		final Set<MOB> followers=(killer!=null)?killer.getGroupMembers(new HashSet<MOB>()):(new HashSet<MOB>());
		if(combatCharClass==null)
			combatCharClass=CMClass.getCharClass("StdCharClass");
		if(deathRoom!=null)
		{
			for(int m=0;m<deathRoom.numInhabitants();m++)
			{
				final MOB mob=deathRoom.fetchInhabitant(m);
				if((combatCharClass.isValidClassDivider(killer,killed,mob,followers))
				&&(!dividers.contains(mob)))
					dividers.add(mob);
			}
		}
		if((killer!=null)&&(!dividers.contains(killer))&&(killer!=killed)&&(CMLib.flags().isInTheGame(killer,true)))
			dividers.add(killer);
		return dividers;
	}

	@Override
	public Set<MOB> getCombatDividers(MOB killer, MOB killed, CharClass combatCharClass)
	{
		if((killer==null)||(killed==null))
			return new SHashSet<MOB>();
		final Set<MOB> dividers=new SHashSet<MOB>();
		Room R=killer.location();
		if(R!=null)
			getCombatDividers(killer,killed,R,dividers,combatCharClass);
		R=killed.location();
		if((R!=null)&&(R!=killer.location()))
			getCombatDividers(killer,killed,R,dividers,combatCharClass);
		return dividers;
	}

	protected DeadBody justDie(final MOB source, final MOB target)
	{
		if(target==null)
			return null;
		final Room deathRoom=target.location();
		if(deathRoom == null)
			return null;

		//TODO: this creates too many loops.  The right thing is to loop once
		// and call  a boolean function to populate all these lists.
		final CharClass combatCharClass=getCombatDominantClass(source,target);
		final Set<MOB> beneficiaries=getCombatBeneficiaries(source,target,combatCharClass);
		final Set<MOB> hisGroupH=target.getGroupMembers(new HashSet<MOB>());
		for(final Enumeration<MOB> m=deathRoom.inhabitants();m.hasMoreElements();)
		{
			final MOB M=m.nextElement();
			if((M!=null)&&(M.getVictim()==target))
				pickNextVictim(M, target, hisGroupH);
		}
		final Set<MOB> dividers=getCombatDividers(source,target,combatCharClass);

		dispenseExperience(beneficiaries,dividers,target);

		final String currency=CMLib.beanCounter().getCurrency(target);
		final double deadMoney=CMLib.beanCounter().getTotalAbsoluteValue(target,currency);
		double myAmountOfDeadMoney=0.0;
		final Vector<MOB> goldLooters=new Vector<MOB>();
		for (final MOB M : beneficiaries)
		{
			if(((M.isAttributeSet(MOB.Attrib.AUTOGOLD))
			&&(!goldLooters.contains(M)))
			&&(M!=target)
			&&(M.location()==deathRoom)
			&&(deathRoom.isInhabitant(M)))
				goldLooters.addElement(M);
		}
		if((goldLooters.size()>0)&&(deadMoney>0))
		{
			myAmountOfDeadMoney=CMath.div(deadMoney,goldLooters.size());
			CMLib.beanCounter().subtractMoney(target,deadMoney);
		}

		final int[] expLost={100*target.phyStats().level()};
		if(expLost[0]<100)
			expLost[0]=100;
		String[] cmds=null;
		if((target.isMonster())||(target.soulMate()!=null))
			cmds=CMParms.toStringArray(CMParms.parseCommas(CMProps.getVar(CMProps.Str.MOBDEATH),true));
		else
			cmds=CMParms.toStringArray(CMParms.parseCommas(CMProps.getVar(CMProps.Str.PLAYERDEATH),true));

		DeadBody body=null; //must be done before consequences because consequences could be purging
		if((!CMParms.containsIgnoreCase(cmds,"RECALL"))
		&&(!isKnockedOutUponDeath(target,source)))
			body=target.killMeDead(true);

		handleCombatLossConsequences(target,source,cmds,expLost,L("^*You lose @x1 experience points.^?^.",""+expLost[0]));

		if(!isKnockedOutUponDeath(target,source))
		{
			Room bodyRoom=deathRoom;
			if((body!=null)&&(body.owner() instanceof Room)&&(((Room)body.owner()).isContent(body)))
				bodyRoom=(Room)body.owner();
			if((source!=null)&&(body!=null))
			{
				body.setKillerName(source.Name());
				body.setIsKillerPlayer(!source.isMonster());
				body.setKillerTool(source.fetchWieldedItem());
				if(body.getKillerTool()==null)
					body.setKillerTool(source.getNaturalWeapon());
			}

			if((!target.isMonster())&&(CMLib.dice().rollPercentage()==1)&&(!CMSecurity.isDisabled(CMSecurity.DisFlag.AUTODISEASE)))
			{
				final Ability A=CMClass.getAbility("Disease_Amnesia");
				if((A!=null)&&(target.fetchEffect(A.ID())==null))
					A.invoke(target,target,true,0);
			}

			if(target.soulMate()!=null)
			{
				final Session s=target.session();
				s.setMob(target.soulMate());
				target.soulMate().setSession(s);
				target.setSession(null);
				target.soulMate().tell(L("^HYour spirit has returned to your body...\n\r\n\r^N"));
				CMLib.commands().postLook(target.soulMate(),true);
				target.setSoulMate(null);
			}

			if((source!=null)
			&&(bodyRoom!=null)
			&&(body!=null)
			&&(source.location()==bodyRoom)
			&&(bodyRoom.isInhabitant(source))
			&&(source.isAttributeSet(MOB.Attrib.AUTOLOOT)))
			{
				if((source.riding()!=null)&&(source.riding() instanceof MOB))
					source.tell(L("You'll need to dismount to loot the body."));
				else
				if((source.riding()!=null)&&(source.riding() instanceof MOB))
					source.tell(L("You'll need to disembark to loot the body."));
				else
				for(int i=bodyRoom.numItems()-1;i>=0;i--)
				{
					final Item item=bodyRoom.getItem(i);
					if((item!=null)
					&&(item.container()==body)
					&&(CMLib.flags().canBeSeenBy(body,source))
					&&((!body.isDestroyedAfterLooting())||(!(item instanceof RawMaterial)))
					&&(CMLib.flags().canBeSeenBy(item,source)))
						CMLib.commands().postGet(source,body,item,false);
				}
				if(body.isDestroyedAfterLooting())
					bodyRoom.recoverRoomStats();
			}

			Coins C=null;
			if((deadMoney>0)&&(myAmountOfDeadMoney>0)&&(body!=null)&&(bodyRoom!=null))
			for(int g=0;g<goldLooters.size();g++)
			{
				C=CMLib.beanCounter().makeBestCurrency(currency,myAmountOfDeadMoney,null,body);
				if(C!=null)
				{
					C.recoverPhyStats();
					bodyRoom.addItem(C,ItemPossessor.Expire.Monster_EQ);
					bodyRoom.recoverRoomStats();
					final MOB mob=goldLooters.elementAt(g);
					if(mob.location()==bodyRoom)
					{
						if((mob.riding()!=null)&&(mob.riding() instanceof MOB))
							mob.tell(L("You'll need to dismount to get @x1 off the body.",C.name()));
						else
						if((mob.riding()!=null)&&(mob.riding() instanceof Item))
							mob.tell(L("You'll need to disembark to get @x1 off the body.",C.name()));
						else
						if(CMLib.flags().canBeSeenBy(body,mob))
							CMLib.commands().postGet(mob,body,C,false);
					}
				}
			}

			if((source != null)&&(source.getVictim()==target))
				source.setVictim(null);
			target.setVictim(null);
			if((body!=null)&&(bodyRoom!=null)&&(body.isDestroyedAfterLooting()))
			{
				for(int i=bodyRoom.numItems()-1;i>=0;i--)
				{
					final Item item=bodyRoom.getItem(i);
					if((item!=null)&&(item.container()==body))
						item.setContainer(null);
				}
				body.destroy();
				bodyRoom.recoverPhyStats();
				return null;
			}
			return body;
		}
		return null;
	}

	@Override
	public String standardHitWord(int type, double pct)
	{
		if((type<0)||(type>=Weapon.TYPE_DESCS.length))
			type=Weapon.TYPE_BURSTING;
		final int[] thresholds=CMProps.getListFileIntList(CMProps.ListFile.DAMAGE_WORDS_THRESHOLDS);
		int damnCode=(int)Math.round(pct * (thresholds.length-1));
		if(damnCode > thresholds.length-1)
			damnCode = thresholds.length-1;
		return getStandardHitWordInternal(type, damnCode);
	}
	
	protected String getStandardHitWordInternal(int type, int damnCode)
	{
		damnCode++; // always add 1 because index into hitwords is type=0, annoy=1;
		final Object[][][] hitWords = CMProps.getListFileGrid(CMProps.ListFile.DAMAGE_WORDS);
		if(hitWords != hitWordsChanged)
		{
			hitWordsChanged=hitWords;
			hitWordIndex=null;
		}
		if(hitWordIndex==null)
		{
			final Object[][][] newWordIndex=new Object[Weapon.TYPE_DESCS.length][][];
			for(int w=0;w<Weapon.TYPE_DESCS.length;w++)
			{
				Object[][] ALL=null;
				Object[][] MINE=null;
				for (final Object[][] hitWord : hitWords)
				{
					if(((String)hitWord[0][0]).equalsIgnoreCase("ALL"))
						ALL=hitWord;
					else
					if(((String)hitWord[0][0]).equalsIgnoreCase(Weapon.TYPE_DESCS[w]))
					{
						MINE = hitWord;
						break;
					}
				}
				if(MINE!=null)
					newWordIndex[w]=MINE;
				else
					newWordIndex[w]=ALL;
			}
			hitWordIndex=newWordIndex;
		}
		final Object[][] HIT_WORDS=hitWordIndex[type];
		if(damnCode<1)
			damnCode=1;
		if(damnCode>=HIT_WORDS.length)
			damnCode=HIT_WORDS.length-1;
		return (String)CMLib.dice().pick(HIT_WORDS[damnCode]);
	}
	
	@Override
	public String standardHitWord(int type, int damage)
	{
		if((type<0)||(type>=Weapon.TYPE_DESCS.length))
			type=Weapon.TYPE_BURSTING;
		final int[] thresholds=CMProps.getListFileIntList(CMProps.ListFile.DAMAGE_WORDS_THRESHOLDS);
		int damnCode=thresholds.length-2;
		for(int i=0;i<thresholds.length;i++)
		{
			if (damage <= thresholds[i])
			{
				damnCode = i;
				break;
			}
		}
		return getStandardHitWordInternal(type, damnCode);
	}

	@Override
	public String armorStr(MOB mob)
	{
		final int armor = -adjustedArmor(mob);
		final int ARMOR_CEILING=CMProps.getListFileFirstInt(CMProps.ListFile.ARMOR_DESCS_CEILING);
		final int numArmorDescs = CMProps.getListFileIndexedListSize(CMProps.ListFile.ARMOR_DESCS);
		return (armor<0)?CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.ARMOR_DESCS,0):(
			   (armor>=ARMOR_CEILING)?
					   CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.ARMOR_DESCS,numArmorDescs-1)
					   +(CMStrings.repeatWithLimit('!',((armor-ARMOR_CEILING)/100),10))
					   +"^. ("+armor+")"
					:
					   (CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.ARMOR_DESCS,(int)Math.round(Math.floor(CMath.mul(CMath.div(armor,ARMOR_CEILING),numArmorDescs))))
					   +"^. ("+armor+")"));
	}

	@Override
	public String fightingProwessStr(MOB mob)
	{
		final int prowess = adjustedAttackBonus(mob,null) - ATTACK_ADJUSTMENT;
		final int PROWESS_CEILING=CMProps.getListFileFirstInt(CMProps.ListFile.PROWESS_DESCS_CEILING);
		final int numProwessDescs = CMProps.getListFileIndexedListSize(CMProps.ListFile.PROWESS_DESCS);
		return (prowess<0)?CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.PROWESS_DESCS,0):(
			   (prowess>=PROWESS_CEILING)
										 ?
								 CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.PROWESS_DESCS,numProwessDescs-1)
								 +(CMStrings.repeatWithLimit('!',((prowess-PROWESS_CEILING)/100),10)+"^. ("+prowess+")")
										 :
								 (CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.PROWESS_DESCS,(int)Math.round(Math.floor(CMath.mul(CMath.div(prowess,PROWESS_CEILING),numProwessDescs))))
								 +"^. ("+prowess+")"));
	}

	protected int getWeaponAttackIndex(final int weaponDamageType, final int weaponClassification)
	{
		switch(weaponClassification)
		{
		case Weapon.CLASS_RANGED: return (weaponDamageType==Weapon.TYPE_LASERING) ? 5 : 0;
		case Weapon.CLASS_THROWN: return (weaponDamageType==Weapon.TYPE_LASERING) ? 5 : 1;
		default:
			switch(weaponDamageType)
			{
			case Weapon.TYPE_SLASHING:
			case Weapon.TYPE_BASHING:
				return 2;
			case Weapon.TYPE_PIERCING:
				return 4;
			case Weapon.TYPE_SHOOT:
				return 0;
			case Weapon.TYPE_LASERING:
				return 5;
			default:
				return 3;
			}
		}
	}

	@Override
	public String standardMissString(final int weaponDamageType, final int weaponClassification, final String weaponName, final boolean useExtendedMissString)
	{
		final int listIndex = getWeaponAttackIndex(weaponDamageType, weaponClassification);
		if(!useExtendedMissString)
			return CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.MISS_DESCS,listIndex);
		return CMStrings.replaceAll(CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.WEAPON_MISS_DESCS,listIndex),"<TOOLNAME>",weaponName)+CMLib.protocol().msp("missed.wav",20);
	}


	@Override
	public String standardHitString(final int weaponDamageType, final int weaponClass, final int damageAmount, final String weaponName)
	{
		final int listIndex;
		if((weaponName==null)||(weaponName.length()==0))
			listIndex = getWeaponAttackIndex(weaponDamageType, Weapon.CLASS_NATURAL);
		else
			listIndex = getWeaponAttackIndex(weaponDamageType, weaponClass);
		final StringBuilder str=new StringBuilder(CMStrings.replaceAll(CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.WEAPON_HIT_DESCS,listIndex),"<TOOLNAME>",weaponName));
		switch(weaponClass)
		{
		case Weapon.CLASS_RANGED:
			return str.append(CMLib.protocol().msp("arrow.wav",20)).toString();
		case Weapon.CLASS_THROWN:
			return str.append(CMLib.protocol().msp("arrow.wav",20)).toString();
		default:
			return str.append(CMLib.protocol().msp("punch"+CMLib.dice().roll(1,7,0)+".wav",20)).toString();
		}
	}

	@Override
	public String standardMobCondition(MOB viewer,MOB mob)
	{
		int pct=(int)Math.round(Math.floor((CMath.div(mob.curState().getHitPoints(),mob.maxState().getHitPoints()))*10));
		if(pct<0)
			pct=0;
		final int numHealthDescs=CMProps.getListFileIndexedListSize(CMProps.ListFile.HEALTH_CHART);
		if(pct>=numHealthDescs)
			pct=numHealthDescs-1;
		return CMStrings.replaceAll(CMProps.getListFileChoiceFromIndexedList(CMProps.ListFile.HEALTH_CHART,pct),"<MOB>",mob.name(viewer));
	}

	@Override
	public void resistanceMsgs(MOB source, MOB target, CMMsg msg)
	{
		if(msg.value()>0)
			return;

		if(target.amDead())
			return;

		String tool=null;
		String endPart=" from <T-NAME>.";
		if(source==target)
		{
			source=null;
			endPart=".";
		}
		if(msg.tool() instanceof Trap)
			endPart=".";
		else
		if(msg.tool() instanceof Ability)
			tool=((Ability)msg.tool()).name();
		

		String tackOn=null;
		switch(msg.targetMinor())
		{
		case CMMsg.TYP_MIND:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the mental attack" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_GAS:
			if(tool == null)
				tackOn = L("<S-NAME> resist(s) the noxious fumes" + endPart);
			else
				tackOn = L("<S-NAME> resist(s) the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_COLD:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the cold blast" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_ELECTRIC:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the electrical attack" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_FIRE:
			if(tool == null)
				tackOn = L("<S-NAME> resist(s) the blast of heat" + endPart);
			else
				tackOn = L("<S-NAME> resist(s) the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_WATER:
			if(tool == null)
				tackOn = L("<S-NAME> dodge(s) the wet blast" + endPart);
			else
				tackOn = L("<S-NAME> dodge(s) the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_UNDEAD:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the evil attack" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_POISON:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the poison" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_DISEASE:
			if(tool == null)
				tackOn = L("<S-NAME> resist(s) the disease.");
			else
				tackOn = L("<S-NAME> resist(s) the @x1.",tool);
			break;
		case CMMsg.TYP_JUSTICE:
			break;
		case CMMsg.TYP_CAST_SPELL:
			if(tool == null)
				tackOn = L("<S-NAME> resist(s) the magical attack" + endPart);
			else
				tackOn = L("<S-NAME> resist(s) the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_PARALYZE:
			if(tool == null)
				tackOn = L("<S-NAME> resist(s) the paralysis" + endPart);
			else
				tackOn = L("<S-NAME> resist(s) the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_SONIC:
			if(tool == null)
				tackOn = L("<S-NAME> shake(s) off the sonic" + endPart);
			else
				tackOn = L("<S-NAME> shake(s) off the @x1" + endPart,tool);
			break;
		case CMMsg.TYP_LASER:
			if(tool == null)
				tackOn = L("<S-NAME> dodge(s) the laser" + endPart);
			else
				tackOn = L("<S-NAME> dodge(s) the @x1" + endPart,tool);
			break;
		}
		if(tackOn!=null)
		{
			final String newStr=target+"/"+source+"/"+tool;
			if(!newStr.equals(lastStr)||((System.currentTimeMillis()-lastRes)>250))
				msg.addTrailerMsg(CMClass.getMsg(target,source,CMMsg.MSG_OK_ACTION,tackOn));
			lastStr=newStr;
			lastRes=System.currentTimeMillis();
		}
		msg.setValue(msg.value()+1);
	}

	@Override
	public void handleBeingHealed(CMMsg msg)
	{
		if(!(msg.target() instanceof MOB))
			return;
		final MOB target=(MOB)msg.target();
		final int amt=msg.value();
		if(amt>0)
			target.curState().adjHitPoints(amt,target.maxState());
	}

	protected boolean bleedableWeapon(Environmental E)
	{
		if(E==null)
			return false;
		if(E instanceof Weapon)
		{
			return true;
		}
		else
		if(E instanceof Ability)
		{
			final int code=((Ability)E).classificationCode()&Ability.ALL_ACODES;
			switch(code)
			{
			case Ability.ACODE_DISEASE:
			case Ability.ACODE_POISON:
				return false;
			}
			return true;
		}
		else
			return true;
	}

	@Override
	public void handleBeingDamaged(CMMsg msg)
	{
		if(!(msg.target() instanceof MOB))
			return;
		final MOB attacker=msg.source();
		final MOB target=(MOB)msg.target();
		final int dmg=msg.value();
		if(Log.combatChannelOn())
		{
			final Item DI=target.fetchWieldedItem();
			final Item KI=attacker.fetchWieldedItem();
			final String tool=(msg.tool()==null)?"null":msg.tool().name();
			final String type=(msg.sourceMinor()==CMMsg.NO_EFFECT)?"??":CMMsg.TYPE_DESCS[msg.sourceMinor()];
			Log.combatOut("DAMG",attacker.Name()+":"+attacker.phyStats().getCombatStats()+":"+attacker.curState().getCombatStats()+":"+((KI==null)?"null":KI.name())+":"+target.Name()+":"+target.phyStats().getCombatStats()+":"+target.curState().getCombatStats()+":"+((DI==null)?"null":DI.name())+":"+tool+":"+type+":"+dmg);
		}
		synchronized(("DMG"+target.Name().toUpperCase()).intern())
		{
			if((dmg>0)&&(target.curState().getHitPoints()>0))
			{
				if((!target.curState().adjHitPoints(-dmg,target.maxState()))
				&&(target.curState().getHitPoints()<1)
				&&(target.location()!=null))
					postDeath(attacker,target,msg);
				else
				{
					if((Math.round(CMath.div(dmg,target.maxState().getHitPoints())*100.0)>=CMProps.getIntVar(CMProps.Int.INJBLEEDPCTHP))
					&&bleedableWeapon(msg.tool()))
					{
						final Ability A2=CMClass.getAbility("Bleeding");
						if(A2!=null)
							A2.invoke((target),(target),true,0);
					}
					if((target.curState().getHitPoints()<target.getWimpHitPoint())
					&&(target.getWimpHitPoint()>0)
					&&(target.isInCombat()))
						postPanic(target,msg);
					else
					if((CMProps.getIntVar(CMProps.Int.INJPCTHP)>=(int)Math.round(CMath.div(target.curState().getHitPoints(),target.maxState().getHitPoints())*100.0))
					&&(!CMLib.flags().isGolem(target))
					&&(target.fetchEffect("Injury")==null))
					{
						Ability A=CMClass.getAbility("Injury");
						if(A!=null)
						{
							A.startTickDown(target,target,Ability.TICKS_ALMOST_FOREVER);
							A=target.fetchEffect(A.ID());
							if(A!=null)
								A.okMessage(target,msg);
						}
					}
				}
			}
		}
	}

	@Override
	public void handleDeath(CMMsg msg)
	{
		final MOB deadmob=msg.source();
		if(!deadmob.amDead())
		{
			if((!deadmob.isMonster())&&(deadmob.soulMate()==null))
			{
				CMLib.coffeeTables().bump(deadmob,CoffeeTableRow.STAT_DEATHS);
				final PlayerStats playerStats=deadmob.playerStats();
				if(playerStats!=null)
					playerStats.setHygiene(0);
				final List<String> channels=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.DETAILEDDEATHS);
				final List<String> channels2=CMLib.channels().getFlaggedChannelNames(ChannelsLibrary.ChannelFlag.DEATHS);
				if(!CMLib.flags().isCloaked(deadmob))
				for(int i=0;i<channels.size();i++)
				{
					if((msg.tool() instanceof MOB))
						CMLib.commands().postChannel(channels.get(i),deadmob.clans(),L("@x1 was just killed in @x2 by @x3.",deadmob.Name(),CMLib.map().getExtendedRoomID(deadmob.location()),msg.tool().Name()),true);
					else
						CMLib.commands().postChannel(channels.get(i),deadmob.clans(),L("@x1 has just died at @x2",deadmob.Name(),CMLib.map().getExtendedRoomID(deadmob.location())),true);
				}
				if(!CMLib.flags().isCloaked(deadmob))
				{
					for(int i=0;i<channels2.size();i++)
						if((msg.tool() instanceof MOB))
							CMLib.commands().postChannel(channels2.get(i),deadmob.clans(),L("@x1 was just killed.",deadmob.Name()),true);
				}
			}
			if(msg.tool() instanceof MOB)
			{
				final MOB killer=(MOB)msg.tool();
				doDeathPostProcessing(msg);
				justDie(killer,deadmob);
			}
			else
				justDie(null,deadmob);
			deadmob.tell(deadmob,msg.target(),msg.tool(),msg.sourceMessage());
			if(deadmob.riding()!=null)
				deadmob.riding().delRider(deadmob);
			if(CMLib.flags().isCataloged(deadmob))
				CMLib.catalog().bumpDeathPickup(deadmob);
		}
	}

	@Override
	public void doDeathPostProcessing(CMMsg msg)
	{
		final MOB deadmob=msg.source();
		if(msg.tool() instanceof MOB)
		{
			final MOB killer=(MOB)msg.tool();
			if(Log.killsChannelOn())
			{
				final Item KI=killer.fetchWieldedItem();
				final Item DI=deadmob.fetchWieldedItem();
				final String room=CMLib.map().getExtendedRoomID((killer.location()!=null)?killer.location():deadmob.location());
				Log.killsOut("KILL",room+":"+killer.Name()+":"+killer.phyStats().getCombatStats()+":"+killer.curState().getCombatStats()+":"+((KI==null)?"null":KI.name())+":"+deadmob.Name()+":"+deadmob.phyStats().getCombatStats()+":"+deadmob.curState().getCombatStats()+":"+((DI==null)?"null":DI.name()));
			}
			if(Log.combatChannelOn())
			{
				final Item DI=deadmob.fetchWieldedItem();
				final Item KI=killer.fetchWieldedItem();
				Log.combatOut("KILL",killer.Name()+":"+killer.phyStats().getCombatStats()+":"+killer.curState().getCombatStats()+":"+((KI==null)?"null":KI.name())+":"+deadmob.Name()+":"+deadmob.phyStats().getCombatStats()+":"+deadmob.curState().getCombatStats()+":"+((DI==null)?"null":DI.name()));
			}
			CMLib.achievements().possiblyBumpAchievement(killer, AchievementLibrary.Event.KILLS, 1, deadmob);
			if((deadmob!=null)&&(killer!=null)
			&&(deadmob.soulMate()==null)
			&&(killer!=deadmob)&&(!killer.isMonster()))
			{
				if(!deadmob.isMonster())
				{
					CMLib.coffeeTables().bump(deadmob,CoffeeTableRow.STAT_PKDEATHS);
					if(killer.playerStats()!=null)
						CMLib.players().bumpPrideStat(killer,PrideStat.PVPKILLS, 1);
				}
				if((killer.session()!=null)
				&&((deadmob.session()==null)||(!deadmob.session().getAddress().equalsIgnoreCase(killer.session().getAddress()))))
				{
					final List<Pair<Clan,Integer>> list = CMLib.clans().findRivalrousClans(killer, deadmob);
					for(final Pair<Clan,Integer> c : list)
						c.first.recordClanKill(killer,deadmob);
				}
			}
		}
	}

	protected void pickNextVictim(MOB observer, MOB deadmob, Set<MOB> deadGroupH)
	{
		final Room R=observer.location();
		if(R!=null)
		{
			MOB newTargetM=null;
			final Set<MOB> myGroupH=observer.getGroupMembers(new HashSet<MOB>());
			for(int r=0;r<R.numInhabitants();r++)
			{
				final MOB M=R.fetchInhabitant(r);
				if((M!=observer)
				&&(M!=deadmob)
				&&(M!=null)
				&&(M.getVictim()==observer)
				&&(!M.amDead())
				&&(CMLib.flags().isInTheGame(M,true))
				&&(observer.mayIFight(M)))
				{
					newTargetM=M;
					break;
				}
			}
			if(newTargetM==null)
			for(int r=0;r<R.numInhabitants();r++)
			{
				final MOB M=R.fetchInhabitant(r);
				if(M==null)
					continue;
				final MOB vic=M.getVictim();
				if((M!=observer)
				&&(M!=deadmob)
				&&(deadGroupH.contains(M)
					||((vic!=null)&&(myGroupH.contains(vic))))
				&&(!M.amDead())
				&&(CMLib.flags().isInTheGame(M,true))
				&&(observer.mayIFight(M)))
				{
					newTargetM=M;
					break;
				}
			}
			observer.setVictim(null);
			if(newTargetM!=null)
				observer.setVictim(newTargetM);
		}
	}

	@Override
	public void handleObserveDeath(MOB observer, MOB fighting, CMMsg msg)
	{
		// no longer does a damn thing
	}

	@Override
	public void handleBeingAssaulted(CMMsg msg)
	{
		if(!(msg.target() instanceof MOB))
			return;
		final MOB attacker=msg.source();
		final MOB target=(MOB)msg.target();

		if((!target.isInCombat())
		&&(target.location()!=null)
		&&(target.location().isInhabitant(attacker))
		&&((!CMath.bset(msg.sourceMajor(),CMMsg.MASK_ALWAYS))
			||(!(msg.tool() instanceof DiseaseAffect))))
		{
			establishRange(target,attacker,msg.tool());
			target.setVictim(attacker);
		}
		if(target.isInCombat())
		{
			if(attacker.session()!=null)
			{
				if(!target.isMonster())
					attacker.session().setLastPKFight();
				else
					attacker.session().setLastNPCFight();
			}
			if(msg.targetMinor()==CMMsg.TYP_WEAPONATTACK)
			{
				Item weapon=attacker.getNaturalWeapon();
				if((msg.tool() instanceof Item))
					weapon=(Item)msg.tool();
				if(weapon!=null)
				{
					final boolean isHit=rollToHit(attacker,target);
					postWeaponAttackResult(attacker,target,weapon,isHit);
					if(isHit)
						msg.setValue(1);
				}
				if((target.soulMate()==null)
				&&(target.playerStats()!=null)
				&&(target.location()!=null)
				&&(!CMSecurity.isDisabled(CMSecurity.DisFlag.HYGIENE)))
					target.playerStats().adjHygiene(PlayerStats.HYGIENE_FIGHTDIRTY);

				if((attacker.isMonster())&&(!attacker.isInCombat()))
					attacker.setVictim(target);
			}
			else
			if(msg.tool() instanceof Item)
				postWeaponAttackResult(attacker,target,(Item)msg.tool(),true);
		}
		if(CMLib.flags().isSitting(target)||CMLib.flags().isSleeping(target))
			CMLib.commands().postStand(target,true);
	}

	@Override
	public void makeFollowersFight(MOB observerM, MOB defenderM, MOB attackerM)
	{
		if((attackerM==null)||(defenderM==null)||observerM==null)
			return;
		if(attackerM==defenderM)
			return;
		if((defenderM==observerM)||(attackerM==observerM))
			return;
		if((defenderM.location()!=observerM.location())||(defenderM.location()!=attackerM.location()))
			return;
		if((observerM.isAttributeSet(MOB.Attrib.AUTOASSIST)))
			return;
		if(observerM.isInCombat())
			return;
		final MOB observerFollows=observerM.amFollowing();
		final MOB targetFollows=defenderM.amFollowing();
		final MOB sourceFollows=attackerM.amFollowing();

		if((observerFollows==defenderM)
		||(targetFollows==observerM)
		||((targetFollows!=null)&&(targetFollows==observerFollows)))
		{
			observerM.setVictim(attackerM);
			establishRange(observerM,attackerM,observerM.fetchWieldedItem());
		}
		else
		if((observerFollows==attackerM)
		||(sourceFollows==observerM)
		||((sourceFollows!=null)&&(sourceFollows==observerFollows)))
		{
			observerM.setVictim(defenderM);
			establishRange(observerM,defenderM,observerM.fetchWieldedItem());
		}
	}

	public List<MOB> getAllInProximity(MOB to, int distance)
	{
		final Room R=to.location();
		final Vector<MOB> V=new Vector<MOB>();
		V.addElement(to);
		if(R==null)
			return V;
		final Vector<MOB> everyV=new Vector<MOB>();
		for(int i=0;i<R.numInhabitants();i++)
			everyV.addElement(R.fetchInhabitant(i));
		if(!everyV.contains(to))
			everyV.addElement(to);
		final int[][] map=new int[everyV.size()][everyV.size()];
		for(int x=0;x<map.length;x++)
			for(int y=0;y<map.length;y++)
				map[x][y]=-1;

		return V;
	}

	private int maxRangeWith(final MOB mob, final Environmental tool)
	{
		int max = 0;
		if (tool != null)
			max = tool.maxRange();
		if (mob.maxRange() < max)
			max = mob.maxRange();
		return max;
	}

	private int minRangeWith(final MOB mob, final Environmental tool)
	{
		if (tool != null)
			return tool.minRange();
		return mob.minRange();
	}
	
	@Override
	public void establishRange(MOB source, MOB target, Environmental tool)
	{
		// establish and enforce range
		if((source.rangeToTarget()<0))
		{
			if(source.riding()!=null)
			{
				if((target==source.riding())||(source.riding().amRiding(target)))
					source.setRangeToTarget(0);
				else
				if((source.riding() instanceof MOB)
				   &&(((MOB)source.riding()).isInCombat())
				   &&(((MOB)source.riding()).getVictim()==target)
				   &&(((MOB)source.riding()).rangeToTarget()>=0)
				   &&(((MOB)source.riding()).rangeToTarget()<source.rangeToTarget()))
				{
					source.setRangeToTarget(((MOB)source.riding()).rangeToTarget());
					source.recoverPhyStats();
					return;
				}
				else
				for(int r=0;r<source.riding().numRiders();r++)
				{
					final Rider rider=source.riding().fetchRider(r);
					if(!(rider instanceof MOB))
						continue;
					final MOB otherMOB=(MOB)rider;
					if((otherMOB!=source)
					   &&(otherMOB.isInCombat())
					   &&(otherMOB.getVictim()==target)
					   &&(otherMOB.rangeToTarget()>=0)
					   &&(otherMOB.rangeToTarget()<source.rangeToTarget()))
					{
						source.setRangeToTarget(otherMOB.rangeToTarget());
						source.recoverPhyStats();
						return;
					}
				}
			}

			final MOB follow=source.amFollowing();
			if((target.getVictim()==source)&&(target.rangeToTarget()>=0))
				source.setRangeToTarget(target.rangeToTarget());
			else
			if((follow!=null)&&(follow.location()==source.location()))
			{
				int newRange=follow.fetchFollowerOrder(source);
				if(newRange<0)
				{
					if(follow.rangeToTarget()>=0)
					{
						newRange=follow.rangeToTarget();
						if(newRange<maxRangeWith(source,tool))
							newRange=maxRangeWith(source,tool);
					}
					else
						newRange=maxRangeWith(source,tool);
				}
				else
				{
					if(follow.rangeToTarget()>=0)
						newRange=newRange+follow.rangeToTarget();
				}
				if((source.location()!=null)&&(source.location().maxRange()<newRange))
					newRange=source.location().maxRange();
				source.setRangeToTarget(newRange);
			}
			else
				source.setRangeToTarget(maxRangeWith(source,tool));
			source.recoverPhyStats();
		}
	}

	protected void subtickAttack(MOB fighter, Item weapon, int folrange)
	{
		if((weapon!=null)&&(weapon.amWearingAt(Wearable.IN_INVENTORY)))
			weapon=fighter.fetchWieldedItem();
		if((!fighter.isAttributeSet(MOB.Attrib.AUTOMELEE)))
			postAttack(fighter,fighter.getVictim(),weapon);
		else
		{
			boolean inminrange=(fighter.rangeToTarget()>=minRangeWith(fighter, weapon));
			boolean inmaxrange=(fighter.rangeToTarget()<=maxRangeWith(fighter, weapon));
			if((folrange>=0)&&(fighter.rangeToTarget()>=0)&&(folrange!=fighter.rangeToTarget()))
			{
				if(fighter.rangeToTarget()<folrange)
					inminrange=false;
				else
				if(fighter.rangeToTarget()>folrange)
				{
					// these settings are ONLY to ensure that neither of the
					// next two conditions evaluate to true.
					inminrange=true;
					inmaxrange=false;
					// we advance
					final CMMsg msg=CMClass.getMsg(fighter,fighter.getVictim(),CMMsg.MSG_ADVANCE,L("<S-NAME> advances(s) at <T-NAMESELF>."));
					if(fighter.location().okMessage(fighter,msg))
					{
						fighter.location().send(fighter,msg);
						fighter.setRangeToTarget(fighter.rangeToTarget()-1);
						if((fighter.getVictim()!=null)&&(fighter.getVictim().getVictim()==fighter))
						{
							fighter.getVictim().setRangeToTarget(fighter.rangeToTarget());
							fighter.getVictim().recoverPhyStats();
						}
					}
				}
			}

			if((!inminrange)&&(fighter.curState().getMovement()>=25))
			{
				final CMMsg msg=CMClass.getMsg(fighter,fighter.getVictim(),CMMsg.MSG_RETREAT,L("<S-NAME> retreat(s) before <T-NAME>."));
				if(fighter.location().okMessage(fighter,msg))
					fighter.location().send(fighter,msg);
			}
			else
			if(inminrange&&inmaxrange&&((weapon!=null)||(fighter.rangeToTarget()==0)))
				postAttack(fighter,fighter.getVictim(),weapon);
		}
	}

	protected void subtickBeforeAttack(final MOB fighter, final CombatSystem combatSystem)
	{
		// combat que system eats up standard commands
		// before using any attacks
		while(((combatSystem==CombatLibrary.CombatSystem.QUEUE)||(combatSystem==CombatLibrary.CombatSystem.TURNBASED))
		&&(!fighter.amDead())
		&&(fighter.dequeCommand()))
			{}
	}
	protected void subtickAfterAttack(MOB fighter)
	{
		// this code is for auto-retargeting of players
		// is mostly not handled by combatabilities in a smarter way
		final MOB target=fighter.getVictim();
		if((target!=null)
		&&(fighter.isMonster())
		&&(target.isMonster())
		&&(CMLib.dice().rollPercentage()==1)
		&&((fighter.amFollowing()==null)||(fighter.amFollowing().isMonster()))
		&&(!target.amDead())
		&&(fighter.location()!=null))
		{
			MOB M=null;
			final Room R=fighter.location();
			MOB nextVictimM=null;
			for(int m=0;m<R.numInhabitants();m++)
			{
				M=R.fetchInhabitant(m);
				if((M!=null)
				&&(!M.isMonster())
				&&(M.getVictim()==fighter)
				&&((nextVictimM==null)||(M.rangeToTarget()<nextVictimM.rangeToTarget())))
					nextVictimM=M;
			}
			if((nextVictimM!=null)&&(nextVictimM.isInCombat()))
				fighter.setVictim(nextVictimM);
		}
	}

	@Override
	public void dispenseExperience(Set<MOB> killers, Set<MOB> dividers, MOB killed)
	{
		int totalLevels=0;
		int expAmount=100;
		int expAddition=25;

		for (final MOB mob : dividers)
		{
			totalLevels += (mob.phyStats().level()*mob.phyStats().level());
			expAmount += expAddition;
			expAddition -= expAddition/4;
		}
		for (final MOB mob : killers)
		{
			int myAmount=(int)Math.round(CMath.mul(expAmount,CMath.div(mob.phyStats().level()*mob.phyStats().level(),totalLevels)));
			if(myAmount>100)
				myAmount=100;
			CMLib.leveler().postExperience(mob,killed,"",myAmount,false);
		}
	}

	@Override
	public void tickCombat(MOB fighter)
	{
		Item weapon=fighter.fetchWieldedItem();

		if((fighter.isAttributeSet(MOB.Attrib.AUTODRAW))&&(weapon==null))
		{
			CMLib.commands().postDraw(fighter,false,true);
			weapon=fighter.fetchWieldedItem();
		}

		final CombatSystem combatSystem=CombatSystem.values()[CMProps.getIntVar(CMProps.Int.COMBATSYSTEM) % CombatSystem.values().length];

		subtickBeforeAttack(fighter, combatSystem);

		final int folrange=(fighter.isAttributeSet(MOB.Attrib.AUTOMELEE)
						&&(fighter.amFollowing()!=null)
						&&(fighter.amFollowing().getVictim()==fighter.getVictim())
						&&(fighter.amFollowing().rangeToTarget()>=0)
						&&(fighter.amFollowing().fetchFollowerOrder(fighter)>=0))?
							fighter.amFollowing().fetchFollowerOrder(fighter)+fighter.amFollowing().rangeToTarget():-1;
		if(CMLib.flags().isAliveAwakeMobile(fighter,true))
		{
			if(((combatSystem!=CombatLibrary.CombatSystem.MANUAL)&&(combatSystem!=CombatLibrary.CombatSystem.TURNBASED))
			||(fighter.isMonster()))
			{
				final int saveAction=(combatSystem!=CombatLibrary.CombatSystem.DEFAULT)?0:1;
				int numAttacks=(int)Math.round(Math.floor(fighter.actions()))-saveAction;
				if((combatSystem==CombatLibrary.CombatSystem.DEFAULT)
				&&(numAttacks>(int)Math.round(Math.floor(fighter.phyStats().speed()+0.9))))
					numAttacks=(int)Math.round(Math.floor(fighter.phyStats().speed()+0.9));
				for(int s=0;s<numAttacks;s++)
				{
					if((!fighter.amDead())
					&&(fighter.curState().getHitPoints()>0)
					&&(fighter.isInCombat())
					&&(fighter.actions()>=1.0)
					&&((s==0)||(CMLib.flags().isStanding(fighter))))
					{
						fighter.setActions(fighter.actions()-1.0);
						subtickAttack(fighter,weapon,folrange);
					}
					else
						break;
				}
				if(CMLib.dice().rollPercentage()>(fighter.charStats().getStat(CharStats.STAT_CONSTITUTION)*4))
					fighter.curState().adjMovement(-1,fighter.maxState());
			}
		}
		subtickAfterAttack(fighter);
	}

	@Override
	public boolean isKnockedOutUponDeath(MOB deadM, MOB killerM)
	{
		String whatToDo=null;
		if(((deadM.isMonster())||(deadM.soulMate()!=null)))
			whatToDo=CMProps.getVar(CMProps.Str.MOBDEATH).toUpperCase();
		else
			whatToDo=CMProps.getVar(CMProps.Str.PLAYERDEATH).toUpperCase();
		final List<String> whatsToDo=CMParms.parseCommas(whatToDo,true);
		final double[] fakeVarVals={1.0,1.0,1.0};
		for(int w=0;w<whatsToDo.size();w++)
		{
			whatToDo=whatsToDo.get(w);
			if(whatToDo.startsWith("OUT ")&&(CMath.isMathExpression(whatToDo.substring(4).trim(),fakeVarVals)))
				return true;
		}
		return false;
	}

	@Override
	public void expendEnergy(final MOB mob, final boolean expendMovement)
	{
		if(mob==null)
		{
			return;
		}
		final Room room=mob.location();
		if(room!=null)
		{
			final CharState curState=mob.curState();
			final CharState maxState=mob.maxState();
			if(expendMovement)
			{
				int move=-room.pointsPerMove();
				if(mob.phyStats().weight()>mob.maxCarry())
					move+=(int)Math.round(CMath.mul(move,10.0*CMath.div(mob.phyStats().weight()-mob.maxCarry(),mob.maxCarry())));
				curState.adjMovement(move,maxState);
			}
			if((!CMLib.flags().isSleeping(mob))
			&&(!CMSecurity.isAllowed(mob,room,CMSecurity.SecFlag.IMMORT)))
			{
				int factor=mob.baseWeight()/500;
				if(factor<1)
					factor=1;
				if((!CMSecurity.isDisabled(CMSecurity.DisFlag.THIRST))
				&&(mob.maxState().getThirst() < (Integer.MAX_VALUE/2)))
					curState.adjThirst(-(room.thirstPerRound()*factor),maxState.maxThirst(mob.baseWeight()));
				if((!CMSecurity.isDisabled(CMSecurity.DisFlag.HUNGER))
				&&(mob.maxState().getHunger() < (Integer.MAX_VALUE/2)))
					curState.adjHunger(-factor,maxState.maxHunger(mob.baseWeight()));
			}
			final boolean thirsty=(curState.getThirst()<=0);
			final boolean hungry=(curState.getHunger()<=0);
			if((hungry||thirsty)&&(!expendMovement))
			{
				final int ticksThirsty=curState.adjTicksThirsty(thirsty);
				final int ticksHungry=curState.adjTicksHungry(hungry);

				if((ticksThirsty>CharState.DEATH_THIRST_TICKS)
				||(ticksHungry>CharState.DEATH_HUNGER_TICKS))
				{
					if(thirsty)
						mob.tell(L("YOU ARE DYING OF THIRST!"));
					if(hungry)
						mob.tell(L("YOU ARE DYING OF HUNGER!"));
					postDeath(null,mob,null);
				}
				else
				if(ticksThirsty>CharState.DEATH_THIRST_TICKS-30)
					mob.tell(L("You are dehydrated, and near death.  DRINK SOMETHING!"));
				else
				if(ticksHungry>CharState.DEATH_HUNGER_TICKS-30)
					mob.tell(L("You are starved, and near death.  EAT SOMETHING!"));
				else
				{
					if(thirsty && ((ticksThirsty-1 % CharState.ANNOYANCE_DEFAULT_TICKS)==0))
					{
						if(ticksThirsty>((CharState.DEATH_THIRST_TICKS/2)+(CharState.DEATH_THIRST_TICKS/4)))
							mob.tell(L("You are dehydrated! Drink something!"));
						else
						if(ticksThirsty>(CharState.DEATH_THIRST_TICKS/2))
							mob.tell(L("You are parched! Drink something!"));
						else
							mob.tell(L("You are thirsty."));
					}
					if((hungry) && ((ticksHungry-1 % CharState.ANNOYANCE_DEFAULT_TICKS)==0))
					{
						if(ticksHungry>((CharState.DEATH_HUNGER_TICKS/2)+(CharState.DEATH_HUNGER_TICKS/4)))
							mob.tell(L("You are starved! Eat something!"));
						else
						if(ticksHungry>(CharState.DEATH_HUNGER_TICKS/2))
							mob.tell(L("You are famished! Eat something!"));
						else
							mob.tell(L("You are hungry."));
					}
				}
			}
		}
	}

	@Override
	public boolean handleCombatLossConsequences(MOB deadM, MOB killerM, String[] consequences, int[] lostExperience, String message)
	{
		if((consequences==null)||(consequences.length==0))
			return false;
		if(lostExperience==null)
			lostExperience=new int[1];
		final int baseExperience=lostExperience[0];
		lostExperience[0]=0;
		int rejuv=deadM.phyStats().rejuv();
		if((rejuv==0)||(rejuv==Integer.MAX_VALUE))
			rejuv=deadM.phyStats().level();
		if(((!deadM.isMonster())&&(deadM.soulMate()==null)))
			rejuv=1;
		final double[] varVals={
				deadM.basePhyStats().level()>deadM.phyStats().level()?deadM.basePhyStats().level():deadM.phyStats().level(),
				(killerM!=null)?killerM.phyStats().level():0,
				rejuv
		};
		for (final String command : consequences)
		{
			final String whatToDo=command.toUpperCase();
			if(whatToDo.startsWith("UNL"))
			{
				final Vector<String> V=CMParms.parse(whatToDo);
				int times=1;
				if((V.size()>1)&&(CMath.s_int(V.lastElement())>1))
					times=CMath.s_int(V.lastElement());
				for(int t=0;t<times;t++)
					CMLib.leveler().unLevel(deadM);
			}
			else
			if(whatToDo.startsWith("RECALL"))
				deadM.killMeDead(false);
			else
			if(whatToDo.startsWith("ASTR"))
			{
				final Ability A=CMClass.getAbility("Prop_AstralSpirit");
				if((A!=null)&&(deadM.fetchAbility(A.ID())==null))
				{
					deadM.tell(L("^HYou are now a spirit.^N"));
					if(whatToDo.startsWith("ASTRAL_R"))
					{
						A.setMiscText("SELF-RES");
						deadM.tell(L("^HFind your corpse and use ENTER [body name] to re-enter your body.^N"));
					}
					else
						deadM.tell(L("^HFind your corpse have someone resurrect it.^N"));
					deadM.addAbility(A);
					A.autoInvocation(deadM, false);
				}
			}
			else
			if(whatToDo.startsWith("OUT ")&&(CMath.isMathExpression(whatToDo.substring(4).trim(),varVals)))
			{
				final Ability A=CMClass.getAbility("Skill_ArrestingSap");
				final int tickDown=CMath.s_parseIntExpression(whatToDo.substring(4).trim(),varVals);
				if((A!=null)&&(tickDown>0))
				{
					A.invoke(deadM,new XVector<String>(""+tickDown,"SAFELY"),deadM,true,0);
					deadM.resetToMaxState();
				}
			}
			else
			if(whatToDo.startsWith("PUR"))
			{
				final MOB deadMOB=CMLib.players().getLoadPlayer(deadM.Name());
				if(deadMOB!=null)
				{
					CMLib.players().obliteratePlayer(deadMOB,true,false);
					return false;
				}
			}
			else
			if(whatToDo.startsWith("LOSESK"))
			{
				if(deadM.numAbilities()>0)
				{
					final Ability A=deadM.fetchAbility(CMLib.dice().roll(1,deadM.numAbilities(),-1));
					if(A!=null)
					{
						deadM.tell(L("You've forgotten @x1.",A.Name()));
						deadM.delAbility(A);
						if(A.isAutoInvoked())
						{
							final Ability A2=deadM.fetchEffect(A.ID());
							A2.unInvoke();
							deadM.delEffect(A2);
						}
					}
				}
			}
			else
			if(CMath.isMathExpression(whatToDo,varVals))
			{
				lostExperience[0]=CMath.s_parseIntExpression(whatToDo,varVals);
				if(lostExperience[0]>0)
				{
					message=CMStrings.replaceAll(message,"@x1",""+lostExperience[0]);
					deadM.tell(message);
					CMLib.leveler().postExperience(deadM,null,null,-lostExperience[0],false);
				}
			}
			else
			if(whatToDo.startsWith("EXPER"))
			{
				lostExperience[0]=baseExperience;
				if(lostExperience[0]>0)
				{
					message=CMStrings.replaceAll(message,"@x1",""+baseExperience);
					deadM.tell(message);
					CMLib.leveler().postExperience(deadM,null,null,-baseExperience,false);
				}
			}
		}
		return true;
	}

	@Override
	public boolean postRevengeAttack(MOB attacker, MOB defender)
	{
		if((attacker!=null)
		&&(!attacker.isInCombat())
		&&(!attacker.amDead())
		&&(!attacker.amDestroyed())
		&&(attacker.isMonster())
		&&(attacker!=defender)
		&&(defender!=null)
		&&(attacker.location()==defender.location())
		&&(attacker.location().isInhabitant(defender))
		&&(CMLib.flags().canBeSeenBy(defender,attacker)))
			return postAttack(attacker,defender,attacker.fetchWieldedItem());
		return false;
	}
	
	@Override
	public int getTickStatus()
	{
		return tickStatus;
	}

	protected void tickAllShips()
	{
		for(final Enumeration<BoardableShip> s = CMLib.map().ships();s.hasMoreElements();)
		{
			final BoardableShip ship = s.nextElement();
			ship.tick(ship, Tickable.TICKID_SPECIALMANEUVER);
			ship.tick(ship, Tickable.TICKID_SPECIALCOMBAT);
		}
	}
	
	protected void runSpecialCombat()
	{
		tickAllShips();
	}
	
	@Override 
	public boolean tick(Tickable ticking, int tickID)
	{
		try
		{
			if(!CMSecurity.isDisabled(CMSecurity.DisFlag.SPECOMBATTHREAD))
			{
				isDebugging=CMSecurity.isDebugging(DbgFlag.UTILITHREAD);
				tickStatus=Tickable.STATUS_ALIVE;
				runSpecialCombat();
			}
		}
		finally
		{
			tickStatus=Tickable.STATUS_NOT;
			setThreadStatus(serviceClient,"sleeping");
		}
		return true;
	}

	@Override
	public MOB getBreatheKiller(final MOB victim)
	{
		MOB victimKiller = victim.getVictim();
		if(victimKiller != null)
			return victimKiller;
		final Room R=victim.location();
		if(R!=null)
		{
			for(final Enumeration<Ability> a=R.effects();a.hasMoreElements();)
			{
				final Ability A=a.nextElement();
				if((A!=null)
				&&(A.invoker()!=null)
				&&(A.invoker()!=victim))
				{
					if(A.abstractQuality()==Ability.QUALITY_MALICIOUS)
						return A.invoker();
					victimKiller = A.invoker();
				}
			}
		}
		for(final Enumeration<Ability> a=victim.personalEffects();a.hasMoreElements();)
		{
			final Ability A=a.nextElement();
			if((A!=null)
			&&(A.invoker()!=null)
			&&(A.invoker()!=victimKiller))
			{
				if(A.abstractQuality()==Ability.QUALITY_MALICIOUS)
					return A.invoker();
				victimKiller = A.invoker();
			}
		}
		return (victimKiller == null) ? victim : victimKiller;
	}
	
	@Override
	public boolean shutdown()
	{
		if(CMLib.threads().isTicking(this, TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK))
		{
			CMLib.threads().deleteTick(this, TICKID_SUPPORT|Tickable.TICKID_SOLITARYMASK);
			serviceClient=null;
		}
		return true;
	}

}