package slimeknights.tconstruct.library.tools;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.attributes.AttributeModifier;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
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.materials.ExtraMaterialStats;
import slimeknights.tconstruct.library.materials.HandleMaterialStats;
import slimeknights.tconstruct.library.materials.HeadMaterialStats;
import slimeknights.tconstruct.library.materials.IMaterialStats;
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.tinkering.Category;
import slimeknights.tconstruct.library.tinkering.IToolStationDisplay;
import slimeknights.tconstruct.library.tinkering.PartMaterialType;
import slimeknights.tconstruct.library.tinkering.TinkersItem;
import slimeknights.tconstruct.library.traits.ITrait;
import slimeknights.tconstruct.library.utils.TagUtil;
import slimeknights.tconstruct.library.utils.TinkerUtil;
import slimeknights.tconstruct.library.utils.ToolHelper;
import slimeknights.tconstruct.library.utils.TooltipBuilder;
import slimeknights.tconstruct.tools.TinkerMaterials;
import slimeknights.tconstruct.tools.TinkerTools;
import slimeknights.tconstruct.tools.traits.InfiTool;
/**
* Intermediate abstraction layer for all tools/melee weapons. This class has all the callbacks for blocks and enemies
* so tools and weapons can share behaviour.
*/
public abstract class ToolCore extends TinkersItem implements IToolStationDisplay {
public final static int DEFAULT_MODIFIERS = 3;
public static final String TAG_SWITCHED_HAND_HAX = "SwitchedHand";
public ToolCore(PartMaterialType... requiredComponents) {
super(requiredComponents);
this.setCreativeTab(TinkerRegistry.tabTools);
this.setNoRepair(); // >_>
TinkerRegistry.registerTool(this);
addCategory(Category.TOOL);
}
@Override
public int getMaxDamage(ItemStack stack) {
return ToolHelper.getDurabilityStat(stack);
}
@Override
public void setDamage(ItemStack stack, int damage) {
super.setDamage(stack, damage);
if(getDamage(stack) == getMaxDamage(stack)) {
ToolHelper.breakTool(stack, null);
}
}
@Override
public boolean isDamageable() {
return true;
}
@Override
public boolean showDurabilityBar(ItemStack stack) {
return super.showDurabilityBar(stack) && !ToolHelper.isBroken(stack);
}
/* Tool and Weapon specific properties */
/**
* Multiplier applied to the actual mining speed of the tool
* Internally a hammer and pick have the same speed, but a hammer is 2/3 slower
*/
public float miningSpeedModifier() {
return 1f;
}
/** Multiplier for damage from materials. Should be fixed per tool. */
public abstract float damagePotential();
/**
* A fixed damage value where the calculations start to apply dimishing returns.
* Basically if you'd hit more than that damage with this tool, the damage is gradually reduced depending on how much the cutoff is exceeded.
*/
public float damageCutoff() {
return 15.0f; // in general this should be sufficient and only needs increasing if it's a stronger weapon
// fun fact: diamond sword with sharpness V has 15 damage
}
/**
* Allows you set the base attack speed, can be changed by modifiers. Equivalent to the vanilla attack speed.
* 4 is equal to any standard item. Value has to be greater than zero.
*/
public abstract double attackSpeed();
/**
* Knockback modifier. Basically this takes the vanilla knockback on hit and modifies it by this factor.
*/
public float knockback() {
return 1.0f;
}
/**
* Actually deal damage to the entity we hit. Can be overridden for special behaviour
*
* @return True if the entity was hit. Usually the return value of {@link Entity#attackEntityFrom(DamageSource, float)}
*/
public boolean dealDamage(ItemStack stack, EntityLivingBase player, Entity entity, float damage) {
if(player instanceof EntityPlayer) {
return entity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) player), damage);
}
return entity.attackEntityFrom(DamageSource.causeMobDamage(player), damage);
}
protected boolean readyForSpecialAttack(EntityLivingBase player) {
return player instanceof EntityPlayer && ((EntityPlayer) player).getCooledAttackStrength(0.5f) > 0.9f;
}
/**
* Called when an entity is getting damaged with the tool.
* Reduce the tools durability accordingly
* player can be null!
*/
public void reduceDurabilityOnHit(ItemStack stack, EntityPlayer player, float damage) {
damage = Math.max(1f, damage / 10f);
if(!hasCategory(Category.WEAPON)) {
damage *= 2;
}
ToolHelper.damageTool(stack, (int) damage, player);
}
@Override
public float getStrVsBlock(ItemStack stack, IBlockState state) {
if(isEffective(state) || ToolHelper.isToolEffective(stack, state)) {
return ToolHelper.calcDigSpeed(stack, state);
}
return super.getStrVsBlock(stack, state);
}
public boolean isEffective(IBlockState state) {
return false;
}
@Override
public boolean canHarvestBlock(@Nonnull IBlockState state, ItemStack stack) {
return isEffective(state);
}
@Override
public boolean onBlockStartBreak(ItemStack itemstack, BlockPos pos, EntityPlayer player) {
if(!ToolHelper.isBroken(itemstack) && this instanceof IAoeTool && ((IAoeTool) this).isAoeHarvestTool()) {
for(BlockPos extraPos : ((IAoeTool) this).getAOEBlocks(itemstack, player.getEntityWorld(), player, pos)) {
ToolHelper.breakExtraBlock(itemstack, player.getEntityWorld(), player, extraPos, pos);
}
}
// this is a really dumb hack.
// Basically when something with silktouch harvests a block from the offhand
// the game can't detect that. so we have to switch around the items in the hands for the break call
// it's switched back in onBlockDestroyed
if(DualToolHarvestUtils.shouldUseOffhand(player, pos, player.getHeldItemMainhand())) {
ItemStack off = player.getHeldItemOffhand();
switchItemsInHands(player);
// remember, off is in the mainhand now
NBTTagCompound tag = TagUtil.getTagSafe(off);
tag.setLong(TAG_SWITCHED_HAND_HAX, player.getEntityWorld().getTotalWorldTime());
off.setTagCompound(tag);
}
return super.onBlockStartBreak(itemstack, pos, player);
}
@Override
public boolean onLeftClickEntity(ItemStack stack, EntityPlayer player, Entity entity) {
return ToolHelper.attackEntity(stack, this, player, entity);
}
@Override
public boolean onEntitySwing(EntityLivingBase entityLiving, ItemStack stack) {
/*if(attackSpeed() > 0) {
int speed = Math.min(5, attackSpeed());
ToolHelper.swingItem(speed, entityLiving);
return true;
}*/
return super.onEntitySwing(entityLiving, stack);
}
@Override
public boolean hitEntity(ItemStack stack, EntityLivingBase target, EntityLivingBase attacker) {
float speed = ToolHelper.getActualAttackSpeed(stack);
int time = Math.round(20f / speed);
if(time < target.hurtResistantTime / 2) {
target.hurtResistantTime = (target.hurtResistantTime + time) / 2;
target.hurtTime = (target.hurtTime + time) / 2;
}
return super.hitEntity(stack, target, attacker);
}
@Nonnull
@Override
public Multimap<String, AttributeModifier> getAttributeModifiers(@Nonnull EntityEquipmentSlot slot, ItemStack stack) {
Multimap<String, AttributeModifier> multimap = super.getAttributeModifiers(slot, stack);
if(slot == EntityEquipmentSlot.MAINHAND && !ToolHelper.isBroken(stack)) {
multimap.put(SharedMonsterAttributes.ATTACK_DAMAGE.getName(), new AttributeModifier(ATTACK_DAMAGE_MODIFIER, "Weapon modifier", (double) ToolHelper.getActualAttack(stack), 0));
multimap.put(SharedMonsterAttributes.ATTACK_SPEED.getName(), new AttributeModifier(ATTACK_SPEED_MODIFIER, "Weapon modifier", (double) ToolHelper.getActualAttackSpeed(stack) - 4d, 0));
}
return multimap;
}
@Override
public List<String> getInformation(ItemStack stack) {
return getInformation(stack, true);
}
@Override
public void getTooltip(ItemStack stack, List<String> tooltips) {
if(ToolHelper.isBroken(stack)) {
tooltips.add("" + TextFormatting.DARK_RED + TextFormatting.BOLD + getBrokenTooltip(stack));
}
super.getTooltip(stack, tooltips);
}
protected String getBrokenTooltip(ItemStack itemStack) {
return Util.translate(TooltipBuilder.LOC_Broken);
}
@Override
public void getTooltipDetailed(ItemStack stack, List<String> tooltips) {
tooltips.addAll(getInformation(stack, false));
}
public List<String> getInformation(ItemStack stack, boolean detailed) {
TooltipBuilder info = new TooltipBuilder(stack);
info.addDurability(!detailed);
if(hasCategory(Category.HARVEST)) {
info.addHarvestLevel();
info.addMiningSpeed();
}
if(hasCategory(Category.LAUNCHER)) {
info.addDrawSpeed();
info.addRange();
info.addProjectileBonusDamage();
}
info.addAttack();
if(ToolHelper.getFreeModifiers(stack) > 0) {
info.addFreeModifiers();
}
if(detailed) {
info.addModifierInfo();
}
return info.getTooltip();
}
@Override
public void getTooltipComponents(ItemStack stack, List<String> tooltips) {
List<Material> materials = TinkerUtil.getMaterialsFromTagList(TagUtil.getBaseMaterialsTagList(stack));
List<PartMaterialType> component = getRequiredComponents();
if(materials.size() < component.size()) {
return;
}
for(int i = 0; i < component.size(); i++) {
PartMaterialType pmt = component.get(i);
Material material = materials.get(i);
// get (one possible) toolpart used to craft the thing
Iterator<IToolPart> partIter = pmt.getPossibleParts().iterator();
if(!partIter.hasNext()) {
continue;
}
IToolPart part = partIter.next();
ItemStack partStack = part.getItemstackWithMaterial(material);
if(partStack != null) {
// we have the part, add it
tooltips.add(material.getTextColor() + TextFormatting.UNDERLINE + partStack.getDisplayName());
Set<ITrait> usedTraits = Sets.newHashSet();
// find out which stats and traits it contributes and add it to the tooltip
for(IMaterialStats stats : material.getAllStats()) {
if(pmt.usesStat(stats.getIdentifier())) {
tooltips.addAll(stats.getLocalizedInfo());
for(ITrait trait : pmt.getApplicableTraitsForMaterial(material)) {
if(!usedTraits.contains(trait)) {
tooltips.add(material.getTextColor() + trait.getLocalizedName());
usedTraits.add(trait);
}
}
}
}
tooltips.add("");
}
}
}
@Nonnull
@SideOnly(Side.CLIENT)
@Override
public FontRenderer getFontRenderer(ItemStack stack) {
return ClientProxy.fontRenderer;
}
@SideOnly(Side.CLIENT)
@Override
public boolean hasEffect(ItemStack stack) {
return TagUtil.hasEnchantEffect(stack);
}
@Nonnull
@Override
public String getItemStackDisplayName(@Nonnull ItemStack stack) {
// if the tool is not named we use the repair tools for a prefix like thing
List<Material> materials = TinkerUtil.getMaterialsFromTagList(TagUtil.getBaseMaterialsTagList(stack));
// we save all the ones for the name in a set so we don't have the same material in it twice
Set<Material> nameMaterials = Sets.newLinkedHashSet();
for(int index : getRepairParts()) {
if(index < materials.size()) {
nameMaterials.add(materials.get(index));
}
}
return Material.getCombinedItemName(super.getItemStackDisplayName(stack), nameMaterials);
}
// Creative tab items
@Override
public void getSubItems(@Nonnull Item itemIn, CreativeTabs tab, List<ItemStack> subItems) {
addDefaultSubItems(subItems);
}
protected void addDefaultSubItems(List<ItemStack> subItems, Material... fixedMaterials) {
for(Material head : TinkerRegistry.getAllMaterials()) {
List<Material> mats = new ArrayList<Material>(requiredComponents.length);
for(int i = 0; i < requiredComponents.length; i++) {
if(fixedMaterials.length > i && fixedMaterials[i] != null && requiredComponents[i].isValidMaterial(fixedMaterials[i])) {
mats.add(fixedMaterials[i]);
}
else {
// todo: check for applicability with stats
mats.add(head);
}
}
ItemStack tool = buildItem(mats);
// only valid ones
if(hasValidMaterials(tool)) {
subItems.add(tool);
if(!Config.listAllMaterials) {
break;
}
}
}
}
protected void addInfiTool(List<ItemStack> subitems, String name) {
ItemStack tool = getInfiTool(name);
if(hasValidMaterials(tool)) {
subitems.add(tool);
}
}
protected ItemStack getInfiTool(String name) {
// The InfiHarvester!
List<Material> materials = ImmutableList.of(TinkerMaterials.slime, TinkerMaterials.cobalt, TinkerMaterials.ardite, TinkerMaterials.ardite);
materials = materials.subList(0, requiredComponents.length);
ItemStack tool = buildItem(materials);
InfiTool.INSTANCE.apply(tool);
tool.setStackDisplayName(name);
return tool;
}
@Override
public int getHarvestLevel(ItemStack stack, @Nonnull String toolClass) {
if(this.getToolClasses(stack).contains(toolClass)) {
// will return 0 if the tag has no info anyway
return ToolHelper.getHarvestLevelStat(stack);
}
return super.getHarvestLevel(stack, toolClass);
}
/** A simple string identifier for the tool, used for identification in texture generation etc. */
public String getIdentifier() {
return Util.getItemLocation(this).getResourcePath();
}
/** The tools name completely without material information */
@Override
public String getLocalizedToolName() {
return Util.translate(getUnlocalizedName() + ".name");
}
/** The tools name with the given material. e.g. "Wooden Pickaxe" */
public String getLocalizedToolName(Material material) {
return material.getLocalizedItemName(getLocalizedToolName());
}
/** Returns info about the Tool. Displayed in the tool stations etc. */
public String getLocalizedDescription() {
return Util.translate(getUnlocalizedName() + ".desc");
}
@Override
protected int repairCustom(Material material, ItemStack[] repairItems) {
RecipeMatch.Match match = RecipeMatch.of(TinkerTools.sharpeningKit).matches(repairItems);
if(match == null) {
return 0;
}
for(ItemStack stacks : match.stacks) {
// invalid material?
if(TinkerTools.sharpeningKit.getMaterial(stacks) != material) {
return 0;
}
}
RecipeMatch.removeMatch(repairItems, match);
HeadMaterialStats stats = material.getStats(MaterialTypes.HEAD);
float durability = stats.durability * match.amount * TinkerTools.sharpeningKit.getCost();
durability /= Material.VALUE_Ingot;
return (int) (durability);
}
/* Additional Trait callbacks */
@Override
public void onUpdate(ItemStack stack, World worldIn, Entity entityIn, int itemSlot, boolean isSelected) {
super.onUpdate(stack, worldIn, entityIn, itemSlot, isSelected);
onUpdateTraits(stack, worldIn, entityIn, itemSlot, isSelected);
}
protected void onUpdateTraits(ItemStack stack, World worldIn, Entity entityIn, int itemSlot, boolean isSelected) {
NBTTagList list = TagUtil.getTraitsTagList(stack);
for(int i = 0; i < list.tagCount(); i++) {
ITrait trait = TinkerRegistry.getTrait(list.getStringTagAt(i));
if(trait != null) {
trait.onUpdate(stack, worldIn, entityIn, itemSlot, isSelected);
}
}
}
@Override
public boolean onBlockDestroyed(ItemStack stack, World worldIn, IBlockState state, BlockPos pos, EntityLivingBase entityLiving) {
// move item back into offhand. See onBlockBreakStart
if(stack != null && entityLiving != null && stack.hasTagCompound()) {
NBTTagCompound tag = stack.getTagCompound();
if(tag.getLong(TAG_SWITCHED_HAND_HAX) == entityLiving.getEntityWorld().getTotalWorldTime()) {
tag.removeTag(TAG_SWITCHED_HAND_HAX);
stack.setTagCompound(tag);
switchItemsInHands(entityLiving);
}
}
if(ToolHelper.isBroken(stack)) {
return false;
}
boolean effective = isEffective(state) || ToolHelper.isToolEffective(stack, worldIn.getBlockState(pos));
int damage = effective ? 1 : 2;
afterBlockBreak(stack, worldIn, state, pos, entityLiving, damage, effective);
return hasCategory(Category.TOOL);
}
protected void switchItemsInHands(EntityLivingBase entityLiving) {
ItemStack main = entityLiving.getHeldItemMainhand();
ItemStack off = entityLiving.getHeldItemOffhand();
entityLiving.setHeldItem(EnumHand.OFF_HAND, main);
entityLiving.setHeldItem(EnumHand.MAIN_HAND, off);
}
public void afterBlockBreak(ItemStack stack, World world, IBlockState state, BlockPos pos, EntityLivingBase player, int damage, boolean wasEffective) {
NBTTagList list = TagUtil.getTraitsTagList(stack);
for(int i = 0; i < list.tagCount(); i++) {
ITrait trait = TinkerRegistry.getTrait(list.getStringTagAt(i));
if(trait != null) {
trait.afterBlockBreak(stack, world, state, pos, player, wasEffective);
}
}
ToolHelper.damageTool(stack, damage, player);
}
// elevate to public
@Override
public RayTraceResult rayTrace(@Nonnull World worldIn, @Nonnull EntityPlayer playerIn, boolean useLiquids) {
return super.rayTrace(worldIn, playerIn, useLiquids);
}
protected void preventSlowDown(Entity entityIn, float originalSpeed) {
TinkerTools.proxy.preventPlayerSlowdown(entityIn, originalSpeed, this);
}
@Override
public boolean shouldCauseBlockBreakReset(ItemStack oldStack, ItemStack newStack) {
return shouldCauseReequipAnimation(oldStack, newStack, false);
}
@SideOnly(Side.CLIENT)
@Override
public boolean shouldCauseReequipAnimation(ItemStack oldStack, @Nonnull ItemStack newStack, boolean slotChanged) {
if(TagUtil.getResetFlag(newStack)) {
TagUtil.setResetFlag(newStack, false);
return true;
}
if(oldStack == newStack) {
return false;
}
if(slotChanged) {
return true;
}
if(oldStack.hasEffect() != newStack.hasEffect()) {
return true;
}
Multimap<String, AttributeModifier> attributesNew = newStack.getAttributeModifiers(EntityEquipmentSlot.MAINHAND);
Multimap<String, AttributeModifier> attributesOld = oldStack.getAttributeModifiers(EntityEquipmentSlot.MAINHAND);
if(attributesNew.size() != attributesOld.size()) {
return true;
}
for(String key : attributesOld.keySet()) {
if(!attributesNew.containsKey(key)) {
return true;
}
Iterator<AttributeModifier> iter1 = attributesNew.get(key).iterator();
Iterator<AttributeModifier> iter2 = attributesOld.get(key).iterator();
while(iter1.hasNext() && iter2.hasNext()) {
if(!iter1.next().equals(iter2.next())) {
return true;
}
}
}
if(oldStack.getItem() == newStack.getItem() && newStack.getItem() instanceof ToolCore) {
return !isEqualTinkersItem(oldStack, newStack);
}
return !ItemStack.areItemStacksEqual(oldStack, newStack);
}
/**
* Builds a default tool from:
* 1. Handle
* 2. Head
* 3. Accessoire (if present)
*/
protected ToolNBT buildDefaultTag(List<Material> materials) {
ToolNBT data = new ToolNBT();
if(materials.size() >= 2) {
HandleMaterialStats handle = materials.get(0).getStatsOrUnknown(MaterialTypes.HANDLE);
HeadMaterialStats head = materials.get(1).getStatsOrUnknown(MaterialTypes.HEAD);
// start with head
data.head(head);
// add in accessoires if present
if(materials.size() >= 3) {
ExtraMaterialStats binding = materials.get(2).getStatsOrUnknown(MaterialTypes.EXTRA);
data.extra(binding);
}
// calculate handle impact
data.handle(handle);
}
// 3 free modifiers
data.modifiers = DEFAULT_MODIFIERS;
return data;
}
public static boolean isEqualTinkersItem(ItemStack item1, ItemStack item2) {
if(item1 == null || item2 == null || item1.getItem() != item2.getItem()) {
return false;
}
if(!(item1.getItem() instanceof ToolCore)) {
return false;
}
NBTTagCompound tag1 = TagUtil.getTagSafe(item1);
NBTTagCompound tag2 = TagUtil.getTagSafe(item2);
NBTTagList mods1 = TagUtil.getModifiersTagList(tag1);
NBTTagList mods2 = TagUtil.getModifiersTagList(tag2);
if(mods1.tagCount() != mods1.tagCount()) {
return false;
}
// check modifiers
for(int i = 0; i < mods1.tagCount(); i++) {
NBTTagCompound tag = mods1.getCompoundTagAt(i);
ModifierNBT data = ModifierNBT.readTag(tag);
IModifier modifier = TinkerRegistry.getModifier(data.identifier);
if(modifier != null && !modifier.equalModifier(tag, mods2.getCompoundTagAt(i))) {
return false;
}
}
return TagUtil.getBaseMaterialsTagList(tag1).equals(TagUtil.getBaseMaterialsTagList(tag2)) && // materials used
TagUtil.getBaseModifiersUsed(tag1) == TagUtil.getBaseModifiersUsed(tag2) && // number of free modifiers used
TagUtil.getOriginalToolStats(tag1).equals(TagUtil.getOriginalToolStats(tag2)); // unmodified base stats
}
}