package slimeknights.tconstruct.library.tinkering;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import gnu.trove.set.hash.THashSet;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.EnumRarity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.translation.I18n;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.annotation.Nonnull;
import slimeknights.mantle.util.RecipeMatch;
import slimeknights.tconstruct.common.ClientProxy;
import slimeknights.tconstruct.common.config.Config;
import slimeknights.tconstruct.library.TinkerRegistry;
import slimeknights.tconstruct.library.Util;
import slimeknights.tconstruct.library.events.TinkerEvent;
import slimeknights.tconstruct.library.materials.HeadMaterialStats;
import slimeknights.tconstruct.library.materials.Material;
import slimeknights.tconstruct.library.materials.MaterialTypes;
import slimeknights.tconstruct.library.modifiers.IModifier;
import slimeknights.tconstruct.library.modifiers.ModifierNBT;
import slimeknights.tconstruct.library.modifiers.TinkerGuiException;
import slimeknights.tconstruct.library.traits.ITrait;
import slimeknights.tconstruct.library.utils.TagUtil;
import slimeknights.tconstruct.library.utils.Tags;
import slimeknights.tconstruct.library.utils.TinkerUtil;
import slimeknights.tconstruct.library.utils.ToolBuilder;
import slimeknights.tconstruct.library.utils.ToolHelper;
import slimeknights.tconstruct.library.utils.TooltipBuilder;
/**
* The base for each Tinker tool.
*/
public abstract class TinkersItem extends Item implements ITinkerable, IModifyable, IRepairable {
protected final PartMaterialType[] requiredComponents;
// used to classify what the thing can do
protected final Set<Category> categories = new THashSet<Category>();
public TinkersItem(PartMaterialType... requiredComponents) {
this.requiredComponents = requiredComponents;
this.setMaxStackSize(1);
//this.setHasSubtypes(true);
}
/* Tool Information */
public List<PartMaterialType> getRequiredComponents() {
return ImmutableList.copyOf(requiredComponents);
}
public List<PartMaterialType> getToolBuildComponents() {
return getRequiredComponents();
}
protected void addCategory(Category... categories) {
Collections.addAll(this.categories, categories);
}
public boolean hasCategory(Category category) {
return categories.contains(category);
}
protected Category[] getCategories() {
Category[] out = new Category[categories.size()];
int i = 0;
for(Category category : categories) {
out[i++] = category;
}
return out;
}
/* INDESTRUCTIBLE */
@Override
public boolean hasCustomEntity(ItemStack stack) {
return true;
}
@Nonnull
@Override
public Entity createEntity(World world, Entity location, ItemStack itemstack) {
EntityItem entity = new IndestructibleEntityItem(world, location.posX, location.posY, location.posZ, itemstack);
if(location instanceof EntityItem) {
// workaround for private access on that field >_>
NBTTagCompound tag = new NBTTagCompound();
location.writeToNBT(tag);
entity.setPickupDelay(tag.getShort("PickupDelay"));
}
entity.motionX = location.motionX;
entity.motionY = location.motionY;
entity.motionZ = location.motionZ;
return entity;
}
/* Building the Item */
public boolean validComponent(int slot, ItemStack stack) {
if(slot > requiredComponents.length || slot < 0) {
return false;
}
return requiredComponents[slot].isValid(stack);
}
/**
* Builds an Itemstack of this tool with the given materials, if applicable.
*
* @param stacks Items to build with. Have to be in the correct order and have exact length. No nulls!
* @return The built item or null if invalid input.
*/
public ItemStack buildItemFromStacks(ItemStack[] stacks) {
List<Material> materials = new ArrayList<Material>(stacks.length);
if(stacks.length != requiredComponents.length) {
return null;
}
// not a valid part arrangement for this tool
for(int i = 0; i < stacks.length; i++) {
if(!validComponent(i, stacks[i])) {
return null;
}
materials.add(TinkerUtil.getMaterialFromStack(stacks[i]));
}
return buildItem(materials);
}
/**
* Builds an Itemstack of this tool with the given materials.
*
* @param materials Materials to build with. Have to be in the correct order. No nulls!
* @return The built item or null if invalid input.
*/
public ItemStack buildItem(List<Material> materials) {
ItemStack tool = new ItemStack(this);
tool.setTagCompound(buildItemNBT(materials));
return tool;
}
/**
* Builds the NBT for a new tinker item with the given data.
*
* @param materials Materials to build with. Have to be in the correct order. No nulls!
* @return The built nbt
*/
public NBTTagCompound buildItemNBT(List<Material> materials) {
NBTTagCompound basetag = new NBTTagCompound();
NBTTagCompound toolTag = buildTag(materials);
NBTTagCompound dataTag = buildData(materials);
basetag.setTag(Tags.BASE_DATA, dataTag);
basetag.setTag(Tags.TOOL_DATA, toolTag);
// copy of the original tool data
basetag.setTag(Tags.TOOL_DATA_ORIG, toolTag.copy());
// save categories on the tool
TagUtil.setCategories(basetag, getCategories());
// add traits
addMaterialTraits(basetag, materials);
// fire toolbuilding event
TinkerEvent.OnItemBuilding.fireEvent(basetag, ImmutableList.copyOf(materials), this);
return basetag;
}
/**
* Creates an NBT Tag with the materials that were used to build the item.
*/
private NBTTagCompound buildData(List<Material> materials) {
NBTTagCompound base = new NBTTagCompound();
NBTTagList materialList = new NBTTagList();
for(Material material : materials) {
materialList.appendTag(new NBTTagString(material.identifier));
}
// pre-type base-modifier list
NBTTagList modifierList = new NBTTagList();
// we cannot set the type directly, but it gets typed by adding a tag, so we add and remove one
modifierList.appendTag(new NBTTagString());
modifierList.removeTag(0);
base.setTag(Tags.BASE_MATERIALS, materialList);
base.setTag(Tags.BASE_MODIFIERS, modifierList);
return base;
}
/**
* Builds an unusable tool that only has the rendering info
*/
public ItemStack buildItemForRendering(List<Material> materials) {
ItemStack tool = new ItemStack(this);
NBTTagCompound base = new NBTTagCompound();
base.setTag(Tags.BASE_DATA, buildData(materials));
tool.setTagCompound(base);
return tool;
}
public ItemStack buildItemForRenderingInGui() {
List<Material> materials = IntStream.range(0, getRequiredComponents().size())
.mapToObj(this::getMaterialForPartForGuiRendering)
.collect(Collectors.toList());
return buildItemForRendering(materials);
}
@SideOnly(Side.CLIENT)
public Material getMaterialForPartForGuiRendering(int index) {
return ClientProxy.RenderMaterials[index % ClientProxy.RenderMaterials.length];
}
public abstract NBTTagCompound buildTag(List<Material> materials);
/** Checks whether an Item built from materials has only valid materials. Uses the standard NBT to determine materials. */
public boolean hasValidMaterials(ItemStack stack) {
// checks if the materials used support all stats needed
NBTTagList list = TagUtil.getBaseMaterialsTagList(stack);
List<Material> materials = TinkerUtil.getMaterialsFromTagList(list);
// something went wrooooong
if(materials.size() != requiredComponents.length) {
return false;
}
// check if all materials used have the stats needed
for(int i = 0; i < materials.size(); i++) {
Material material = materials.get(i);
PartMaterialType required = requiredComponents[i];
if(!required.isValidMaterial(material)) {
return false;
}
}
return true;
}
public void addMaterialTraits(NBTTagCompound root, List<Material> materials) {
int size = requiredComponents.length;
// safety
if(materials.size() < size) {
size = materials.size();
}
// add corresponding traits per material usage
for(int i = 0; i < size; i++) {
PartMaterialType required = requiredComponents[i];
Material material = materials.get(i);
for(ITrait trait : required.getApplicableTraitsForMaterial(material)) {
ToolBuilder.addTrait(root, trait, material.materialTextColor);
}
}
}
/* Repairing */
/** Returns indices of the parts that are used for repairing */
public int[] getRepairParts() {
return new int[]{1}; // index 1 usually is the head. 0 is handle.
}
public float getRepairModifierForPart(int index) {
return 1f;
}
@Override
public ItemStack repair(ItemStack repairable, ItemStack[] repairItems) {
if(repairable.getItemDamage() == 0 && !ToolHelper.isBroken(repairable)) {
// undamaged and not broken - no need to repair
return null;
}
// we assume the first required part exclusively determines repair material
List<Material> materials = TinkerUtil.getMaterialsFromTagList(TagUtil.getBaseMaterialsTagList(repairable));
if(materials.isEmpty()) {
return null;
}
// ensure the items only contain valid items
ItemStack[] items = Util.copyItemStackArray(repairItems);
boolean foundMatch = false;
for(int index : getRepairParts()) {
Material material = materials.get(index);
if(repairCustom(material, items) > 0) {
foundMatch = true;
}
RecipeMatch.Match match = material.matches(items);
// not a single match -> nothing to repair with
if(match == null) {
continue;
}
foundMatch = true;
while((match = material.matches(items)) != null) {
RecipeMatch.removeMatch(items, match);
}
}
if(!foundMatch) {
return null;
}
// check if all items were used
for(int i = 0; i < repairItems.length; i++) {
// was non-null and did not get modified (stacksize changed or null now, usually)
if(repairItems[i] != null && ItemStack.areItemStacksEqual(repairItems[i], items[i])) {
// found an item that was not touched
return null;
}
}
// now do it all over again with the real items, to actually repair \o/
ItemStack item = repairable.copy();
while(item.getItemDamage() > 0) {
int amount = calculateRepairAmount(materials, repairItems);
// nothing to repair with, we're therefore done
if(amount <= 0) {
break;
}
ToolHelper.repairTool(item, calculateRepair(item, amount));
// save that we repaired it :I
NBTTagCompound tag = TagUtil.getExtraTag(item);
TagUtil.addInteger(tag, Tags.REPAIR_COUNT, 1);
TagUtil.setExtraTag(item, tag);
}
return item;
}
/** Allows for custom repair items. Remove used items from the array. */
protected int repairCustom(Material material, ItemStack[] repairItems) {
return 0;
}
protected int calculateRepairAmount(List<Material> materials, ItemStack[] repairItems) {
Set<Material> materialsMatched = Sets.newHashSet();
float durability = 0f;
// try to match each material once
for(int index : getRepairParts()) {
Material material = materials.get(index);
if(materialsMatched.contains(material)) {
continue;
}
// custom repairing
durability += repairCustom(material, repairItems) * getRepairModifierForPart(index);
RecipeMatch.Match match = material.matches(repairItems);
if(match != null) {
HeadMaterialStats stats = material.getStats(MaterialTypes.HEAD);
if(stats != null) {
materialsMatched.add(material);
durability += ((float) stats.durability * (float) match.amount * getRepairModifierForPart(index)) / 144f;
RecipeMatch.removeMatch(repairItems, match);
}
}
}
durability *= 1f + ((float) materialsMatched.size() - 1) / 9f;
return (int) durability;
}
protected int calculateRepair(ItemStack tool, int amount) {
float origDur = TagUtil.getOriginalToolStats(tool).durability;
float actualDur = ToolHelper.getDurabilityStat(tool);
// calculate in modifiers that change the total durability of a tool, like diamond
// they should not punish the player with higher repair costs
float durabilityFactor = actualDur / origDur;
float increase = (float) amount * Math.min(10f, durabilityFactor);
increase = Math.max(increase, actualDur / 64f);
//increase = Math.max(50, increase);
int modifiersUsed = TagUtil.getBaseModifiersUsed(tool.getTagCompound());
float mods = 1.0f;
if(modifiersUsed == 1) {
mods = 0.95f;
}
else if(modifiersUsed == 2) {
mods = 0.9f;
}
else if(modifiersUsed >= 3) {
mods = 0.85f;
}
increase *= mods;
NBTTagCompound tag = TagUtil.getExtraTag(tool);
int repair = tag.getInteger(Tags.REPAIR_COUNT);
float repairDimishingReturns = (100 - repair / 2) / 100f;
if(repairDimishingReturns < 0.5f) {
repairDimishingReturns = 0.5f;
}
increase *= repairDimishingReturns;
return (int) Math.ceil(increase);
}
/* Information */
@Override
public void addInformation(ItemStack stack, EntityPlayer playerIn, List<String> tooltip,
boolean advanced) {
boolean shift = Util.isShiftKeyDown();
boolean ctrl = Util.isCtrlKeyDown();
// modifiers
if(!shift && !ctrl) {
getTooltip(stack, tooltip);
tooltip.add("");
// info tooltip for detailed and componend info
tooltip.add(Util.translate("tooltip.tool.holdShift"));
tooltip.add(Util.translate("tooltip.tool.holdCtrl"));
tooltip.add(TextFormatting.BLUE +
I18n.translateToLocalFormatted("attribute.modifier.plus.0",
Util.df.format(ToolHelper.getActualDamage(stack, playerIn)),
I18n
.translateToLocal("attribute.name.generic.attackDamage")));
}
// detailed data
else if(Config.extraTooltips && shift) {
getTooltipDetailed(stack, tooltip);
}
// component data
else if(Config.extraTooltips && ctrl) {
getTooltipComponents(stack, tooltip);
}
}
@Override
public void getTooltip(ItemStack stack, List<String> tooltips) {
// Default tooltip: modifiers
TooltipBuilder.addModifierTooltips(stack,tooltips);
}
@Nonnull
@Override
public EnumRarity getRarity(ItemStack stack) {
// prevents enchanted items to have a different name color
return EnumRarity.COMMON;
}
@Override
public boolean isBookEnchantable(ItemStack stack, ItemStack book) {
return false;
}
/* NBT loading */
@Override
public boolean updateItemStackNBT(NBTTagCompound nbt) {
// when the itemstack is loaded from NBT we recalculate all the data
if(nbt.hasKey(Tags.BASE_DATA)) {
try {
ToolBuilder.rebuildTool(nbt, this);
} catch(TinkerGuiException e) {
// nothing to do
}
}
// return value shouldn't matter since it's never checked
return true;
}
}