package com.planet_ink.coffee_mud.Races;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.AbilityMapper.AbilityMapping;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.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.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.lang.ref.WeakReference;
import java.util.*;

/*
   Copyright 2001-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.
*/
@SuppressWarnings({"unchecked","rawtypes"})
public class StdRace implements Race
{
	@Override
	public String ID()
	{
		return "StdRace";
	}

	protected static final int[] breatheAnythingArray	= new int[0];
	protected static final int[] breatheAirArray		= new int[] { RawMaterial.RESOURCE_AIR };
	protected static final int[] breatheWaterArray		= new int[] { RawMaterial.RESOURCE_FRESHWATER, RawMaterial.RESOURCE_SALTWATER };
	protected static final int[] breatheAirWaterArray	= new int[] { RawMaterial.RESOURCE_FRESHWATER, RawMaterial.RESOURCE_SALTWATER, RawMaterial.RESOURCE_AIR };
	protected static final List empty					= new ReadOnlyVector(1);
	//  												   an ey ea he ne ar ha to le fo no gi mo wa ta wi
	private static final int[] 	parts					= {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
	protected static final SearchIDList emptyIDs = new CMUniqSortSVec(1);

	private final int[]		agingChart				= { 0, 1, 3, 15, 35, 53, 70, 74, 78 };
	protected String		baseStatChgDesc			= null;
	protected String		sensesChgDesc			= null;
	protected String		dispChgDesc				= null;
	protected String		abilitiesDesc			= null;
	protected String		languagesDesc			= null;
	protected Weapon		naturalWeapon			= null;
	protected boolean		mappedCulturalAbilities	= false;
	protected List<Item>	outfitChoices			= null;
	protected List<Weapon>	naturalWeaponChoices	= null;
	protected Set<String>	naturalAbilImmunities	= new HashSet<String>();
	
	protected Map<Integer,SearchIDList<Ability>> racialAbilityMap	= null;
	protected Map<Integer,SearchIDList<Ability>> racialEffectMap	= null;

	private final static String localizedStaticName = CMLib.lang().L("StdRace");

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

	@Override
	public int shortestMale()
	{
		return 24;
	}

	@Override
	public int shortestFemale()
	{
		return 24;
	}

	@Override
	public int heightVariance()
	{
		return 5;
	}

	@Override
	public int lightestWeight()
	{
		return 60;
	}

	@Override
	public int weightVariance()
	{
		return 10;
	}

	@Override
	public long forbiddenWornBits()
	{
		return 0;
	}

	private final static String localizedStaticRacialCat = CMLib.lang().L("Unknown");

	@Override
	public String racialCategory()
	{
		return localizedStaticRacialCat;
	}

	@Override
	public boolean isGeneric()
	{
		return false;
	}

	@Override
	public boolean classless()
	{
		return false;
	}

	@Override
	public boolean leveless()
	{
		return false;
	}

	@Override
	public boolean expless()
	{
		return false;
	}

	@Override
	public int[] bodyMask()
	{
		return parts;
	}

	@Override
	public int[] getAgingChart()
	{
		return agingChart;
	}

	@Override
	public int[] getBreathables()
	{
		return breatheAirArray;
	}

	@Override
	public boolean useRideClass()
	{
		return false;
	}

	protected int practicesAtFirstLevel()
	{
		return 0;
	}

	protected int trainsAtFirstLevel()
	{
		return 0;
	}

	protected String[] racialEffectNames()
	{
		return null;
	}

	protected int[] racialEffectLevels()
	{
		return null;
	}

	protected String[] racialEffectParms()
	{
		return null;
	}

	protected String[] racialAbilityNames()
	{
		return null;
	}

	protected int[] racialAbilityLevels()
	{
		return null;
	}

	protected int[] racialAbilityProficiencies()
	{
		return null;
	}

	protected boolean[] racialAbilityQuals()
	{
		return null;
	}

	protected String[] culturalAbilityNames()
	{
		return null;
	}

	@Override
	public String[] abilityImmunities()
	{
		return CMParms.toStringArray(this.naturalAbilImmunities);
	}

	protected int[] culturalAbilityProficiencies()
	{
		return null;
	}

	protected boolean uncharmable()
	{
		return false;
	}

	protected boolean destroyBodyAfterUse()
	{
		return false;
	}

	@Override
	public int availabilityCode()
	{
		return Area.THEME_FANTASY | Area.THEME_SKILLONLYMASK;
	}

	@Override
	public CMObject newInstance()
	{
		return this;
	}

	@Override
	public void initializeClass()
	{
	}
	
	public boolean fertile()
	{
		return true;
	}

	@Override
	public CMObject copyOf()
	{
		try
		{
			final StdRace E=(StdRace)this.clone();
			return E;
		}
		catch(final CloneNotSupportedException e)
		{
			return this;
		}
	}

	public Race healthBuddy()
	{
		return this;
	}

	@Override
	public boolean canBreedWith(Race R)
	{
		if(!fertile())
			return false;
		if(R==null)
			return false;
		if(ID().equals("Human")||R.ID().equals("Human"))
			return true;
		return ID().equals(R.ID());
	}

	@Override
	public void affectPhyStats(Physical affected, PhyStats affectableStats)
	{

	}

	@Override
	public void affectCharStats(MOB affectedMob, CharStats charStats)
	{
	}

	@Override
	public String makeMobName(char gender, int age)
	{
		switch(age)
		{
			case Race.AGE_INFANT:
			{
				switch(gender)
				{
				case 'M':
				case 'm':
					return L("boy infant @x1", name().toLowerCase());
				case 'F':
				case 'f':
					return L("girl infant @x1", name().toLowerCase());
				default:
					return L("baby @x1", name().toLowerCase());
				}
			}
			case Race.AGE_TODDLER:
			{
				switch(gender)
				{
				case 'M':
				case 'm':
					return L("baby boy @x1", name().toLowerCase());
				case 'F':
				case 'f':
					return L("baby girl @x1", name().toLowerCase());
				default:
					return L("baby @x1", name().toLowerCase());
				}
			}
			case Race.AGE_CHILD:
			{
				switch(gender)
				{
				case 'M':
				case 'm':
					return L("little boy @x1", name().toLowerCase());
				case 'F':
				case 'f':
					return L("little girl @x1", name().toLowerCase());
				default:
					return L("young @x1", name().toLowerCase());
				}
			}
			case Race.AGE_YOUNGADULT:
			case Race.AGE_MATURE:
			case Race.AGE_MIDDLEAGED:
			default:
			{
				switch(gender)
				{
				case 'M':
				case 'm':
					return L("male @x1", name().toLowerCase());
				case 'F':
				case 'f':
					return L("female @x1", name().toLowerCase());
				default:
					return name().toLowerCase();
				}
			}
			case Race.AGE_OLD:
			case Race.AGE_VENERABLE:
			case Race.AGE_ANCIENT:
			{
				switch(gender)
				{
				case 'M':
				case 'm':
					return L("old male @x1",name().toLowerCase());
				case 'F':
				case 'f':
					return L("old female @x1",name().toLowerCase());
				default:
					return L("old @x1",name().toLowerCase());
				}
			}
		}
	}

	@Override
	public void agingAffects(MOB mob, CharStats baseStats, CharStats charStats)
	{
		if((baseStats.getStat(CharStats.STAT_AGE)>0)
		&&(!CMSecurity.isAllowed(mob,mob.location(),CMSecurity.SecFlag.IMMORT))
		&&(!CMSecurity.isDisabled(CMSecurity.DisFlag.ALL_AGEING)))
		{
			switch(baseStats.ageCategory())
			{
				case -1: break;
				case Race.AGE_INFANT:
				case Race.AGE_TODDLER:
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)-10);
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-2);
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)-4);
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)-4);
					break;
				case Race.AGE_CHILD:
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)-5);
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-1);
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)-2);
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)-2);
					break;
				case Race.AGE_YOUNGADULT:
				case Race.AGE_MATURE:
					break;
				case Race.AGE_MIDDLEAGED:
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)+5);
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-1);
					charStats.setStat(CharStats.STAT_MAX_CONSTITUTION_ADJ,charStats.getStat(CharStats.STAT_MAX_CONSTITUTION_ADJ)-1);
					charStats.setStat(CharStats.STAT_MAX_DEXTERITY_ADJ,charStats.getStat(CharStats.STAT_MAX_DEXTERITY_ADJ)-1);
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)+1);
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)+1);
					charStats.setStat(CharStats.STAT_MAX_CHARISMA_ADJ,charStats.getStat(CharStats.STAT_MAX_CHARISMA_ADJ)+1);
					break;
				case Race.AGE_OLD:
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)+10);
					charStats.setStat(CharStats.STAT_SAVE_UNDEAD,charStats.getStat(CharStats.STAT_SAVE_UNDEAD)-5);
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-2);
					charStats.setStat(CharStats.STAT_MAX_CONSTITUTION_ADJ,charStats.getStat(CharStats.STAT_MAX_CONSTITUTION_ADJ)-2);
					charStats.setStat(CharStats.STAT_MAX_DEXTERITY_ADJ,charStats.getStat(CharStats.STAT_MAX_DEXTERITY_ADJ)-2);
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)+2);
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)+2);
					charStats.setStat(CharStats.STAT_MAX_CHARISMA_ADJ,charStats.getStat(CharStats.STAT_MAX_CHARISMA_ADJ)+2);
					break;
				case Race.AGE_VENERABLE:
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)+15);
					charStats.setStat(CharStats.STAT_SAVE_UNDEAD,charStats.getStat(CharStats.STAT_SAVE_UNDEAD)-25);
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-3);
					charStats.setStat(CharStats.STAT_MAX_CONSTITUTION_ADJ,charStats.getStat(CharStats.STAT_MAX_CONSTITUTION_ADJ)-3);
					charStats.setStat(CharStats.STAT_MAX_DEXTERITY_ADJ,charStats.getStat(CharStats.STAT_MAX_DEXTERITY_ADJ)-3);
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)+3);
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)+3);
					charStats.setStat(CharStats.STAT_MAX_CHARISMA_ADJ,charStats.getStat(CharStats.STAT_MAX_CHARISMA_ADJ)+3);
					break;
				case Race.AGE_ANCIENT:
				{
					final int[] chart=getAgingChart();
					final int diff=chart[Race.AGE_ANCIENT]-chart[Race.AGE_VENERABLE];
					final int age=baseStats.getStat(CharStats.STAT_AGE)-chart[Race.AGE_ANCIENT];
					int num=(diff>0)?(int)Math.abs(Math.floor(CMath.div(age,diff)))-1:1;
					if(num==0)
						num=1;
					if(num>16)
						num=16;
					charStats.setStat(CharStats.STAT_SAVE_MIND,charStats.getStat(CharStats.STAT_SAVE_MIND)+20+(5*num));
					charStats.setStat(CharStats.STAT_SAVE_UNDEAD,charStats.getStat(CharStats.STAT_SAVE_UNDEAD)-50+15+(5*num));
					charStats.setStat(CharStats.STAT_MAX_STRENGTH_ADJ,charStats.getStat(CharStats.STAT_MAX_STRENGTH_ADJ)-(3+(1*num)));
					charStats.setStat(CharStats.STAT_MAX_CONSTITUTION_ADJ,charStats.getStat(CharStats.STAT_MAX_CONSTITUTION_ADJ)-(3+(num)));
					charStats.setStat(CharStats.STAT_MAX_DEXTERITY_ADJ,charStats.getStat(CharStats.STAT_MAX_DEXTERITY_ADJ)-(3+(num)));
					charStats.setStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ,charStats.getStat(CharStats.STAT_MAX_INTELLIGENCE_ADJ)+(3+(num)));
					charStats.setStat(CharStats.STAT_MAX_WISDOM_ADJ,charStats.getStat(CharStats.STAT_MAX_WISDOM_ADJ)+(3+(num)));
					charStats.setStat(CharStats.STAT_MAX_CHARISMA_ADJ,charStats.getStat(CharStats.STAT_MAX_CHARISMA_ADJ)+(3+(num)));
					break;
				}
			}
		}
	}

	@Override
	public void affectCharState(MOB affectedMob, CharState affectableMaxState)
	{

	}

	@Override
	public boolean okMessage(final Environmental myHost, final CMMsg msg)
	{
		if((msg.target()==myHost)
		&&(msg.tool() instanceof Ability)
		&&(myHost instanceof MOB))
		{
			if(uncharmable()
			&&(CMath.bset(((Ability)msg.tool()).flags(),Ability.FLAG_CHARMING)))
			{
				msg.source().location().show(msg.source(),myHost,CMMsg.MSG_OK_VISUAL,L("<T-NAME> seem(s) unaffected by the charm magic from <S-NAMESELF>."));
				return false;
			}
			else
			if(naturalAbilImmunities.contains(msg.tool().ID()))
			{
				return false;
			}
		}
		return true;
	}

	@Override
	public void executeMsg(final Environmental myHost, final CMMsg msg)
	{
		// the sex rules
		if(!(myHost instanceof MOB))
			return;

		final MOB myChar=(MOB)myHost;
		if((msg.tool() instanceof Social)
		&&(msg.amITarget(myChar)||(msg.source()==myChar))
		&&(myChar.location()==msg.source().location())
		&&(msg.sourceMinor()!=CMMsg.TYP_CHANNEL)
		&&(msg.tool().Name().startsWith("MATE ")
			||msg.tool().Name().startsWith("SEX ")))
		{
			if(msg.tool().Name().endsWith("SELF"))
			{
				if((msg.source()==myChar)
				&&(fertile())
				&&(msg.source().fetchWornItems(Wearable.WORN_LEGS|Wearable.WORN_WAIST,(short)-2048,(short)0).size()==0))
				{
					if(msg.source().maxState().getFatigue()>Long.MIN_VALUE/2)
						msg.source().curState().adjFatigue(CharState.FATIGUED_MILLIS,msg.source().maxState());
					if(myChar.maxState().getFatigue()>Long.MIN_VALUE/2)
						myChar.curState().adjFatigue(CharState.FATIGUED_MILLIS,myChar.maxState());
					final Ability A=CMClass.getAbility("Spell_Blindness");
					if(A!=null)
						A.invoke(myChar,myChar,true,myChar.phyStats().level());
				}
			}
			else
			if((msg.target()==myChar)
			&&(msg.tool().Name().endsWith("<T-NAME>")))
			{
				final boolean srcExhausted=((msg.source().curState().getMovement()<(msg.source().maxState().getMovement()/2))
						||(msg.source().curState().getFatigue()>=CharState.FATIGUED_MILLIS));
				final boolean meExhausted=((myChar.curState().getMovement()<(myChar.maxState().getMovement()/2))
						||(myChar.curState().getFatigue()>=CharState.FATIGUED_MILLIS));
				if((myChar.charStats().getStat(CharStats.STAT_GENDER)==('F'))
				&&(msg.source().charStats().getStat(CharStats.STAT_GENDER)==('M'))
				&&(myChar.fetchWornItems(Wearable.WORN_LEGS|Wearable.WORN_WAIST,(short)-2048,(short)0).size()==0)
				&&(msg.source().fetchWornItems(Wearable.WORN_LEGS|Wearable.WORN_WAIST,(short)-2048,(short)0).size()==0)
				&&(msg.source().charStats().getMyRace().canBreedWith(this))
				&&((msg.source().charStats().getStat(CharStats.STAT_AGE)==0)
						||((msg.source().charStats().ageCategory()>Race.AGE_CHILD)
								&&(msg.source().charStats().ageCategory()<Race.AGE_OLD)))
				&&((myChar.charStats().getStat(CharStats.STAT_AGE)==0)
						||((myChar.charStats().ageCategory()>Race.AGE_CHILD)
								&&(myChar.charStats().ageCategory()<Race.AGE_OLD))))
				{
					if(srcExhausted)
						msg.source().tell(L("You are exhausted!"));
					else
					{
						if(msg.source().maxState().getFatigue()>Long.MIN_VALUE/2)
							msg.source().curState().adjFatigue(CharState.FATIGUED_MILLIS,msg.source().maxState());
						msg.source().curState().adjMovement(-msg.source().maxState().getMovement()/2, msg.source().maxState());
					}
					if(meExhausted)
						myChar.tell(L("You are exhausted!"));
					else
					{
						if(myChar.maxState().getFatigue()>Long.MIN_VALUE/2)
							myChar.curState().adjFatigue(CharState.FATIGUED_MILLIS,myChar.maxState());
						myChar.curState().adjMovement(-myChar.maxState().getMovement()/2, myChar.maxState());
					}
					if(!srcExhausted && !meExhausted && (CMLib.dice().rollPercentage()<10))
					{
						final Ability A=CMClass.getAbility("Pregnancy");
						if((A!=null)
						&&(myChar.fetchAbility(A.ID())==null)
						&&(myChar.fetchEffect(A.ID())==null))
							A.invoke(msg.source(),myChar,true,0);
					}
				}
			}
		}
	}

	@Override
	public String arriveStr()
	{
		return "arrives";
	}

	@Override
	public String leaveStr()
	{
		return "leaves";
	}

	@Override
	public void level(MOB mob, List<String> gainedAbilityIDs)
	{
	}

	@Override
	public int adjustExperienceGain(MOB host, MOB mob, MOB victim, int amount)
	{
		return amount;
	}

	@Override
	public int getTickStatus()
	{
		return Tickable.STATUS_NOT;
	}

	@Override
	public boolean tick(Tickable myChar, int tickID)
	{
		return true;
	}
	
	public String L(final String str, final String ... xs)
	{
		return CMLib.lang().fullSessionTranslation(str, xs);
	}
	
	@Override
	public void startRacing(MOB mob, boolean verifyOnly)
	{
		if((!mappedCulturalAbilities)
		&&(culturalAbilityNames()!=null))
		{
			for(int a=0;a<culturalAbilityNames().length;a++)
				CMLib.ableMapper().addCharAbilityMapping(ID(),0,culturalAbilityNames()[a],false);
			mappedCulturalAbilities=true;
		}
		for(final Ability A : racialAbilities(mob))
		{
			if((A!=null)
			&&((A.classificationCode()&Ability.ALL_ACODES)==Ability.ACODE_LANGUAGE))
			{
				A.autoInvocation(mob, false);
				A.invoke(mob,mob,false,0);
				break;
			}
		}
		if(!verifyOnly)
		{
			if(mob.basePhyStats().level()<=1)
			{
				mob.setPractices(mob.getPractices()+practicesAtFirstLevel());
				mob.setTrains(mob.getTrains()+trainsAtFirstLevel());
			}
			setHeightWeight(mob.basePhyStats(),(char)mob.baseCharStats().getStat(CharStats.STAT_GENDER));

			final boolean isChild=CMLib.flags().isChild(mob);
			final boolean isAnimal=CMLib.flags().isAnimalIntelligence(mob);
			final boolean isMonster=mob.isMonster();
			
			if((culturalAbilityNames()!=null)&&(culturalAbilityProficiencies()!=null)
			&&(culturalAbilityNames().length==culturalAbilityProficiencies().length))
			{
				for(int a=0;a<culturalAbilityNames().length;a++)
				{
					Ability A=CMClass.getAbility(culturalAbilityNames()[a]);
					if(A!=null)
					{
						A.setProficiency(culturalAbilityProficiencies()[a]);
						mob.addAbility(A);
						A.autoInvocation(mob, false);
						if(((A.classificationCode()&Ability.ALL_ACODES)==Ability.ACODE_LANGUAGE)
						&&(isMonster)
						&&(!isChild))
						{
							if(A.proficiency()>0)
								A.setProficiency(100);
							A.invoke(mob,mob,false,0);
							if(isChild && (!isAnimal))
							{
								A=mob.fetchAbility("Common");
								if(A==null){ A=CMClass.getAbility("Common"); if(A!=null)mob.addAbility(A);}
								if(A!=null)
									A.setProficiency(100);
							}
						}
					}
				}
			}
		}
	}

	@Override
	public Weapon myNaturalWeapon()
	{
		if(naturalWeapon==null)
			naturalWeapon=CMClass.getWeapon("Natural");
		return naturalWeapon;
	}

	@Override
	public List<Item> outfit(MOB myChar)
	{
		return outfitChoices;
	}

	@Override
	public String healthText(MOB viewer, MOB mob)
	{
		return CMLib.combat().standardMobCondition(viewer,mob);
	}

	protected Weapon funHumanoidWeapon()
	{
		if(naturalWeaponChoices==null)
		{
			naturalWeaponChoices=new Vector<Weapon>();
			for(int i=1;i<11;i++)
			{
				naturalWeapon=CMClass.getWeapon("StdWeapon");
				if(naturalWeapon==null)
					continue;
				naturalWeapon.setMaterial(RawMaterial.RESOURCE_LEATHER);
				switch (i)
				{
				case 1:
				case 2:
				case 3:
					naturalWeapon.setName(L("a quick punch"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 4:
					naturalWeapon.setName(L("fingernails and teeth"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_PIERCING);
					break;
				case 5:
					naturalWeapon.setName(L("an elbow"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 6:
					naturalWeapon.setName(L("a backhand"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 7:
					naturalWeapon.setName(L("a strong jab"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 8:
					naturalWeapon.setName(L("a stinging punch"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 9:
					if(bodyMask()[Race.BODY_LEG]>0)
						naturalWeapon.setName(L("a knee"));
					else
					if(bodyMask()[Race.BODY_GILL]>0)
						naturalWeapon.setName(L("a fin"));
					else
						naturalWeapon.setName(L("a limb"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				case 10:
					naturalWeapon.setName(L("a head butt"));
					naturalWeapon.setWeaponDamageType(Weapon.TYPE_BASHING);
					break;
				}
				naturalWeapon.setUsesRemaining(1000);
				naturalWeaponChoices.add(naturalWeapon);
			}
		}
		if(naturalWeaponChoices.size()>0)
			return naturalWeaponChoices.get(CMLib.dice().roll(1,naturalWeaponChoices.size(),-1));
		return CMClass.getWeapon("Natural");
	}

	@Override
	public List<RawMaterial> myResources()
	{
		return new Vector<RawMaterial>();
	}

	@Override
	public void setHeightWeight(PhyStats stats, char gender)
	{
		int weightModifier=0;
		if(weightVariance()>0)
			weightModifier=CMLib.dice().roll(1,weightVariance(),0);
		stats.setWeight(lightestWeight()+weightModifier);
		int heightModifier=0;
		if(heightVariance()>0)
		{
			if(weightModifier>0)
			{
				final double variance=CMath.div(weightModifier,weightVariance());
				heightModifier=(int)Math.round(CMath.mul(heightVariance(),variance));
			}
			else
				heightModifier=CMLib.dice().roll(1,heightVariance(),0);
		}
		if (gender == 'M')
			stats.setHeight(shortestMale()+heightModifier);
		 else
			stats.setHeight(shortestFemale()+heightModifier);
	}

	@Override
	public int compareTo(CMObject o)
	{
		return CMClass.classID(this).compareToIgnoreCase(CMClass.classID(o));
	}

	protected RawMaterial makeResource(String name, int type)
	{
		final PhysicalAgent A = CMLib.materials().makeResource(type,ID(),true,name);
		if(A instanceof RawMaterial)
			return (RawMaterial)A;
		return null;
	}

	@Override
	public DeadBody getCorpseContainer(MOB mob, Room room)
	{
		if(room==null)
			room=mob.location();

		final DeadBody bodyI=(DeadBody)CMClass.getItem("Corpse");
		if(mob.isMonster())
		{
			if((mob.amFollowing()!=null)
			&&((!mob.amFollowing().isMonster())||(!mob.amUltimatelyFollowing().isMonster())))
				bodyI.setSavedMOB((MOB)mob.copyOf());
		}
		
		bodyI.setCharStats((CharStats)mob.baseCharStats().copyOf());
		bodyI.basePhyStats().setLevel(mob.basePhyStats().level());
		bodyI.basePhyStats().setWeight(mob.basePhyStats().weight());
		bodyI.setIsPlayerCorpse(!mob.isMonster());
		bodyI.setTimeOfDeath(System.currentTimeMillis());
		bodyI.setMobPKFlag(mob.isAttributeSet(MOB.Attrib.PLAYERKILL));
		bodyI.setName(L("the body of @x1",mob.Name().replace('\'','`')));
		bodyI.setMobName(mob.Name().replace('\'','`'));
		bodyI.setMobHash(mob.hashCode());
		bodyI.setMobDescription(mob.description().replace('\'','`'));
		bodyI.setDisplayText(L("the body of @x1 lies here.",mob.Name().replace('\'','`')));
		final Ability ageA=mob.fetchEffect("Age");
		if(ageA!=null)
			bodyI.addNonUninvokableEffect(ageA);
		if(room!=null)
		{
			final ItemPossessor.Expire expireCode;
			if(mob.isMonster() && (mob.playerStats()==null))
				expireCode=ItemPossessor.Expire.Monster_Body;
			else
				expireCode=ItemPossessor.Expire.Player_Body;
			room.addItem(bodyI,expireCode);
		}
		bodyI.setIsDestroyAfterLooting(destroyBodyAfterUse());
		bodyI.recoverPhyStats();
		for(final Enumeration<Ability> a=mob.effects();a.hasMoreElements();)
		{
			final Ability A=a.nextElement();
			if((A!=null)&&(A instanceof DiseaseAffect))
			{
				if((CMath.bset(((DiseaseAffect)A).spreadBitmap(),DiseaseAffect.SPREAD_CONSUMPTION))
				||(CMath.bset(((DiseaseAffect)A).spreadBitmap(),DiseaseAffect.SPREAD_CONTACT)))
					bodyI.addNonUninvokableEffect((Ability)A.copyOf());
			}
		}

		final Vector<Item> items=new Vector<Item>();
		CMLib.beanCounter().getTotalAbsoluteNativeValue(mob); // converts mob.get-Money();
		if(mob.getMoneyVariation()>0.0)
			CMLib.beanCounter().addMoney(mob, Math.random()*mob.getMoneyVariation());
		else
		if(mob.getMoneyVariation()<0.0)
			CMLib.beanCounter().subtractMoney(mob, -(Math.random()*mob.getMoneyVariation()));
		final Hashtable<Item,Container> containerMap=new Hashtable<Item,Container>();
		final Hashtable<Item,Container> itemMap=new Hashtable<Item,Container>();
		final LinkedList<Item> itemsToGo=new LinkedList<Item>();
		for(int i=0;i<mob.numItems();i++)
		{
			final Item thisItem=mob.getItem(i);
			if(thisItem != null)
				itemsToGo.add(thisItem);
		}
		for(Item thisItem : itemsToGo)
		{
			if((thisItem!=null)&&(thisItem.isSavable()))
			{
				if(mob.isMonster())
				{
					Item newItem=CMLib.utensils().isRuinedLoot(mob,thisItem);
					if(newItem==null)
						continue;
					if(newItem==thisItem)
						newItem=(Item)thisItem.copyOf();
					if(newItem != null)
					{
						if(newItem instanceof Container)
							itemMap.put(thisItem,(Container)newItem);
						if(thisItem.container()!=null)
							containerMap.put(thisItem,thisItem.container());
						newItem.setContainer(null);
						newItem.setExpirationDate( System.currentTimeMillis() +
												   CMProps.getIntVar( CMProps.Int.EXPIRE_MONSTER_EQ )* TimeManager.MILI_HOUR );
						newItem.recoverPhyStats();
						thisItem=newItem;
					}
				}
				else
					mob.delItem(thisItem);
				thisItem.unWear();
				if(thisItem.container()==null)
					thisItem.setContainer(bodyI);
				if(room!=null)
					room.addItem(thisItem);
				items.addElement(thisItem);
			}
			else
			if(thisItem!=null)
				mob.delItem(thisItem);
		}
		itemsToGo.clear();

		final Item dropItem=CMLib.catalog().getDropItem(mob,false);
		if(dropItem!=null)
		{
			dropItem.unWear();
			if(dropItem.container()==null)
				dropItem.setContainer(bodyI);
			if(room!=null)
				room.addItem(dropItem);
			items.addElement(dropItem);
		}

		for(final Enumeration e=itemMap.keys();e.hasMoreElements();)
		{
			final Item oldItem=(Item)e.nextElement();
			final Item newItem=itemMap.get(oldItem);
			final Item oldContainer=containerMap.get(oldItem);
			if((oldContainer!=null)&&(newItem!=null))
				newItem.setContainer(itemMap.get(oldContainer));
		}
		if(destroyBodyAfterUse())
		{
			for(Item I : myResources())
			{
				if(I!=null)
				{
					I=(Item)I.copyOf();
					I.setContainer(bodyI);
					if(room!=null)
						room.addItem(I,ItemPossessor.Expire.Monster_EQ);
				}
			}
		}
		else
		if((room != null)&&(room.isContent(bodyI)))
		{
			// remove duplicate already looted corpses
			for(int i=0;i<room.numItems();i++)
			{
				final Item thisItem=mob.getItem(i);
				if((thisItem instanceof DeadBody)
				&&(thisItem.Name().equals(bodyI.Name()))
				&&(((DeadBody)thisItem).getContents().size()==0))
				{
					thisItem.destroy();
					break;
				}
			}
		}
		return bodyI;
	}

	@Override
	public int numRacialEffects(MOB mob)
	{
		return racialEffectsList(mob).size();
	}

	@Override
	public ChameleonList<Ability> racialEffects(final MOB mob)
	{
		final StdRace myRace = this;
		final List<Ability> myList=racialEffectsList(mob);
		final List<Ability> finalV=new Vector<Ability>(myList.size());
		for(final Ability A : myList)
		{
			A.makeNonUninvokable();
			A.setSavable(false); // must come AFTER the above
			A.setAffectedOne(mob);
			finalV.add(A);
		}
		final ChameleonList<Ability> finalFinalV;
		if(mob==null)
		{
			finalFinalV = new ChameleonList<Ability>(finalV,
			new ChameleonList.Signaler<Ability>(myList)
			{
				@Override
				public boolean isDeprecated()
				{
					return false;
				}

				@Override
				public void rebuild(final ChameleonList<Ability> me)
				{
				}
			});
		}
		else
		{
			finalFinalV = new ChameleonList(finalV,
			new ChameleonList.Signaler<Ability>(myList)
			{
				@Override
				public boolean isDeprecated()
				{
					if((mob.amDestroyed())
					||(mob.charStats().getMyRace() != myRace)
					|| (racialEffectsList(mob) != oldReferenceListRef.get()))
						return true;
					return false;
				}

				@Override
				public void rebuild(final ChameleonList<Ability> me)
				{
					if(mob.amDestroyed())
						oldReferenceListRef=new WeakReference<List<Ability>>(empty);
					else
					{
						final ChameleonList<Ability> newList = mob.charStats().getMyRace().racialEffects(mob);
						me.changeMeInto(newList);
					}
				}
			});
		}
		return finalFinalV;
	}

	public final List<Ability> racialEffectsList(final MOB mob)
	{
		if(racialEffectNames()==null)
			return empty;

		if((racialEffectMap==null)
		&&(racialEffectNames()!=null)
		&&(racialEffectLevels()!=null)
		&&(racialEffectParms()!=null))
			racialEffectMap=new Hashtable<Integer,SearchIDList<Ability>>();

		if(racialEffectMap==null)
			return empty;

		final Integer level;
		if(mob!=null)
			level=Integer.valueOf(mob.phyStats().level());
		else
			level=Integer.valueOf(Integer.MAX_VALUE);

		if(racialEffectMap.containsKey(level))
			return racialEffectMap.get(level);
		final CMUniqSortSVec<Ability> finalV = new CMUniqSortSVec<Ability>();
		for(int v=0;v<racialEffectLevels().length;v++)
		{
			if((racialEffectLevels()[v]<=level.intValue())
			&&(racialEffectNames().length>v)
			&&(racialEffectParms().length>v))
			{
				final Ability A=CMClass.getAbility(racialEffectNames()[v]);
				if(A!=null)
				{
					// mob was set to null here to make the cache map actually relevant .. see caching below
					A.setProficiency(CMLib.ableMapper().getMaxProficiency((MOB)null,true,A.ID()));
					A.setMiscText(racialEffectParms()[v]);
					A.makeNonUninvokable();
					A.setSavable(false); // must go AFTER the ablve
					finalV.add(A);
				}
			}
		}
		finalV.trimToSize();
		racialEffectMap.put(level, finalV);
		return finalV;
	}

	@Override
	public Race makeGenRace()
	{
		final Race GR=(Race)CMClass.getRace("GenRace").copyOf();
		GR.setRacialParms("<RACE><ID>"+ID()+"</ID><NAME>"+name()+"</NAME></RACE>");
		GR.setStat("CAT",racialCategory());
		GR.setStat("BWEIGHT",""+lightestWeight());
		GR.setStat("VWEIGHT",""+weightVariance());
		GR.setStat("MHEIGHT",""+shortestMale());
		GR.setStat("FHEIGHT",""+shortestFemale());
		GR.setStat("WEAR",""+forbiddenWornBits());
		GR.setStat("AVAIL",""+availabilityCode());
		GR.setStat("VHEIGHT",""+heightVariance());
		GR.setStat("PLAYER",""+CMProps.getIntVar(CMProps.Int.MUDTHEME));
		GR.setStat("LEAVE",leaveStr());
		GR.setStat("ARRIVE",arriveStr());
		GR.setStat("HEALTHRACE",CMClass.classID(this));
		GR.setStat("EVENTRACE",CMClass.classID(this));
		GR.setStat("BODYKILL",""+destroyBodyAfterUse());
		GR.setStat("HELP",""+CMLib.help().getHelpText(name(),null,false));
		GR.setStat("AGING",CMParms.toListString(getAgingChart()));
		GR.setStat("CANRIDE", ""+useRideClass());
		for(int i=0;i<Race.BODYPARTSTR.length;i++)
				GR.bodyMask()[i]=bodyMask()[i];

		Weapon W=myNaturalWeapon();
		final Weapon NW=CMClass.getWeapon("Natural");
		if((W!=null)&&(W!=NW))
		{
			if(!W.isGeneric())
			{
				final Weapon W2=CMClass.getWeapon("GenWeapon");
				W2.setName(W.name());
				W2.setWeaponClassification(W.weaponClassification());
				W2.setWeaponDamageType(W.weaponDamageType());
				W2.basePhyStats().setDamage(W.phyStats().damage());
				W2.basePhyStats().setAttackAdjustment(W.phyStats().attackAdjustment());
				W2.recoverPhyStats();
				W2.text();
				W=W2;
			}
			GR.setStat("WEAPONCLASS",W.ID());
			GR.setStat("WEAPONXML",W.text());
		}
		GR.setStat("WEAPONRACE",getClass().getName());

		final PhyStats RS=(PhyStats)CMClass.getCommon("DefaultPhyStats");
		RS.setAllValues(0);
		final MOB fakeMOB=CMClass.getFactoryMOB();
		affectPhyStats(fakeMOB,RS);
		RS.setRejuv(PhyStats.NO_REJUV);
		GR.setStat("ESTATS",CMLib.coffeeMaker().getPhyStatsStr(RS));

		final CharStats S1=(CharStats)CMClass.getCommon("DefaultCharStats");
		S1.setAllValues(0);
		final CharStats S2=(CharStats)CMClass.getCommon("DefaultCharStats");
		S2.setAllValues(10);
		final CharStats S3=(CharStats)CMClass.getCommon("DefaultCharStats");
		S3.setAllValues(11);
		final CharStats SETSTAT=(CharStats)CMClass.getCommon("DefaultCharStats");
		SETSTAT.setAllValues(0);
		final CharStats ADJSTAT=(CharStats)CMClass.getCommon("DefaultCharStats");
		ADJSTAT.setAllValues(0);
		affectCharStats(fakeMOB,S1);
		affectCharStats(fakeMOB,S2);
		affectCharStats(fakeMOB,S3);
		for(final int i: CharStats.CODES.ALLCODES())
		{
			if(i!=CharStats.STAT_AGE)
			{
				if(CharStats.CODES.isBASE(i))
				{
					final int max = CharStats.CODES.toMAXBASE(i);
					if((S2.getStat(i)==S3.getStat(i))
					&&(S1.getStat(max)!=0))
					{
						SETSTAT.setStat(i,S2.getStat(i));
						S1.setStat(max,0);
						S2.setStat(max,0);
						S3.setStat(max,0);
					}
					else
						ADJSTAT.setStat(i,S1.getStat(i));
				}
				else
					ADJSTAT.setStat(i,S1.getStat(i));
			}
		}
		GR.setStat("ASTATS",CMLib.coffeeMaker().getCharStatsStr(ADJSTAT));
		GR.setStat("CSTATS",CMLib.coffeeMaker().getCharStatsStr(SETSTAT));

		final CharState CS=(CharState)CMClass.getCommon("DefaultCharState"); CS.setAllValues(0);
		affectCharState(fakeMOB,CS);
		GR.setStat("ASTATE",CMLib.coffeeMaker().getCharStateStr(CS));

		//CharState STARTCS=(CharState)CMClass.getCommon("DefaultCharState"); STARTCS.setAllValues(0);
		//startRacing(fakeMOB,falsed);
		//GR.setStat("STARTASTATE",CMLib.coffeeMaker().getCharStateStr(STARTCS));

		GR.setStat("DISFLAGS",""+((classless()?Race.GENFLAG_NOCLASS:0)
								|(leveless()?Race.GENFLAG_NOLEVELS:0)
								|(uncharmable()?Race.GENFLAG_NOCHARM:0)
								|(fertile()?0:Race.GENFLAG_NOFERTILE)
								|(expless()?Race.GENFLAG_NOEXP:0)));

		List<RawMaterial> rscs=myResources();
		if(rscs==null)
			rscs=new Vector<RawMaterial>();
		String txt=null;
		Item I;
		GR.setStat("NUMRSC",""+rscs.size());
		for(int i=0;i<rscs.size();i++)
		{
			I=rscs.get(i);
			I.recoverPhyStats();
			txt=I.text();
			GR.setStat("GETRSCID"+i,I.ID());
			GR.setStat("GETRSCPARM"+i,txt);
		}

		List<Item> outfit=outfit(null);
		if(outfit==null)
			outfit=new Vector<Item>();
		GR.setStat("NUMOFT",""+outfit.size());
		for(int i=0;i<outfit.size();i++)
			GR.setStat("GETOFTID"+i,outfit.get(i).ID());
		for(int i=0;i<outfit.size();i++)
			GR.setStat("GETOFTPARM"+i,outfit.get(i).text());

		GR.setStat("NUMRABLE","");
		if(racialAbilityNames()!=null)
		{
			GR.setStat("NUMRABLE",""+racialAbilityNames().length);
			for(int i=0;i<racialAbilityNames().length;i++)
			{
				GR.setStat("GETRABLE"+i,racialAbilityNames()[i]);
				GR.setStat("GETRABLELVL"+i,""+racialAbilityLevels()[i]);
				GR.setStat("GETRABLEQUAL"+i,""+racialAbilityQuals()[i]);
				GR.setStat("GETRABLEPROF"+i,""+racialAbilityProficiencies()[i]);
			}
		}

		GR.setStat("NUMIABLE","");
		{
			int i=0;
			for(final String ableID : this.naturalAbilImmunities)
				GR.setStat("GETIABLE"+(i++),ableID);
		}

		GR.setStat("NUMCABLE","");
		if(culturalAbilityNames()!=null)
		{
			GR.setStat("NUMCABLE",""+culturalAbilityNames().length);
			for(int i=0;i<culturalAbilityNames().length;i++)
			{
				GR.setStat("GETCABLE"+i,culturalAbilityNames()[i]);
				GR.setStat("GETCABLEPROF"+i,""+culturalAbilityProficiencies()[i]);
			}
		}

		GR.setStat("NUMREFF","");
		if(racialEffectNames()!=null)
		{
			GR.setStat("NUMREFF",""+racialEffectNames().length);
			for(int i=0;i<racialEffectNames().length;i++)
			{
				GR.setStat("GETREFF"+i,racialEffectNames()[i]);
				GR.setStat("GETREFFLVL"+i,""+racialEffectLevels()[i]);
				GR.setStat("GETREFFPARM"+i,racialEffectParms()[i]);
			}
		}
		fakeMOB.destroy();
		return GR;
	}

	@Override
	public Race mixRace(Race race, String newRaceID, String newRaceName)
	{
		final Race GR=(Race)CMClass.getRace("GenRace").copyOf();
		Race race1=this;
		Race race2=race;
		GR.setRacialParms("<RACE><ID>"+newRaceID+"</ID><NAME>"+newRaceName+"</NAME></RACE>");
		if(!race1.isGeneric())
			race1=race1.makeGenRace();
		if(!race2.isGeneric())
			race2=race2.makeGenRace();

		final Race nonHuman=(race1.ID().equals("Human"))?race2:race1;
		final Race otherRace=(nonHuman==race1)?race2:race1;
		GR.setStat("CAT",nonHuman.racialCategory());
		GR.setStat("BWEIGHT",""+((race1.lightestWeight()+race2.lightestWeight())/2));
		GR.setStat("VWEIGHT",""+((race1.weightVariance()+race2.weightVariance())/2));
		GR.setStat("MHEIGHT",""+((race1.shortestMale()+race2.shortestMale())/2));
		GR.setStat("FHEIGHT",""+((race1.shortestFemale()+race2.shortestFemale())/2));
		GR.setStat("VHEIGHT",""+((race1.heightVariance()+race2.heightVariance())/2));
		GR.setStat("PLAYER",""+CMProps.getIntVar(CMProps.Int.MUDTHEME));
		GR.setStat("LEAVE",nonHuman.leaveStr());
		GR.setStat("ARRIVE",nonHuman.arriveStr());
		GR.setStat("HEALTHRACE",otherRace.getStat("HEALTHRACE"));
		GR.setStat("EVENTRACE",otherRace.getStat("EVENTRACE"));
		GR.setStat("WEAPONRACE",otherRace.getStat("WEAPONRACE"));
		final int[] aging=race1.getAgingChart().clone();
		for(int i=0;i<aging.length;i++)
		{
			if((aging[i]==Race.YEARS_AGE_LIVES_FOREVER)&&(race2.getAgingChart()[i]==Race.YEARS_AGE_LIVES_FOREVER))
			{
			}
			else
			if(((aging[i]==Race.YEARS_AGE_LIVES_FOREVER)&&(race2.getAgingChart()[i]==0))
			||((aging[i]==0)&&(race2.getAgingChart()[i]==Race.YEARS_AGE_LIVES_FOREVER)))
				aging[i]=0;
			else
			if((aging[i]==Race.YEARS_AGE_LIVES_FOREVER))
				aging[i]=race2.getAgingChart()[i]*2;
			else
			if((race2.getAgingChart()[i]==Race.YEARS_AGE_LIVES_FOREVER))
				aging[i]=aging[i]*2;
			else
			{
				aging[i]+=race2.getAgingChart()[i];
				aging[i]=aging[i]/2;
			}
		}
		for(int i=aging.length-2;i>=0;i--)
		{
			if(aging[i]>aging[i+1])
				aging[i]=aging[i+1];
		}

		final long race1worn=CMath.s_long(otherRace.getStat("WEAR"));
		final long race2worn=CMath.s_long(nonHuman.getStat("WEAR"));
		long finalWear=0;
		boolean toggle=false;
		for(final long wornCode : Wearable.CODES.ALL())
		{
			if(wornCode != Wearable.IN_INVENTORY)
			{
				if((!CMath.bset(race1worn,wornCode))&&(!CMath.bset(race2worn,wornCode)))
				{
				}
				else
				if(CMath.bset(race1worn,wornCode)&&CMath.bset(race2worn,wornCode))
					finalWear=finalWear|wornCode;
				else
				if(CMath.bset(race1worn,wornCode))
					finalWear=finalWear|wornCode;
				else
				if(toggle)
				{
					finalWear=finalWear|wornCode;
					toggle=!toggle;
				}
			}
		}

		GR.setStat("WEAR",""+finalWear);
		Weapon W=otherRace.myNaturalWeapon();
		if(W==null)
			W=nonHuman.myNaturalWeapon();
		if(W!=null)
		{
			GR.setStat("WEAPONCLASS",W.ID());
			GR.setStat("WEAPONXML",W.text());
		}

		GR.setStat("BODYKILL",""+otherRace.getStat("BODYKILL"));
		GR.setStat("CANRIDE",""+otherRace.getStat("CANRIDE"));
		GR.setStat("AGING",CMParms.toListString(aging));
		for(int i=0;i<Race.BODYPARTSTR.length;i++)
		{
			if((race1.bodyMask()[i]>0)&&(race2.bodyMask()[i]>0))
				GR.bodyMask()[i]=((race1.bodyMask()[i]+race2.bodyMask()[i])/2);
			else
			if((race1.bodyMask()[i]<=0)&&(race2.bodyMask()[i]>=0))
				GR.bodyMask()[i]=race2.bodyMask()[i];
			else
				GR.bodyMask()[i]=race1.bodyMask()[i];
		}

		final PhyStats RS1=(PhyStats)CMClass.getCommon("DefaultPhyStats");
		RS1.setAllValues(0);
		CMLib.coffeeMaker().setPhyStats(RS1,race1.getStat("ESTATS"));

		final PhyStats RS2=(PhyStats)CMClass.getCommon("DefaultPhyStats");
		RS2.setAllValues(0);
		CMLib.coffeeMaker().setPhyStats(RS2,race2.getStat("ESTATS"));

		final PhyStats RS=(PhyStats)CMClass.getCommon("DefaultPhyStats");
		RS.setAbility((RS1.ability()+RS2.ability())/2);
		RS.setArmor((RS2.armor()+RS2.armor())/2);
		RS.setAttackAdjustment((RS1.attackAdjustment()+RS2.attackAdjustment())/2);
		RS.setSensesMask(RS1.sensesMask()|RS2.sensesMask());
		RS.setDisposition(RS1.disposition());
		RS.setDamage((RS1.damage()+RS2.damage())/2);
		RS.setHeight((RS1.height()+RS2.height())/2);
		RS.setSpeed((RS1.speed()+RS2.speed())/2.0);
		RS.setWeight((RS1.weight()+RS2.weight())/2);
		RS.setRejuv(PhyStats.NO_REJUV);
		GR.setStat("ESTATS",CMLib.coffeeMaker().getPhyStatsStr(RS));

		final CharStats SETSTAT1=(CharStats)CMClass.getCommon("DefaultCharStats");
		SETSTAT1.setAllValues(0);
		CMLib.coffeeMaker().setCharStats(SETSTAT1,race1.getStat("CSTATS"));

		final CharStats SETSTAT2=(CharStats)CMClass.getCommon("DefaultCharStats");
		SETSTAT2.setAllValues(0);
		CMLib.coffeeMaker().setCharStats(SETSTAT2,race2.getStat("CSTATS"));

		final CharStats SETSTAT=(CharStats)CMClass.getCommon("DefaultCharStats");
		SETSTAT.setAllValues(0);

		final CharStats ADJSTAT1=(CharStats)CMClass.getCommon("DefaultCharStats");
		ADJSTAT1.setAllValues(0);
		CMLib.coffeeMaker().setCharStats(ADJSTAT1,race1.getStat("ASTATS"));

		final CharStats ADJSTAT2=(CharStats)CMClass.getCommon("DefaultCharStats");
		ADJSTAT2.setAllValues(0);
		CMLib.coffeeMaker().setCharStats(ADJSTAT2,race2.getStat("ASTATS"));

		final CharStats ADJSTAT=(CharStats)CMClass.getCommon("DefaultCharStats");
		ADJSTAT.setAllValues(0);

		for(final int i: CharStats.CODES.ALLCODES())
		{
			if(CharStats.CODES.isBASE(i))
			{
				SETSTAT.setStat(i,(SETSTAT1.getStat(i)+SETSTAT2.getStat(i))/2);
				final int newStat=((ADJSTAT1.getStat(i)+ADJSTAT2.getStat(i))/2);
				if(newStat>5)
					ADJSTAT.setStat(i,5);
				else
					ADJSTAT.setStat(i,newStat);
			}
			else
			if((i!=CharStats.STAT_GENDER)&&(i!=CharStats.STAT_AGE))
				ADJSTAT.setStat(i,(ADJSTAT1.getStat(i)+ADJSTAT2.getStat(i))/2);
		}
		GR.setStat("ASTATS",CMLib.coffeeMaker().getCharStatsStr(ADJSTAT));
		GR.setStat("CSTATS",CMLib.coffeeMaker().getCharStatsStr(SETSTAT));

		final CharState CS1=(CharState)CMClass.getCommon("DefaultCharState");
		CS1.setAllValues(0);
		CMLib.coffeeMaker().setCharState(CS1,race1.getStat("ASTATE"));
		final CharState CS2=(CharState)CMClass.getCommon("DefaultCharState");
		CS2.setAllValues(0);
		CMLib.coffeeMaker().setCharState(CS2,race2.getStat("ASTATE"));
		final CharState CS=(CharState)CMClass.getCommon("DefaultCharState");
		CS.setAllValues(0);

		CS.setFatigue((CS1.getFatigue()+CS2.getFatigue())/2);
		CS.setHitPoints((CS1.getHitPoints()+CS2.getHitPoints())/2);
		CS.setHunger((CS1.getHunger()+CS2.getHunger())/2);
		CS.setMana((CS1.getMana()+CS2.getMana())/2);
		CS.setMovement((CS1.getMovement()+CS2.getMovement())/2);
		CS.setThirst((CS1.getThirst()+CS2.getThirst())/2);
		GR.setStat("ASTATE",CMLib.coffeeMaker().getCharStateStr(CS));

		final CharState STARTCS1=(CharState)CMClass.getCommon("DefaultCharState");
		STARTCS1.setAllValues(0);
		CMLib.coffeeMaker().setCharState(STARTCS1,race1.getStat("STARTASTATE"));

		final CharState STARTCS2=(CharState)CMClass.getCommon("DefaultCharState");
		STARTCS2.setAllValues(0);
		CMLib.coffeeMaker().setCharState(STARTCS1,race2.getStat("STARTASTATE"));

		final CharState STARTCS=(CharState)CMClass.getCommon("DefaultCharState");
		STARTCS.setAllValues(0);

		STARTCS.setFatigue((STARTCS1.getFatigue()+STARTCS2.getFatigue())/2);
		STARTCS.setHitPoints((STARTCS1.getHitPoints()+STARTCS2.getHitPoints())/2);
		STARTCS.setHunger((STARTCS1.getHunger()+STARTCS2.getHunger())/2);
		STARTCS.setMana((STARTCS1.getMana()+STARTCS2.getMana())/2);
		STARTCS.setMovement((STARTCS1.getMovement()+STARTCS2.getMovement())/2);
		STARTCS.setThirst((STARTCS1.getThirst()+STARTCS2.getThirst())/2);
		GR.setStat("STARTASTATE",CMLib.coffeeMaker().getCharStateStr(STARTCS));

		GR.setStat("DISFLAGS",""+(CMath.s_int(race1.getStat("DISFLAGS"))|CMath.s_int(race2.getStat("DISFLAGS"))));

		final List<RawMaterial> rscs=nonHuman.myResources();
		GR.setStat("NUMRSC",""+rscs.size());
		for(int i=0;i<rscs.size();i++)
			GR.setStat("GETRSCID"+i,((Item)rscs.get(i)).ID());
		for(int i=0;i<rscs.size();i++)
			GR.setStat("GETRSCPARM"+i,((Item)rscs.get(i)).text());

		GR.setStat("NUMOFT","");
		final Race outfitRace=(nonHuman.outfit(null)!=null)?nonHuman:otherRace;
		final List<Item> outfit=outfitRace.outfit(null);
		if((outfit!=null)&&(outfit.size()>0))
		{
			GR.setStat("NUMOFT",""+outfit.size());
			for(int i=0;i<outfit.size();i++)
				GR.setStat("GETOFTID"+i,outfit.get(i).ID());
			for(int i=0;i<outfit.size();i++)
				GR.setStat("GETOFTPARM"+i,outfit.get(i).text());
		}

		race1.racialAbilities(null);
		race2.racialAbilities(null);
		final List<AbilityMapping> dvata1=CMLib.ableMapper().getUpToLevelListings(race1.ID(),Integer.MAX_VALUE,true,false);
		final List<AbilityMapping> dvata2=CMLib.ableMapper().getUpToLevelListings(race2.ID(),Integer.MAX_VALUE,true,false);
		// kill half of them.
		for(int i=1;i<dvata1.size();i++)
			dvata1.remove(i);
		for(int i=1;i<dvata2.size();i++)
			dvata2.remove(i);

		if((dvata1.size()+dvata2.size())>0)
			GR.setStat("NUMRABLE",""+(dvata1.size()+dvata2.size()));
		else
			GR.setStat("NUMRABLE","");
		for(int i=0;i<dvata1.size();i++)
		{
			GR.setStat("GETRABLE"+i,dvata1.get(i).abilityID());
			GR.setStat("GETRABLELVL"+i,""+CMLib.ableMapper().getQualifyingLevel(race1.ID(),false,dvata1.get(i).abilityID()));
			GR.setStat("GETRABLEQUAL"+i,""+(!CMLib.ableMapper().getDefaultGain(race1.ID(),false,dvata1.get(i).abilityID())));
			GR.setStat("GETRABLEPROF"+i,""+CMLib.ableMapper().getDefaultProficiency(race1.ID(),false,dvata1.get(i).abilityID()));
		}
		for(int i=0;i<dvata2.size();i++)
		{
			GR.setStat("GETRABLE"+(i+dvata1.size()),dvata2.get(i).abilityID());
			GR.setStat("GETRABLELVL"+(i+dvata1.size()),""+CMLib.ableMapper().getQualifyingLevel(race2.ID(),false,dvata2.get(i).abilityID()));
			GR.setStat("GETRABLEQUAL"+(i+dvata1.size()),""+(!CMLib.ableMapper().getDefaultGain(race2.ID(),false,dvata2.get(i).abilityID())));
			GR.setStat("GETRABLEPROF"+(i+dvata1.size()),""+CMLib.ableMapper().getDefaultProficiency(race2.ID(),false,dvata2.get(i).abilityID()));
		}

		final List<Ability> data=new Vector<Ability>();
		boolean skip=false;
		for(final Ability A : race1.racialEffects(null))
		{
			if(!skip)
				data.add(A);
			skip=!skip;
		}
		for(final Ability A : race2.racialEffects(null))
		{
			if(!skip)
				data.add(A);
			skip=!skip;
		}

		if(data.size()>0)
			GR.setStat("NUMREFF",""+data.size());
		else
			GR.setStat("NUMREFF","");
		for(int i=0;i<data.size();i++)
		{
			final Ability A=data.get(i);
			GR.setStat("GETREFF"+i,A.ID());
			GR.setStat("GETREFFLVL"+i,""+CMLib.ableMapper().getQualifyingLevel(race1.ID(),false,A.ID()));
			GR.setStat("GETREFFPARM"+i,""+CMLib.ableMapper().getDefaultProficiency(race1.ID(),false,A.ID()));
		}
		
		final List<String> imms=new XVector<String>();
		skip=false;
		for(final String A : race1.abilityImmunities())
		{
			if(!skip)
				imms.add(A);
			skip=!skip;
		}
		for(final String A : race2.abilityImmunities())
		{
			if(!skip)
				imms.add(A);
			skip=!skip;
		}
		GR.setStat("NUMIABLE",""+data.size());
		for(int i=0;i<imms.size();i++)
		{
			final String AID=imms.get(i);
			GR.setStat("GETIABLE"+i,AID);
		}
		
		return GR;
	}

	@Override
	public PairVector<String,Integer> culturalAbilities()
	{
		final PairVector<String,Integer> ables=new PairVector<String,Integer>();
		if((culturalAbilityNames()!=null)
		&&(culturalAbilityProficiencies()!=null))
		{
			for(int i=0;i<culturalAbilityNames().length;i++)
				ables.addElement(culturalAbilityNames()[i],Integer.valueOf(culturalAbilityProficiencies()[i]));
		}
		return ables;
	}

	@Override
	public SearchIDList<Ability> racialAbilities(MOB mob)
	{
		if((racialAbilityMap==null)
		&&(racialAbilityNames()!=null)
		&&(racialAbilityLevels()!=null)
		&&(racialAbilityProficiencies()!=null)
		&&(racialAbilityQuals()!=null))
		{
			CMLib.ableMapper().delCharMappings(ID()); // necessary for a "clean start"
			racialAbilityMap=new Hashtable<Integer,SearchIDList<Ability>>();
			for(int i=0;i<racialAbilityNames().length;i++)
			{
				CMLib.ableMapper().addDynaAbilityMapping(
						 ID(),
						 racialAbilityLevels()[i],
						 racialAbilityNames()[i],
						 racialAbilityProficiencies()[i],
						 "",
						 !racialAbilityQuals()[i],
						 false,
						 "");
			}
		}
		if(racialAbilityMap==null)
			return emptyIDs;
		Integer level=null;
		if(mob!=null)
		{
			level=Integer.valueOf(mob.phyStats().level());
			if(level.intValue() == 0) // for mudstarting situations
				level = Integer.valueOf(mob.basePhyStats().level());
		}
		else
			level=Integer.valueOf(Integer.MAX_VALUE);
		if(racialAbilityMap.containsKey(level))
			return racialAbilityMap.get(level);
		if(!CMProps.getBoolVar(CMProps.Bool.MUDSTARTED))
			return emptyIDs;
		final List<AbilityMapper.AbilityMapping> V=CMLib.ableMapper().getUpToLevelListings(ID(),level.intValue(),true,(mob!=null));
		final CMUniqSortSVec<Ability> finalV=new CMUniqSortSVec<Ability>(V.size());
		for(final AbilityMapper.AbilityMapping able : V)
		{
			final Ability A=CMClass.getAbility(able.abilityID());
			if(A!=null)
			{
				A.setProficiency(CMLib.ableMapper().getDefaultProficiency(ID(),false,A.ID()));
				A.setSavable(false);
				A.setMiscText(CMLib.ableMapper().getDefaultParm(ID(),false,A.ID()));
				finalV.add(A);
			}
		}
		finalV.trimToSize();
		finalV.setReadOnly(true);
		racialAbilityMap.put(level,finalV);
		return finalV;
	}

	@Override
	public String getStatAdjDesc()
	{
		makeStatChgDesc();
		return baseStatChgDesc;
	}

	@Override
	public String getSensesChgDesc()
	{
		makeStatChgDesc();
		return sensesChgDesc;
	}

	@Override
	public String getDispositionChgDesc()
	{
		makeStatChgDesc();
		return dispChgDesc;
	}

	@Override
	public String getTrainAdjDesc()
	{
		if(trainsAtFirstLevel()>0)
			return "trains+"+trainsAtFirstLevel();
		if(trainsAtFirstLevel()<0)
			return "trains"+trainsAtFirstLevel();
		return "";
	}

	@Override
	public String getPracAdjDesc()
	{
		if(practicesAtFirstLevel()>0)
			return "practices+"+practicesAtFirstLevel();
		if(practicesAtFirstLevel()<0)
			return "practices"+practicesAtFirstLevel();
		return "";
	}

	@Override
	public String getAbilitiesDesc()
	{
		makeStatChgDesc();
		return abilitiesDesc;
	}

	@Override
	public String getLanguagesDesc()
	{
		makeStatChgDesc();
		return languagesDesc;
	}

	@Override
	public String racialParms()
	{
		return "";
	}

	@Override
	public void setRacialParms(String parms)
	{
	}

	protected void clrStatChgDesc()
	{
		baseStatChgDesc=null;
		dispChgDesc=null;
		sensesChgDesc=null;
		abilitiesDesc = null;
		languagesDesc = null;
	}

	protected void makeStatChgDesc()
	{
		if((baseStatChgDesc == null)
		||(dispChgDesc==null)
		||(sensesChgDesc==null))
		{
			StringBuilder str=new StringBuilder("");
			final Session sess = (Session)CMClass.getCommon("DefaultSession");
			final MOB mob=CMClass.getMOB("StdMOB");
			mob.setSession(sess);
			mob.baseCharStats().setMyRace(this);
			startRacing(mob,false);
			mob.recoverCharStats();
			mob.recoverPhyStats();
			mob.recoverMaxState();
			final MOB mob2=CMClass.getMOB("StdMOB");
			mob2.setSession(sess);
			mob2.baseCharStats().setMyRace(new StdRace());
			mob2.recoverCharStats();
			mob2.recoverPhyStats();
			mob2.recoverMaxState();
			for(final int c: CharStats.CODES.ALLCODES())
			{
				final int oldStat=mob2.charStats().getStat(c);
				final int newStat=mob.charStats().getStat(c);
				if(oldStat>newStat)
					str.append(CharStats.CODES.DESC(c).toLowerCase()+"-"+(oldStat-newStat)+", ");
				else
				if(newStat>oldStat)
					str.append(CharStats.CODES.DESC(c).toLowerCase()+"+"+(newStat-oldStat)+", ");
			}
			dispChgDesc=CMLib.flags().getDispositionStateList(mob);
			sensesChgDesc=CMLib.flags().getSensesStateList(mob);
			mob.destroy();
			mob2.destroy();
			baseStatChgDesc=str.toString();
			if(baseStatChgDesc.endsWith(", "))
				baseStatChgDesc=baseStatChgDesc.substring(0,baseStatChgDesc.length()-2);
			final StringBuilder astr=new StringBuilder("");
			final StringBuilder lstr=new StringBuilder("");

			final List<Ability> ables=new Vector<Ability>();
			ables.addAll(racialAbilities(null));

			final PairVector<String,Integer> cables=culturalAbilities();
			Ability A=null;
			if(cables!=null)
			{
				for(int c=0;c<cables.size();c++)
				{
					A=CMClass.getAbility(cables.getFirst(c));
					if(A!=null)
					{
						A.setProficiency(cables.getSecond(c).intValue());
						ables.add(A);
					}
				}
			}
			for(final Iterator<Ability> e=ables.iterator();e.hasNext();)
			{
				A=e.next();
				str = ((A.classificationCode()&Ability.ALL_ACODES)==Ability.ACODE_LANGUAGE)?lstr:astr;
				if(A.proficiency()<=0)
					str.append(A.name()+", ");
				else
					str.append(A.name()+"("+A.proficiency()+"%), ");
			}
			abilitiesDesc=astr.toString();
			if(abilitiesDesc.endsWith(", "))
				abilitiesDesc=abilitiesDesc.substring(0,abilitiesDesc.length()-2);
			languagesDesc=lstr.toString();
			if(languagesDesc.endsWith(", "))
				languagesDesc=languagesDesc.substring(0,languagesDesc.length()-2);
		}
	}


	protected static String[] CODES={"CLASS","PARMS"};

	@Override
	public int getSaveStatIndex()
	{
		return getStatCodes().length;
	}

	@Override
	public String getStat(String code)
	{
		switch(getCodeNum(code))
		{
		case 0:
			return ID();
		case 1:
			return "" + racialParms();
		}
		return "";
	}

	@Override
	public void setStat(String code, String val)
	{
		switch(getCodeNum(code))
		{
		case 0:
			return;
		case 1:
			setRacialParms(val);
			break;
		}
	}
	
	@Override
	public String[] getStatCodes()
	{
		return CODES;
	}

	@Override
	public boolean isStat(String code)
	{
		return CMParms.indexOf(getStatCodes(), code.toUpperCase().trim()) >= 0;
	}

	protected int getCodeNum(String code)
	{
		for(int i=0;i<CODES.length;i++)
		{
			if(code.equalsIgnoreCase(CODES[i]))
				return i;
		}
		return -1;
	}

	public boolean sameAs(Race E)
	{
		if(!(E instanceof StdRace))
			return false;
		for(int i=0;i<CODES.length;i++)
		{
			if(!E.getStat(CODES[i]).equals(getStat(CODES[i])))
				return false;
		}
		return true;
	}
}
