public class NemesisKnowledge {

	@NbtField
	public UUID nemesisId;

	/**
	 * true if the name of the nemesis is known
	 */
	@NbtField
	public boolean name;

	/**
	 * true if the location of the nemesis is known
	 */
	@NbtField
	public boolean location;

	/**
	 * true if the items of the nemesis are known
	 */
	@NbtField
	public boolean items;

	/**
	 * each integer in this set is the index of
	 * a nemesis's trait that has been discovered
	 */
	@NbtField(genericType = Integer.class)
	public Set<Integer> traits = new HashSet<>();

	public static NemesisKnowledge load(NBTTagCompound c) {
		NemesisKnowledge knowledge = new NemesisKnowledge();
		NbtSerializer.read(c, knowledge);
		return knowledge;
	}

	public static NBTTagCompound save(NemesisKnowledge knowledge) {
		NBTTagCompound c = new NBTTagCompound();
		NbtSerializer.write(c, knowledge);
		return c;
	}

} public class SpawnHandler {

	private static final int BUFF_CHANCE = 4;
	private static final int SPAWN_CHANCE = 2;
	private static final int MAX_SPAWN_DISTANCE = 100;
	public static final int SPAWN_COOLDOWN_PERIOD = 16000;

	public static void init() {
		MinecraftForge.EVENT_BUS.register(new SpawnHandler());
	}

	@SubscribeEvent
	public void handleSpawn(EntityJoinWorldEvent event) {

		if (event.getWorld().isRemote) {
			requestNemesisDataFromServer(event);
			return;
		}

		if (!(event.getEntity() instanceof EntityCreature) || !NemesisUtil.isNemesisClassEntity(event.getEntity())) {
			return;
		}

		EntityCreature entity = (EntityCreature) event.getEntity();

		NemesisActions.handleRandomPromotions(event.getWorld(), entity);

		if (entity.getTags().contains(NemesisSystem.TAG_NEMESIS)) {
			handleRespawnOfNemesis(event);
			return;
		}

		if (entity.getTags().contains(NemesisSystem.TAG_BODY_GUARD)) {
			BehaviorApi.setFollowSpeed(entity, 2);
			return;
		}

		NemesisEntry nemesis = getNemesisForSpawn(event);

		if (nemesis != null) {
			replaceEntityWithNemesis(entity, nemesis);
			MinecraftForge.EVENT_BUS.post(new SpawnEvent(nemesis, entity));
			return;
		}

		buffMobInAroundNemeses(event.getWorld(), entity);
	}

	private void buffMobInAroundNemeses(World world, EntityCreature entity) {
		if (!(entity instanceof EntityMob)) {
			return;
		}

		if (entity.getTags().contains(NemesisSystem.TAG_BUFF_MOB_REINFORCEMENT)) {
			return;
		}

		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(world).list();
		sortByHighestLevel(nemeses);

		for (NemesisEntry nemesis : nemeses) {
			if (buffEntity(entity, nemesis)) {
				return;
			}
		}
	}

	private boolean buffEntity(EntityCreature entity, NemesisEntry nemesis) {

		if (entity.getRNG().nextInt(BUFF_CHANCE) != 0) {
			return false;
		}

		int buffAmount = determineBuffAmount(nemesis, entity.getPosition());

		if (buffAmount < 1) {
			return false;
		}

		updateSharedMonsterAttributes(entity, buffAmount);
		equipMob(entity, buffAmount);
		entity.addTag(NemesisSystem.TAG_BUFFED_MOB);

		spawnReinforcements(entity, buffAmount);

		return true;
	}

	private void equipMob(EntityCreature entity, int buffAmount) {
		int weaponRoll = entity.getRNG().nextInt(buffAmount);
		if (weaponRoll >= 5) {
			entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.DIAMOND_SWORD));
			entity.setItemStackToSlot(EntityEquipmentSlot.OFFHAND, new ItemStack(Items.DIAMOND_SWORD));
		} else if (weaponRoll >= 4) {
			entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.DIAMOND_SWORD));
		} else if (weaponRoll >= 3) {
			entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD));
		} else if (weaponRoll >= 2) {
			entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.STONE_SWORD));
		} else {
			entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.WOODEN_SWORD));
		}

		int chestPlateRoll = entity.getRNG().nextInt(buffAmount);
		if (chestPlateRoll >= 5) {
			entity.setItemStackToSlot(EntityEquipmentSlot.CHEST, new ItemStack(Items.DIAMOND_CHESTPLATE));
		} else if (chestPlateRoll >= 4) {
			entity.setItemStackToSlot(EntityEquipmentSlot.CHEST, new ItemStack(Items.IRON_CHESTPLATE));
		} else if (chestPlateRoll >= 3) {
			entity.setItemStackToSlot(EntityEquipmentSlot.CHEST, new ItemStack(Items.GOLDEN_CHESTPLATE));
		}

	}

	private void updateSharedMonsterAttributes(EntityCreature entity, int buffAmount) {
		for (IAttributeInstance attribute : entity.getAttributeMap().getAllAttributes()) {
			if (attribute.getAttribute() == SharedMonsterAttributes.ATTACK_DAMAGE) {
				attribute.setBaseValue(determineAttackDamage(attribute.getAttributeValue(), buffAmount));
			}

			if (attribute.getAttribute() == SharedMonsterAttributes.MAX_HEALTH) {
				attribute.setBaseValue(determineMaxHealth(attribute.getAttributeValue(), buffAmount));
				entity.setHealth(entity.getMaxHealth());
			}
		}
	}

	protected static double determineAttackDamage(double initial, int buffAmount) {
		return initial * (1 + (double) buffAmount / 6);
	}

	protected static double determineMaxHealth(double initial, int buffAmount) {
		return initial * (1 + (double) buffAmount / 4);
	}

	protected static void sortByHighestLevel(List<NemesisEntry> nemeses) {
		nemeses.sort((a, b) -> b.getLevel() - a.getLevel());
	}

	protected static int determineBuffAmount(NemesisEntry nemesis, BlockPos pos) {
		if (nemesis.isDead()) {
			return 0;
		}

		double distance = getDistance(pos, nemesis.getX(), nemesis.getZ()) + 0.0001;
		double range = nemesis.getRange() * (1 + (nemesis.getLevel() / 2));

		if (distance > range) {
			return 0;
		}
		return (int) (range - distance + 1) / nemesis.getRange();
	}

	private void spawnReinforcements(EntityCreature entity, int buffAmount) {
		int roll = entity.getRNG().nextInt(buffAmount);
		for (int i = 0; i < roll; i++) {
			EntityCreature reinforcement = SpawnApi.getEntityFromString(entity.world, SpawnApi.getEntityString(entity));
			reinforcement.addTag(NemesisSystem.TAG_BUFF_MOB_REINFORCEMENT);
			SpawnApi.spawnEntityCreature(entity.world, reinforcement, entity.getPosition(), 10);
		}
	}

	protected static double getDistance(BlockPos pos, int x, int z) {
		double d0 = pos.getX() - x;
		double d2 = pos.getZ() - z;
		return (double) MathHelper.sqrt(d0 * d0 + d2 * d2);
	}

	private void requestNemesisDataFromServer(EntityJoinWorldEvent event) {
		if (event.getEntity() instanceof EntityCreature) {
			if (!event.getEntity().getTags().contains(NemesisSystem.TAG_NEMESIS)) {
				NemesisSystem.NETWORK.sendToServer(new MessageSyncNemesisRequest(event.getEntity().getPersistentID()));
			}
		}
	}

	private void replaceEntityWithNemesis(EntityCreature entity, NemesisEntry nemesis) {
		entity.setDead();
		spawnNemesis(entity.world, entity.getPosition(), nemesis);
	}

	private void handleRespawnOfNemesis(EntityJoinWorldEvent event) {
		Entity entity = event.getEntity();
		NemesisEntry nemesis = NemesisUtil.loadNemesisFromEntity(event.getEntity());
		if (nemesis == null) {
			/*
			 * missing nemesis data
			 */
			entity.setDead();
			event.setCanceled(true);
		} else if (entity.getTags().contains(NemesisSystem.TAG_SPAWNING)) {
			/*
			 * new nemesis spawn in progress
			 */
			entity.removeTag(NemesisSystem.TAG_SPAWNING);
			nemesis.setSpawned(entity.getEntityId());
			nemesis.setUnloaded(null);
			NemesisRegistryProvider.get(entity.world).update(nemesis);
			sendNemesisDataToClient(nemesis);
		} else if (!nemesis.isSpawned()) {
			/*
			 * nemesis has been marked as despawned
			 */
			event.setCanceled(true);
		} else {
			/*
			 * nemesis is marked unloaded, mark as loaded now he is respawning
			 */
			if (!entity.getPersistentID().equals(nemesis.getEntityUuid())) {
				event.setCanceled(true);
				return;
			}

			nemesis.setSpawned(entity.getEntityId());
			nemesis.setUnloaded(null);
			nemesis.setLastSpawned(entity.world.getTotalWorldTime());
			NemesisRegistryProvider.get(event.getWorld()).update(nemesis);
			sendNemesisDataToClient(nemesis);
		}
	}

	public static void spawnNemesis(World world, BlockPos pos, NemesisEntry nemesis) {
		if (nemesis.isDead()) {
			return;
		}
		if (nemesis.isSpawned()) {
			return;
		}

		String mobType = overrideMobType(nemesis.getMob());

		EntityCreature nemesisEntity = SpawnApi.getEntityFromString(world, mobType);

		if (nemesisEntity == null) {
			return;
		}

		nemesisEntity.setAttackTarget(nemesis.getTargetPlayer());
		nemesisEntity.addTag(NemesisSystem.TAG_SPAWNING);
		if (nemesisEntity instanceof INemesisEntity) {
			((INemesisEntity) nemesisEntity).setNemesis(nemesis);
		}

		EntityDecorator.decorate(nemesisEntity, nemesis);
		SpawnApi.spawnEntityCreature(world, nemesisEntity, pos, 1);

		spawnBodyGuard(nemesisEntity, nemesis);
		nemesisAnnounceEffects(nemesisEntity);

		nemesis.setSpawned(nemesisEntity.getEntityId());
		nemesis.setLastSpawned(world.getTotalWorldTime());
		nemesis.setEntityUuid(nemesisEntity.getPersistentID());
		nemesis.setUnloaded(null);
		NemesisRegistryProvider.get(world).update(nemesis);

		sendNemesisDataToClient(nemesis);
	}

	private static String overrideMobType(String mob) {
		switch (mob) {
		case "minecraft:zombie":
			return NemesisSystem.MODID + ":" + EntityZombieNemesis.NAME;
		case "minecraft:zombie_pigman":
			return NemesisSystem.MODID + ":" + EntityPigZombieNemesis.NAME;
		case "minecraft:zombie_villager":
			return NemesisSystem.MODID + ":" + EntityZombieVillagerNemesis.NAME;
		case "minecraft:husk":
			return NemesisSystem.MODID + ":" + EntityHuskNemesis.NAME;
		case "minecraft:skeleton":
			return NemesisSystem.MODID + ":" + EntitySkeletonNemesis.NAME;
		case "minecraft:stray":
			return NemesisSystem.MODID + ":" + EntityStrayNemesis.NAME;
		}
		return mob;
	}

	private static void sendNemesisDataToClient(NemesisEntry nemesis) {
		NemesisSystem.NETWORK.sendToAll(new MessageSyncNemesis(nemesis));
	}

	private static void nemesisAnnounceEffects(EntityCreature nemesisEntity) {
		World world = nemesisEntity.world;

		if (canSeeSky(nemesisEntity)) {
			world.addWeatherEffect(new EntityLightningBolt(nemesisEntity.world, nemesisEntity.posX, nemesisEntity.posY, nemesisEntity.posZ, true));
		}

		// TODO sound horn
	}

	private static boolean canSeeSky(Entity e) {
		return e.world.canSeeSky(new BlockPos(e.posX, e.posY + (double) e.getEyeHeight(), e.posZ));
	}

	private static void spawnBodyGuard(EntityLiving entity, NemesisEntry nemesis) {

		// TODO use nemesis colors

		int count = 3 + nemesis.getLevel() * 3;

		for (int i = 0; i < count; i++) {
			EntityCreature bodyGuard = new EntityZombie(entity.getEntityWorld());
			bodyGuard.addTag(NemesisSystem.TAG_BODY_GUARD);
			bodyGuard.getEntityData().setUniqueId(NemesisSystem.NBT_NEMESIS_ID, nemesis.getId());
			equipBodyGuard(bodyGuard);
			SpawnApi.spawnEntityCreature(entity.getEntityWorld(), bodyGuard, entity.getPosition(), 10);
			BehaviorApi.setFollowSpeed(bodyGuard, 1.5);
		}
	}

	private static void equipBodyGuard(EntityCreature bodyGuard) {
		int color = 0xffffff;
		bodyGuard.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(Items.STONE_SWORD));
		bodyGuard.setItemStackToSlot(EntityEquipmentSlot.HEAD, colorArmor(new ItemStack(Items.LEATHER_HELMET, 1), color));
		bodyGuard.setItemStackToSlot(EntityEquipmentSlot.CHEST, colorArmor(new ItemStack(Items.LEATHER_CHESTPLATE, 1), color));
		bodyGuard.setItemStackToSlot(EntityEquipmentSlot.LEGS, colorArmor(new ItemStack(Items.LEATHER_LEGGINGS, 1), color));
		bodyGuard.setItemStackToSlot(EntityEquipmentSlot.FEET, colorArmor(new ItemStack(Items.LEATHER_BOOTS, 1), color));
	}

	protected static ItemStack colorArmor(ItemStack stack, int color) {
		ItemArmor armor = (ItemArmor) stack.getItem();
		armor.setColor(stack, color);
		return stack;
	}

	private static NemesisEntry getNemesisForSpawn(EntityEvent event) {

		if (!NemesisUtil.isNemesisClassEntity(event.getEntity())) {
			return null;
		}

		EntityLiving entity = (EntityLiving) event.getEntity();
		World world = entity.world;
		EntityPlayer targetPlayer = findVisiblePlayer(entity);

		if (targetPlayer == null) {
			return null;
		}

		if (world.rand.nextInt(SPAWN_CHANCE) != 0) {
			return null;
		}

		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(event.getEntity().world).list();
		nemeses.removeIf(NemesisEntry::isSpawned);
		nemeses.removeIf(NemesisEntry::isDead);
		nemeses.removeIf((NemesisEntry n) -> notReadyToSpawn(world, n));
		nemeses.removeIf((NemesisEntry n) -> !inRage(entity, n));

		if (nemeses.size() < 1) {
			return null;
		}

		NemesisEntry selectNemesis = nemeses.get(event.getEntity().world.rand.nextInt(nemeses.size()));
		selectNemesis.setTargetPlayer(targetPlayer);
		return selectNemesis;
	}

	private static boolean inRage(Entity entity, NemesisEntry nemesis) {
		int x = (int) entity.posX;
		int z = (int) entity.posZ;

		int nx = nemesis.getX();
		int nz = nemesis.getZ();

		int r = nemesis.getRange();

		return x < nx + r && x > nx - r && z < nz + r && z > nz - r;
	}

	private static boolean notReadyToSpawn(World world, NemesisEntry n) {
		if (n.getLastSpawned() == null) {
			return false;
		}
		long delay = (n.getLastSpawned() + SPAWN_COOLDOWN_PERIOD) - world.getTotalWorldTime();
		return delay > 0;
	}

	private static boolean otherNemesisNearby(EntityLiving entity, World world) {
		int distance = 100;
		List<EntityLiving> entities = world
				.getEntitiesWithinAABB(EntityLiving.class, new AxisAlignedBB(entity.getPosition()).grow(distance, distance, distance));
		for (EntityLiving e : entities) {
			if (e.getTags().contains(NemesisSystem.TAG_NEMESIS) && !e.isDead) {
				return true;
			}
		}
		return false;
	}

	private static EntityPlayer findVisiblePlayer(EntityLiving entity) {
		int distance = MAX_SPAWN_DISTANCE;

		List<EntityPlayer> players = entity.world.getEntitiesWithinAABB(
				EntityPlayer.class,
				new AxisAlignedBB(entity.getPosition()).grow(distance, distance, distance));

		for (EntityPlayer player : players) {
			if (entity.getEntitySenses().canSee(player)) {
				return player;
			}
		}
		return null;
	}

} public class MessageOpenNemesisDetailsGui implements IMessage {

	private NemesisEntry nemesis;
	private NemesisKnowledge knowledge;

	public static void init(int packetId) {
		NemesisSystem.NETWORK.registerMessage(MessageOpenNemesisDetailsGui.Handler.class, MessageOpenNemesisDetailsGui.class, packetId, Side.CLIENT);
	}

	public MessageOpenNemesisDetailsGui() {

	}

	public MessageOpenNemesisDetailsGui(NemesisEntry nemesis, NemesisKnowledge knowledge) {
		this.nemesis = nemesis;
		this.knowledge = knowledge;
	}

	@Override
	public void fromBytes(ByteBuf buf) {
		nemesis = NemesisEntry.load(ByteBufUtils.readTag(buf));
		knowledge = NemesisKnowledge.load(ByteBufUtils.readTag(buf));
	}

	@Override
	public void toBytes(ByteBuf buf) {
		ByteBufUtils.writeTag(buf, NemesisEntry.save(nemesis));
		ByteBufUtils.writeTag(buf, NemesisKnowledge.save(knowledge));
	}

	public static class Handler implements IMessageHandler<MessageOpenNemesisDetailsGui, IMessage> {
		@Override
		public IMessage onMessage(final MessageOpenNemesisDetailsGui message, MessageContext ctx) {
			Minecraft.getMinecraft().addScheduledTask(() -> work(message));
			return null;
		}

		private static void work(MessageOpenNemesisDetailsGui message) {
			NemesisSystem.NEMESIS = message.nemesis;
			NemesisSystem.KNOWLEDGE = message.knowledge;
			NemesisSystem.PROXY.openGui(NemesisSystemGuiHandler.NEMESIS_DETAILS_GUI);
		}
	}

} public class MessageOpenNemesisGui implements IMessage {

	private List<NemesisEntry> nemeses;
	private PlayerKnowledgeBase knowledgeBase;

	public static void init(int packetId) {
		NemesisSystem.NETWORK.registerMessage(MessageOpenNemesisGui.Handler.class, MessageOpenNemesisGui.class, packetId, Side.CLIENT);
	}

	public MessageOpenNemesisGui() {

	}

	public MessageOpenNemesisGui(List<NemesisEntry> nemeses, PlayerKnowledgeBase knowledgeBase) {
		this.nemeses = nemeses;
		this.knowledgeBase = knowledgeBase;
	}

	public MessageOpenNemesisGui(EntityPlayerMP player) {
		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(player.getEntityWorld()).list();
		nemeses.removeIf(NemesisEntry::isDead);
		this.nemeses = nemeses;
		this.knowledgeBase = PlayerKnowledgeBase.get(player);
	}

	@Override
	public void fromBytes(ByteBuf buf) {
		nemeses = NemesisRegistry.readNemesesFromNBT(ByteBufUtils.readTag(buf));
		knowledgeBase = PlayerKnowledgeBase.load(ByteBufUtils.readTag(buf));
	}

	@Override
	public void toBytes(ByteBuf buf) {
		NBTTagCompound c = new NBTTagCompound();
		NemesisRegistry.writeNemesesToNBT(c, nemeses);
		ByteBufUtils.writeTag(buf, c);
		ByteBufUtils.writeTag(buf, PlayerKnowledgeBase.save(knowledgeBase));
	}

	public static class Handler implements IMessageHandler<MessageOpenNemesisGui, IMessage> {
		@Override
		public IMessage onMessage(final MessageOpenNemesisGui message, MessageContext ctx) {
			Minecraft.getMinecraft().addScheduledTask(() -> work(message));
			return null;
		}

		public static void work(MessageOpenNemesisGui message) {
			NemesisSystem.NEMESES = message.nemeses;
			NemesisSystem.KNOWLEDGE_BASE = message.knowledgeBase;

			//EntityPlayer player = Minecraft.getMinecraft().player;
			//player.openGui(NemesisSystem.INSTANCE, NemesisSystemGuiHandler.NEMESIS_LIST_GUI, player.world, (int) player.posX, (int) player.posY, (int) player.posZ);
			NemesisSystem.PROXY.openGui(NemesisSystemGuiHandler.NEMESIS_LIST_GUI);
		}
	}
} public interface INemesisRegistry {

	void register(NemesisEntry nemesis);

	/**
	 * To remove a nemesis, update it with setDead(true)
	 */
	void update(NemesisEntry nemesis);

	NemesisEntry getById(UUID id);

	NemesisEntry getByName(String name);

	List<NemesisEntry> list();

	void clear();

	void markDirty();

	/*

	void unload(UUID id);

	void load(EntityCreature entity, UUID id);

	void promote(UUID id);

	void duelIfCrowded(NemesisEntry opponentOne, NemesisEntry opponentTwo);

	void setDead(UUID id, String slayerName);

	*/
} public class NemesisEntry {

	/**
	 * the range of the nemesis's domain, setting to 50 would make a 100x100 block domain
	 */
	private static final int RANGE = 100;

	/**
	 * the chunk the entity is in is loaded
	 */
	@NbtField
	private Long unloaded;

	/**
	 * the entity ID when this nemesis is spawned
	 */
	@NbtField
	private Integer spawned;

	@NbtField
	private Long lastSpawned;

	@NbtField
	private String title;

	@NbtField
	private String name;

	@NbtField
	private int level;

	@NbtField
	private String mob;

	@NbtField
	private int x;

	@NbtField
	private int z;

	@NbtField
	private UUID id;

	@NbtField
	private UUID entityUuid;

	@NbtField(genericType = Trait.class)
	private List<Trait> traits = new ArrayList<>();

	@NbtField(genericType = LogEntry.class)
	private List<LogEntry> history = new ArrayList<>();

	@NbtField
	private int dimension;

	@NbtField
	private int isChild;

	private transient boolean isDead;
	private transient EntityPlayer targetPlayer;

	@NbtField
	private NonNullList<ItemStack> handInventory = NonNullList.withSize(2, ItemStack.EMPTY);

	@NbtField
	private NonNullList<ItemStack> armorInventory = NonNullList.withSize(4, ItemStack.EMPTY);

	public void register(World world) {
		NemesisRegistryProvider.get(world).register(this);
	}

	@Override
	public String toString() {
		return getNameAndTitle();
	}

	@Deprecated
	public void readFromNBT(NBTTagCompound c) {
		NbtSerializer.read(c, this);
	}

	@Deprecated
	public void writeToNBT(NBTTagCompound c) {
		NbtSerializer.write(c, this);
	}

	public static NemesisEntry load(NBTTagCompound c) {
		NemesisEntry nemesis = new NemesisEntry();
		NbtSerializer.read(c, nemesis);
		return nemesis;
	}

	public static NBTTagCompound save(NemesisEntry nemesis) {
		NBTTagCompound c = new NBTTagCompound();
		NbtSerializer.write(c, nemesis);
		return c;
	}

	public void addToHistory(LogEntry logEntry) {
		if (history == null) {
			history = new ArrayList<>();
		}
		history.add(logEntry);
	}

	@SideOnly(Side.SERVER)
	public void markRegistryDirty() {
		World world = FMLCommonHandler.instance().getMinecraftServerInstance().getWorld(dimension);
		NemesisRegistryProvider.get(world).markDirty();
	}

	public String getName() {
		if (name == null) {
			return "Nobody";
		}
		return name;
	}

	public String getNameAndTitle() {
		return getName() + " the " + getTitle();
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public String getMob() {
		return mob;
	}

	public void setMob(String mob) {
		this.mob = mob;
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getZ() {
		return z;
	}

	public void setZ(int z) {
		this.z = z;
	}

	public NonNullList<ItemStack> getHandInventory() {
		return handInventory;
	}

	public NonNullList<ItemStack> getArmorInventory() {
		return armorInventory;
	}

	public UUID getId() {
		return id;
	}

	public void setId(UUID id) {
		this.id = id;
	}

	public List<Trait> getTraits() {
		return traits;
	}

	public boolean hasTrait(Type type) {
		return traits.stream().filter((Trait t) -> t.type.equals(type)).collect(Collectors.toList()).size() > 0;
	}

	public void setTraits(List<Trait> traits) {
		this.traits = traits;
	}

	public String getTitle() {
		if (title == null) {
			return "Unknown";
		}
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public boolean isDead() {
		return isDead;
	}

	public boolean isSpawned() {
		return spawned != null;
	}

	public boolean isLoaded() {
		return unloaded == null;
	}

	public Long getUnloaded() {
		return unloaded;
	}

	public void setUnloaded(Long unloaded) {
		this.unloaded = unloaded;
	}

	public int getRange() {
		return RANGE;
	}

	public int getDimension() {
		return dimension;
	}

	public void setDimension(int dimension) {
		this.dimension = dimension;
	}

	public Integer getSpawned() {
		return spawned;
	}

	public void setSpawned(Integer spawned) {
		this.spawned = spawned;
	}

	public void setDead(boolean dead) {
		isDead = dead;
	}

	public List<LogEntry> getHistory() {
		return history;
	}

	public void setHistory(List<LogEntry> history) {
		this.history = history;
	}

	public void setHandInventory(NonNullList<ItemStack> handInventory) {
		this.handInventory = handInventory;
	}

	public void setArmorInventory(NonNullList<ItemStack> armorInventory) {
		this.armorInventory = armorInventory;
	}

	public UUID getEntityUuid() {
		return entityUuid;
	}

	public void setEntityUuid(UUID entityUuid) {
		this.entityUuid = entityUuid;
	}

	public Long getLastSpawned() {
		return lastSpawned;
	}

	public void setLastSpawned(Long lastSpawned) {
		this.lastSpawned = lastSpawned;
	}

	public List<Trait> getWeaknesses() {
		return filterTraitByAffect(traits, Affect.WEAKNESS);
	}

	public List<Trait> getStrengths() {
		return filterTraitByAffect(traits, Affect.STRENGTH);
	}

	private static List<Trait> filterTraitByAffect(List<Trait> traits, Affect affect) {
		return traits.stream().filter((Trait t) -> t.type.getAffect().equals(affect)).collect(Collectors.toList());
	}

	public int getIsChild() {
		return isChild;
	}

	public boolean isChild() {
		return isChild == 1;
	}

	public void setChild(int child) {
		isChild = child;
	}

	public EntityPlayer getTargetPlayer() {
		return targetPlayer;
	}

	public void setTargetPlayer(EntityPlayer targetPlayer) {
		this.targetPlayer = targetPlayer;
	}
} public class NemesisRegistryProvider {

	public static INemesisRegistry get(World world) {
		MapStorage storage = world.getPerWorldStorage();
		NemesisRegistry instance = (NemesisRegistry) storage.getOrLoadData(NemesisRegistry.class, NemesisRegistry.NAME);
		if (instance == null) {
			instance = new NemesisRegistry();
			storage.setData(NemesisRegistry.NAME, instance);
		}
		return instance;
	}

} public class DiscoveryUtil {

	public static final String NBT_DISCOVERY = "nemesissystem_discovery";
	public static final String NBT_UNREAD_DISCOVERY = "nemesissystem_unread_discovery";
	public static final String NBT_PLAYER_DISCOVERIES = "nemesissystem_player_discoveries";

	private static int nextBookID = 0;
	private static Random rand = new Random();

	public static NemesisKnowledge getGetPlayerKnowledgeOfNemesis(EntityPlayer player, UUID nemesisId) {
		return PlayerKnowledgeBase.get(player).getKnowledgeOfNemesis(nemesisId);
	}

	public static void newDiscovery(EntityPlayer player, NemesisDiscovery discovery) {
		PlayerKnowledgeBase knowledgeBase = PlayerKnowledgeBase.get(player);
		knowledgeBase.add(discovery);
		knowledgeBase.save(player);
	}

	public static boolean isDiscoveryBook(ItemStack item) {
		if (item == null || item.isEmpty()) {
			return false;
		}
		return item.getItem() == Items.WRITTEN_BOOK && hasDiscoveryTag(item);
	}

	private static boolean hasDiscoveryTag(ItemStack item) {
		if (!item.hasTagCompound()) {
			return false;
		}
		return item.getTagCompound().hasKey(NBT_DISCOVERY) || item.getTagCompound().hasKey(NBT_UNREAD_DISCOVERY);
	}

	public static ItemStack createUnreadBook() {
		ItemStack book = new ItemStack(Items.WRITTEN_BOOK, 1);
		NBTTagCompound bookNbt = new NBTTagCompound();
		bookNbt.setTag(NBT_UNREAD_DISCOVERY, new NBTTagInt(nextBookID++));
		book.setTagCompound(bookNbt);
		book.setStackDisplayName("Unread Nemesis Book");
		return book;
	}

	public static NemesisDiscovery readBook(World world, ItemStack book) {
		if (isUnreadBook(book)) {
			setDiscoveryToBook(world, book, DiscoveryUtil.getRandomDiscovery(world));
		}
		NemesisDiscovery discovery = new NemesisDiscovery();
		discovery.readFromNBT(book.getTagCompound().getCompoundTag(NBT_DISCOVERY));
		return discovery;
	}

	private static boolean isUnreadBook(ItemStack book) {
		if (book.getTagCompound() == null) {
			return false;
		}
		return book.getTagCompound().hasKey(NBT_UNREAD_DISCOVERY);
	}

	private static void setDiscoveryToBook(World world, ItemStack book, NemesisDiscovery discovery) {
		NBTTagCompound bookNbt = book.getTagCompound();
		if (bookNbt == null) {
			bookNbt = new NBTTagCompound();
		}
		bookNbt.removeTag(NBT_UNREAD_DISCOVERY);
		bookNbt.setTag(NBT_DISCOVERY, nbt(discovery));
		book.setTagCompound(bookNbt);
		book.setStackDisplayName(generateBookName(world, discovery));
	}

	private static String generateBookName(World world, NemesisDiscovery discovery) {
		return discovery.toString() + " discovery for " + getNemesisName(world, discovery);
	}

	private static String getNemesisName(World world, NemesisDiscovery discovery) {
		NemesisEntry nemesis = NemesisRegistryProvider.get(world).getById(discovery.nemesisId);
		if (nemesis == null) {
			return "Nemesis";
		}
		return nemesis.getNameAndTitle();
	}

	private static NBTTagCompound nbt(NemesisDiscovery discovery) {
		NBTTagCompound discoveryNbt = new NBTTagCompound();
		discovery.writeToNBT(discoveryNbt);
		return discoveryNbt;
	}

	/**
	 * Get a random discovery for a random nemesis
	 */
	private static NemesisDiscovery getRandomDiscovery(World world) {
		return getRandomDiscovery(getRandomNemesis(world));
	}

	private static NemesisEntry getRandomNemesis(World world) {
		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(world).list();
		return nemeses.get(rand.nextInt(nemeses.size()));
	}

	/**
	 * Get a random discovery about the given nemesis
	 */
	private static NemesisDiscovery getRandomDiscovery(NemesisEntry nemesis) {
		NemesisDiscovery discovery = new NemesisDiscovery();
		discovery.nemesisId = nemesis.getId();

		int infoCount = 2 + nemesis.getTraits().size();

		int roll = rand.nextInt(infoCount);

		if (roll == 0) {
			discovery.type = Type.LOCATION;
			return discovery;
		}

		if (roll == 1) {
			discovery.type = Type.ITEMS;
			return discovery;
		}

		discovery.type = Type.TRAIT;
		discovery.index = roll - 2;
		return discovery;
	}

} public class EntityDecorator {

	/**
	 * Convert the provided entity into the given nemesis
	 *
	 * @param entity the entity to be converted into a nemesis
	 * @param nemesis the nemesis to create
	 */
	public static void decorate(EntityLiving entity, NemesisEntry nemesis) {
		entity.addTag(NemesisSystem.TAG_NEMESIS);
		entity.getEntityData().setUniqueId(NemesisSystem.NBT_NEMESIS_ID, nemesis.getId());
		entity.setCustomNameTag(nemesis.getNameAndTitle());

		if (entity instanceof EntityZombie) {
			((EntityZombie) entity).setChild(nemesis.isChild());
		}

		ItemStack helmet = nemesis.getArmorInventory().get(EntityEquipmentSlot.HEAD.getIndex());

		entity.setItemStackToSlot(EntityEquipmentSlot.HEAD, helmet);
		entity.setItemStackToSlot(EntityEquipmentSlot.CHEST, nemesis.getArmorInventory().get(EntityEquipmentSlot.CHEST.getIndex()));
		entity.setItemStackToSlot(EntityEquipmentSlot.LEGS, nemesis.getArmorInventory().get(EntityEquipmentSlot.LEGS.getIndex()));
		entity.setItemStackToSlot(EntityEquipmentSlot.FEET, nemesis.getArmorInventory().get(EntityEquipmentSlot.FEET.getIndex()));

		entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, nemesis.getHandInventory().get(EntityEquipmentSlot.MAINHAND.getIndex()));
		entity.setItemStackToSlot(EntityEquipmentSlot.OFFHAND, nemesis.getHandInventory().get(EntityEquipmentSlot.OFFHAND.getIndex()));

		for (IAttributeInstance attribute : entity.getAttributeMap().getAllAttributes()) {
			if (attribute.getAttribute() == SharedMonsterAttributes.ATTACK_DAMAGE) {
				attribute.setBaseValue(attribute.getAttributeValue() * (nemesis.getLevel() / 3));
			}

			if (attribute.getAttribute() == SharedMonsterAttributes.ATTACK_SPEED) {
				attribute.setBaseValue(attribute.getAttributeValue() * (nemesis.getLevel() / 8));
			}

			if (attribute.getAttribute() == SharedMonsterAttributes.MAX_HEALTH) {
				attribute.setBaseValue(attribute.getAttributeValue() * nemesis.getLevel());
				entity.setHealth(entity.getMaxHealth());
			}
		}
	}

} public class NemesisActions {

	public static void promote(World world, NemesisEntry nemesis) {
		if (nemesis.getLevel() >= 10) {
			return;
		}
		nemesis.setLevel(nemesis.getLevel() + 1);
		NemesisUtil.enchantEquipment(nemesis);
		if (shouldGainAdditionalTrait(nemesis)) {
			addAdditionalStrength(nemesis);
		} else {
			upgradeTrait(nemesis);
		}
		NemesisRegistryProvider.get(world).update(nemesis);
		MinecraftForge.EVENT_BUS.post(new PromotionEvent(nemesis));
	}

	private static boolean shouldGainAdditionalTrait(NemesisEntry nemesis) {
		return NemesisUtil.rand.nextInt(10 * nemesis.getTraits().size()) == 0;
	}

	private static void addAdditionalStrength(NemesisEntry nemesis) {
		List<Type> availableTraits = Arrays.stream(Type.values())
				.filter(t -> !nemesis.hasTrait(t))
				.filter(t -> t.isStrength())
				.collect(Collectors.toList());

		if (availableTraits.size() < 1) {
			return;
		}

		Type type = availableTraits.get(NemesisUtil.rand.nextInt(availableTraits.size()));
		nemesis.getTraits().add(new Trait(type, 1));
	}

	private static void upgradeTrait(NemesisEntry nemesis) {
		for (Trait trait : nemesis.getTraits()) {
			if (trait.type.isStrength() && upgradeTrait(trait)) {
				return;
			}
		}
	}

	private static void intensifyWeakness(NemesisEntry nemesis) {
		for (Trait trait : nemesis.getTraits()) {
			if (trait.type.isWeakness() && upgradeTrait(trait)) {
				return;
			}
		}
	}

	private static boolean upgradeTrait(Trait t) {
		int max = t.type.getMaxLevel();
		if (t.level >= max) {
			return false;
		}
		t.level++;
		return true;
	}

	public static void demote(World world, NemesisEntry nemesis, String slayerName) {
		nemesis.setLevel(nemesis.getLevel() - 1);
		if (nemesis.getLevel() < 1) {
			kill(world, nemesis, slayerName);
		} else {
			if (NemesisUtil.rand.nextInt(4) == 0) {
				intensifyWeakness(nemesis);
			}
			NemesisRegistryProvider.get(world).update(nemesis);
			MinecraftForge.EVENT_BUS.post(new DemotionEvent(nemesis, slayerName));
		}
	}

	public static NemesisEntry createAndRegisterNemesis(EntityCreature entity, BlockPos nemesisLocation) {

		boolean isChild = false;
		if (entity instanceof EntityZombie) {
			isChild = entity.isChild();
		}

		NemesisEntry nemesis = NemesisBuilder
				.build(entity.getEntityWorld(), SpawnApi.getEntityString(entity), isChild, entity.dimension, 1, nemesisLocation.getX(),
						nemesisLocation.getZ());

		NemesisRegistryProvider.get(entity.world).register(nemesis);

		return nemesis;
	}

	public static void kill(World world, NemesisEntry nemesis, String slayerName) {
		nemesis.setSpawned(null);
		nemesis.setDead(true);
		NemesisRegistryProvider.get(world).update(nemesis);
		MinecraftForge.EVENT_BUS.post(new DeathEvent(nemesis, slayerName));
	}

	public static void duel(World world, NemesisEntry opponentOne, NemesisEntry opponentTwo) {
		NemesisEntry victor;
		NemesisEntry loser;

		int attack1 = 0;
		int attack2 = 0;

		while (attack1 == attack2) {
			attack1 = NemesisUtil.rand.nextInt(opponentOne.getLevel()) + NemesisUtil.rand.nextInt(3);
			attack2 = NemesisUtil.rand.nextInt(opponentOne.getLevel()) + NemesisUtil.rand.nextInt(3);
		}

		if (attack1 > attack2) {
			victor = opponentOne;
			loser = opponentTwo;
		} else {
			victor = opponentTwo;
			loser = opponentOne;
		}

		kill(world, loser, victor.getName());
		promote(world, victor);
		MinecraftForge.EVENT_BUS.post(new DuelEvent(victor, loser));
	}

	public static void duelIfCrowded(World world, NemesisEntry exclude, boolean onlyIfCrowded) {
		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(world).list();
		nemeses.removeIf(NemesisEntry::isDead);

		if (onlyIfCrowded && nemeses.size() < NemesisConfig.NEMESIS_LIMIT) {
			return;
		}

		nemeses.removeIf(NemesisEntry::isSpawned);
		if (exclude != null) {
			nemeses.removeIf((NemesisEntry n) -> n.getId().equals(exclude.getId()));
		}

		if (nemeses.size() < 2) {
			return;
		}

		//TODO factor in distance, the closer the nemeses the more likely they should be to duelIfCrowded

		// get the weaklings
		Collections.shuffle(nemeses);
		nemeses.sort(Comparator.comparingInt(NemesisEntry::getLevel));
		duel(world, nemeses.get(0), nemeses.get(1));
	}

	public static void promoteRandomNemesis(EntityCreature entity, INemesisRegistry registry, List<NemesisEntry> nemeses) {
		if (nemeses == null || nemeses.size() < 1) {
			return;
		}
		promote(entity.world, nemeses.get(entity.getRNG().nextInt(nemeses.size())));
	}

	public static void handleRandomPromotions(World world, EntityCreature entity) {
		INemesisRegistry registry = NemesisRegistryProvider.get(world);

		List<NemesisEntry> nemeses = registry.list();
		nemeses.removeIf(NemesisEntry::isDead);

		if (isAtHalfPopulation(nemeses)) {
			return;
		}

		promoteRandomNemesis(entity, registry, nemeses);
		BlockPos pos = NemesisUtil.getRandomNemesisLocation(entity.world, BlockPos.ORIGIN);
		createAndRegisterNemesis(getRandomEntityFromWhitelist(world), pos);
	}

	private static EntityCreature getRandomEntityFromWhitelist(World world) {
		String mob = NemesisConfig.MOB_WHITELIST[world.rand.nextInt(NemesisConfig.MOB_WHITELIST.length)];
		return SpawnApi.getEntityFromString(world, mob);
	}

	private static boolean isAtHalfPopulation(List<NemesisEntry> nemeses) {
		return nemeses.size() >= (NemesisConfig.NEMESIS_LIMIT / 2);
	}

	public static void throwPearl(EntityLiving entity, EntityLivingBase target) {
		World world = entity.getEntityWorld();
		EntityEnderPearl pearl = new EntityEnderPearl(world, entity);

		double dX = target.posX - entity.posX;
		double dY = target.getEntityBoundingBox().minY + (double) (target.height / 3.0F) - pearl.posY;
		double dZ = target.posZ - entity.posZ;

		double distanceSq = dX * dX + dY * dY + dZ * dZ;

		if (distanceSq < 20) {
			return;
		}

		double levelDistance = MathHelper.sqrt(dX * dX + dZ * dZ);

		pearl.setThrowableHeading(dX, dY + levelDistance * 0.20000000298023224D, dZ, 1.6F,
				(float) (14 - world.getDifficulty().getDifficultyId() * 4));

		entity.playSound(SoundEvents.ENTITY_ENDERPEARL_THROW, 1.0F, 1.0F / (world.rand.nextFloat() * 0.4F + 0.8F));

		world.spawnEntity(pearl);
	}
} public class NemesisBuilder {

	private static Random rand = new Random();

	private static final Item[] MELEE_WEAPONS = {
			Items.STONE_SHOVEL, Items.STONE_AXE, Items.STONE_HOE, Items.STONE_PICKAXE, Items.STONE_SWORD,
			Items.GOLDEN_SHOVEL, Items.GOLDEN_AXE, Items.GOLDEN_HOE, Items.GOLDEN_PICKAXE, Items.GOLDEN_SWORD,
			Items.IRON_SHOVEL, Items.IRON_AXE, Items.IRON_HOE, Items.IRON_PICKAXE, Items.IRON_SWORD,
			Items.DIAMOND_SHOVEL, Items.DIAMOND_AXE, Items.DIAMOND_HOE, Items.DIAMOND_PICKAXE, Items.DIAMOND_SWORD,
	};

	private static final Item[] HELMETS = {
			Items.LEATHER_HELMET,
			Items.GOLDEN_HELMET,
			Items.IRON_HELMET,
			Items.DIAMOND_HELMET,
			Items.CHAINMAIL_HELMET
	};

	private static final Item[] CHEST_PLATES = {
			Items.LEATHER_CHESTPLATE,
			Items.GOLDEN_CHESTPLATE,
			Items.IRON_CHESTPLATE,
			Items.DIAMOND_CHESTPLATE,
			Items.CHAINMAIL_CHESTPLATE
	};

	private static final Item[] LEGGINGS = {
			Items.LEATHER_LEGGINGS,
			Items.GOLDEN_LEGGINGS,
			Items.IRON_LEGGINGS,
			Items.DIAMOND_LEGGINGS,
			Items.CHAINMAIL_LEGGINGS
	};

	private static final Item[] BOOTS = {
			Items.LEATHER_BOOTS,
			Items.GOLDEN_BOOTS,
			Items.IRON_BOOTS,
			Items.DIAMOND_BOOTS,
			Items.CHAINMAIL_BOOTS
	};

	public static NemesisEntry build(World world, String mob, boolean isChild, int dimension, int level, int x, int z) {
		NemesisEntry nemesis = new NemesisEntry();

		nemesis.setId(UUID.randomUUID());
		nemesis.setName(getUniqueName(world));
		nemesis.setTitle(getUniqueTitle(world));

		nemesis.setLevel(level);
		nemesis.setMob(mob);
		nemesis.setChild(isChild ? 1 : 0);
		nemesis.setX(x);
		nemesis.setZ(z);
		nemesis.setDimension(dimension);

		nemesis.setTraits(new ArrayList<>());

		/*
		 * add a strength
		 */
		nemesis.getTraits().add(new Trait(Type.STRENGTHS[rand.nextInt(Type.STRENGTHS.length)], 1));
		//nemesis.getTraits().add(Trait.TELEPORT);

		/*
		 * add a weakness
		 */
		nemesis.getTraits().add(new Trait(Type.WEAKNESSES[rand.nextInt(Type.WEAKNESSES.length)], 1));
		//nemesis.getWeaknesses().add(Weakness.GREEDY);

		nemesis.getHandInventory().set(0, new ItemStack(MELEE_WEAPONS[rand.nextInt(MELEE_WEAPONS.length)]));
		setOffhandItem(nemesis);
		setArmor(nemesis);
		NemesisUtil.enchantEquipment(nemesis);

		return nemesis;
	}

	public static String getUniqueTitle(World world) {
		List<NemesisEntry> nemeses = NemesisRegistryProvider.get(world).list();
		String title = getRandomTitle();
		while (!isUniqueTitle(title, nemeses)) {
			title = getRandomTitle();
		}
		return title;
	}

	public static String getRandomTitle() {
		return NameBuilder.TITLES[rand.nextInt(NameBuilder.TITLES.length)];
	}

	private static boolean isUniqueTitle(String title, List<NemesisEntry> nemeses) {
		for (NemesisEntry nemesis : nemeses) {
			if (nemesis.getTitle().equals(title)) {
				return false;
			}
		}
		return true;
	}

	public static String getUniqueName(World world) {
		String name = NameBuilder.build();
		if (NemesisRegistryProvider.get(world).getByName(name) == null) {
			return name;
		}
		return getUniqueName(world);
	}

	private static void setArmor(NemesisEntry nemesis) {
		// TODO design armor sets
		nemesis.getArmorInventory().set(EntityEquipmentSlot.HEAD.getIndex(), new ItemStack(HELMETS[rand.nextInt(HELMETS.length)]));
		nemesis.getArmorInventory().set(EntityEquipmentSlot.CHEST.getIndex(), new ItemStack(CHEST_PLATES[rand.nextInt(CHEST_PLATES.length)]));
		nemesis.getArmorInventory().set(EntityEquipmentSlot.LEGS.getIndex(), new ItemStack(LEGGINGS[rand.nextInt(LEGGINGS.length)]));
		nemesis.getArmorInventory().set(EntityEquipmentSlot.FEET.getIndex(), new ItemStack(BOOTS[rand.nextInt(BOOTS.length)]));
	}

	private static void setOffhandItem(NemesisEntry nemesis) {
		ItemStack offhand;
		switch (nemesis.getTraits().get(0).type) {
		case FIREBALL:
			offhand = new ItemStack(Items.LAVA_BUCKET);
			break;
		case ARCHER:
			offhand = new ItemStack(Items.BOW);
			break;
		case POTION:
			offhand = new ItemStack(Items.POTIONITEM);
			break;
		case SUMMON:
			offhand = new ItemStack(Items.STICK);
			break;
		case REFLECT:
			// TODO color shield
			offhand = new ItemStack(Items.SHIELD);
			break;
		case TELEPORT:
			offhand = new ItemStack(Items.ENDER_PEARL);
			break;
		case DOUBLE_MELEE:
			offhand = new ItemStack(MELEE_WEAPONS[rand.nextInt(MELEE_WEAPONS.length)]);
			break;
		default:
			offhand = ItemStack.EMPTY;
		}
		nemesis.getHandInventory().set(1, offhand);
	}

} public class NemesisUtil {

	public static final Random rand = new Random();

	public static boolean isNemesisClassEntity(Entity entity) {
		String entityType = SpawnApi.getEntityString(entity);
		return Arrays.asList(NemesisConfig.MOB_WHITELIST).contains(entityType);
	}

	public static BlockPos getRandomNemesisLocation(World world, BlockPos around) {
		BlockPos pos = null;
		for(int attempt = 0; attempt < 20; attempt++){
			pos = getRandomLocation(around);
			if(isValidNemesisLocation(world, pos)){
				return pos;
			}
		}
		return pos;
	}

	private static boolean isValidNemesisLocation(World world, BlockPos pos) {
		BlockPos scan = new BlockPos(pos.getX(), world.getActualHeight(), pos.getZ());
		while(scan.getY() > 0){
			if (!world.isAirBlock(scan)) {
				return world.getBlockState(scan).isOpaqueCube();
			}
			scan = scan.down();
		}
		return false;
	}

	public static BlockPos getRandomLocation(BlockPos around) {
		int fifthRadius = NemesisConfig.NEMESIS_SETTLE_RADIUS / 5;
		int distance = fifthRadius + rand.nextInt(fifthRadius * 4);
		double radians = Math.toRadians(rand.nextDouble() * 360);
		int x = (int) (distance * Math.cos(radians));
		int z = (int) (distance * Math.sin(radians));
		BlockPos out = new BlockPos(around.getX() + x, around.getY(), around.getZ() + z);
		return out;
	}

	public static void enchantEquipment(NemesisEntry nemesis) {
		if (nemesis == null) {
			return;
		}
		enchantItems(nemesis.getArmorInventory());
		enchantItems(nemesis.getHandInventory());
	}

	public static void enchantItems(List<ItemStack> items) {
		if (items == null) {
			return;
		}
		for (ItemStack item : items) {
			if (rand.nextBoolean()) {
				enchantItem(item);
			}
		}
	}

	public static void enchantItem(ItemStack item) {
		if (item == null || item.isEmpty()) {
			return;
		}
		if (!improveEnchants(item)) {
			addNewEnchantment(item);
		}
	}

	private static boolean improveEnchants(ItemStack item) {
		boolean improved = false;
		Map<Enchantment, Integer> enchantments = EnchantmentHelper.getEnchantments(item);

		if (enchantments.isEmpty()) {
			return false;
		}

		for (Entry<Enchantment, Integer> enchant : enchantments.entrySet()) {
			if (shouldImproveEnchantment(rand, enchant.getKey(), enchant.getValue())) {
				enchantments.put(enchant.getKey(), enchant.getValue() + 1);
				improved = true;
			}
		}

		if (improved) {
			EnchantmentHelper.setEnchantments(enchantments, item);
		}

		return improved;
	}

	private static boolean shouldImproveEnchantment(Random rand, Enchantment enchantment, Integer level) {
		return level < enchantment.getMaxLevel() && rand.nextBoolean();
	}

	private static void addNewEnchantment(ItemStack item) {
		EnchantmentHelper.addRandomEnchantment(rand, item, 1, true);
		removeDuplicateEnchantments(item);
	}

	private static void removeDuplicateEnchantments(ItemStack item) {
		EnchantmentHelper.setEnchantments(EnchantmentHelper.getEnchantments(item), item);
	}

	public static boolean isBodyGuard(EntityLiving searchEntity, UUID id) {
		return searchEntity.getTags().contains(NemesisSystem.TAG_BODY_GUARD)
				&& id.equals(searchEntity.getEntityData().getUniqueId(NemesisSystem.NBT_NEMESIS_ID));
	}

	public static boolean isNemesis(EntityLiving searchEntity) {
		return searchEntity.getTags().contains(NemesisSystem.TAG_NEMESIS);
	}

	public static boolean isNemesis(EntityLiving searchEntity, UUID id) {
		return isNemesis(searchEntity) && id.equals(searchEntity.getEntityData().getUniqueId(NemesisSystem.NBT_NEMESIS_ID));
	}

	public static List<EntityCreature> findNemesisBodyGuards(World world, UUID id, BlockPos position) {
		int distance = 100;

		return world.getEntitiesWithinAABB(EntityCreature.class, nearByBox(position, distance),
				(EntityCreature searchEntity) -> isBodyGuard(searchEntity, id)
		);
	}

	public static EntityLiving findNemesisAround(World world, UUID id, BlockPos position, int distance) {
		List<EntityLiving> entities = world.getEntitiesWithinAABB(EntityLiving.class, nearByBox(position, distance),
				(EntityLiving searchEntity) -> isNemesis(searchEntity, id)
		);

		if (entities.size() < 1) {
			return null;
		}

		return entities.get(0);
	}

	public static List<EntityPlayer> findPlayersAround(World world, BlockPos position, int distance) {
		return world.getEntitiesWithinAABB(EntityPlayer.class, nearByBox(position, distance));
	}

	public static AxisAlignedBB nearByBox(BlockPos position, int radius) {
		return new AxisAlignedBB(position).grow(radius, radius, radius);
	}

	public static NemesisEntry loadNemesisFromEntity(Entity nemesisEntity) {
		if (nemesisEntity == null) {
			return null;
		}
		UUID id = nemesisEntity.getEntityData().getUniqueId(NemesisSystem.NBT_NEMESIS_ID);
		return NemesisRegistryProvider.get(nemesisEntity.getEntityWorld()).getById(id);
	}

	public static String romanize(int i) {
		switch (i) {
		case 1:
			return "I";
		case 2:
			return "II";
		case 3:
			return "III";
		case 4:
			return "IV";
		case 5:
			return "V";
		case 6:
			return "VI";
		case 7:
			return "VII";
		case 8:
			return "VIII";
		case 9:
			return "IX";
		case 10:
			return "X";
		}
		return Integer.toString(i, 10);
	}
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.minecraft.command.CommandBase;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.command.WrongUsageException;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityCreature;
import net.minecraft.entity.EntityList;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.torocraft.nemesissystem.discovery.NemesisKnowledge;
import net.torocraft.nemesissystem.handlers.SpawnHandler;
import net.torocraft.nemesissystem.network.MessageOpenNemesisDetailsGui;
import net.torocraft.nemesissystem.network.MessageOpenNemesisGui;
import net.torocraft.nemesissystem.registry.INemesisRegistry;
import net.torocraft.nemesissystem.registry.NemesisEntry;
import net.torocraft.nemesissystem.registry.NemesisRegistryProvider;
import net.torocraft.nemesissystem.util.DiscoveryUtil;
import net.torocraft.nemesissystem.util.EntityDecorator;
import net.torocraft.nemesissystem.util.NemesisActions;
import net.torocraft.nemesissystem.util.NemesisBuilder;
import net.torocraft.nemesissystem.util.NemesisUtil;
import net.torocraft.torotraits.api.SpawnApi;
import net.torocraft.torotraits.traits.Trait;
import net.torocraft.torotraits.traits.Type;
			return;
		}

		EntityPlayer player = getCommandSenderAsPlayer(sender);
		World world = player.world;
		int x = player.getPosition().getX();
		int z = player.getPosition().getZ();

		NemesisEntry nemesis = new NemesisEntry();

		nemesis.setId(TEST_ID);
		nemesis.setName(NemesisBuilder.getUniqueName(world) + " of Test");
		nemesis.setTitle(NemesisBuilder.getUniqueTitle(world));

		nemesis.setLevel(10);
		nemesis.setMob("minecraft:zombie");
		nemesis.setChild(0);
		nemesis.setX(x);
		nemesis.setZ(z);
		nemesis.setDimension(player.dimension);

		nemesis.setTraits(new ArrayList<>());
		//nemesis.getTraits().add(new Trait(Type.WOOD_ALLERGY, 5));
		//nemesis.getTraits().add(new Trait(Type.ARCHER, 4));
		//nemesis.getTraits().add(new Trait(Type.FIREBALL, 4));
		nemesis.getTraits().add(new Trait(Type.GLUTTONY, 1));
		nemesis.getTraits().add(new Trait(Type.GREEDY, 1));
		nemesis.getTraits().add(new Trait(Type.CHICKEN, 1));

		INemesisRegistry registry = NemesisRegistryProvider.get(world);
		if (registry.getById(TEST_ID) == null) {
			registry.register(nemesis);
			System.out.println("created test nemesis: " + nemesis);
		} else {
			registry.update(nemesis);
			System.out.println("updated test nemesis: " + nemesis);
		}
	}

	private void demote(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException {
		if (args.length != 2) {
			throw new WrongUsageException("commands.nemesis_system.usage");
		}
		World world = server.getWorld(senderDimId(sender));
		INemesisRegistry registry = NemesisRegistryProvider.get(world);
		NemesisEntry nemesis = registry.getByName(args[1]);
		if (nemesis == null) {
			return;
		}
		NemesisActions.demote(world, nemesis, "Server Command");
		registry.markDirty();
	}

	private void promote(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException {
		if (args.length != 2) {
			throw new WrongUsageException("commands.nemesis_system.usage");
		}
		World world = server.getWorld(senderDimId(sender));
		INemesisRegistry registry = NemesisRegistryProvider.get(world);
		NemesisEntry nemesis = registry.getByName(args[1]);
		if (nemesis == null) {
			return;
		}
		NemesisActions.promote(world, nemesis);
		registry.markDirty();
	}

	private void enchant(ICommandSender sender) throws CommandException {
		if (sender instanceof EntityPlayer) {
			NemesisUtil.enchantItems(getHotBarItems(getCommandSenderAsPlayer(sender)));
			logHotBarItems(getCommandSenderAsPlayer(sender));
		}
	}

	private void logHotBarItems(EntityPlayer player) {
		InventoryPlayer inv = player.inventory;
		for (int i = 0; i < inv.getSizeInventory(); i++) {
			if (InventoryPlayer.isHotbar(i)) {
				ItemStack stack = inv.getStackInSlot(i);
				System.out.println(stack.getTagCompound());
			}
		}
	}

	private List<ItemStack> getHotBarItems(EntityPlayer player) {
		InventoryPlayer inv = player.inventory;
		List<ItemStack> items = new ArrayList<>();
		for (int i = 0; i < inv.getSizeInventory(); i++) {
			if (InventoryPlayer.isHotbar(i)) {
				items.add(inv.getStackInSlot(i));
			}
		}
		return items;
	}

	private void duel(ICommandSender sender) {
		if (sender instanceof EntityPlayer) {
			NemesisActions.duelIfCrowded(sender.getEntityWorld(), null, false);
		}
	}

	private void gui(ICommandSender sender, String[] args) throws CommandException {

		if (!(sender instanceof EntityPlayer)) {
			return;
		}

		EntityPlayerMP player = getCommandSenderAsPlayer(sender);

		if (args.length == 1) {
			NemesisSystem.NETWORK.sendTo(new MessageOpenNemesisGui(player), player);
			return;
		}

		if (args.length == 2) {
			NemesisEntry nemesis = NemesisRegistryProvider.get(player.world).getByName(args[1]);
			NemesisKnowledge knowledge = DiscoveryUtil.getGetPlayerKnowledgeOfNemesis(player, nemesis.getId());
			if (knowledge == null) {
				knowledge = new NemesisKnowledge();
			}
			NemesisSystem.NETWORK.sendTo(new MessageOpenNemesisDetailsGui(nemesis, knowledge), player);