diff --git a/src/main/java/fathertoast/specialmobs/client/FishingRodItemPropertyGetter.java b/src/main/java/fathertoast/specialmobs/client/FishingRodItemPropertyGetter.java new file mode 100644 index 0000000..c0fa0c0 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/client/FishingRodItemPropertyGetter.java @@ -0,0 +1,34 @@ +package fathertoast.specialmobs.client; + +import fathertoast.specialmobs.common.entity.ai.IAngler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.FishingRodItem; +import net.minecraft.item.IItemPropertyGetter; +import net.minecraft.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Item property getter override that allows the fishing rod item animation to function for any entity implementing IAngler. + */ +@OnlyIn( Dist.CLIENT ) +public class FishingRodItemPropertyGetter implements IItemPropertyGetter { + + @Override + public float call( @Nonnull ItemStack stack, @Nullable ClientWorld world, @Nullable LivingEntity entity ) { + if( entity == null ) return 0.0F; + + boolean inMainHand = entity.getMainHandItem() == stack; + boolean inOffhand = entity.getOffhandItem() == stack && !(entity.getMainHandItem().getItem() instanceof FishingRodItem); + + return (inMainHand || inOffhand) && ( + entity instanceof PlayerEntity && ((PlayerEntity) entity).fishing != null || + entity instanceof IAngler && ((IAngler) entity).isLineOut() // Line added to vanilla logic + ) ? 1.0F : 0.0F; + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java index 81ad2a3..7ac023b 100644 --- a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java +++ b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java @@ -68,6 +68,7 @@ public class SpecialMobs { * o ranged attack AI * - silverfish * ? ranged attack AI + * + puffer * - endermen * o witches * o ability to equip held items diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/AIHelper.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/AIHelper.java index 3322dcb..7a335d3 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/ai/AIHelper.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/AIHelper.java @@ -11,7 +11,7 @@ import java.util.ArrayList; */ public final class AIHelper { - /** Inserts an AI goal at the specified priority. Existing goals have their priority adjusted accordingly. */ + /** Inserts an AI goal at the specified priority. Existing goals have their priority incremented accordingly. */ public static void insertGoal( GoalSelector ai, int priority, Goal goal ) { for( PrioritizedGoal task : new ArrayList<>( ai.availableGoals ) ) { if( task.getPriority() >= priority ) task.priority++; @@ -19,6 +19,14 @@ public final class AIHelper { ai.addGoal( priority, goal ); } + /** Inserts an AI goal at the specified priority. Existing goals have their priority decremented accordingly. */ + public static void insertGoalReverse( GoalSelector ai, int priority, Goal goal ) { + for( PrioritizedGoal task : new ArrayList<>( ai.availableGoals ) ) { + if( task.getPriority() <= priority ) task.priority--; + } + ai.addGoal( priority, goal ); + } + /** Removes all goals with the specified priority. */ public static void removeGoals( GoalSelector ai, int priority ) { for( PrioritizedGoal task : new ArrayList<>( ai.availableGoals ) ) { diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/IAngler.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/IAngler.java new file mode 100644 index 0000000..2dd1f1e --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/IAngler.java @@ -0,0 +1,14 @@ +package fathertoast.specialmobs.common.entity.ai; + +/** + * Monsters must implement this interface to shoot fish hooks. + * This allows get and set methods for the fish hook so that the server can communicate rendering info to the client. + */ +public interface IAngler { + + /** Sets this angler's line as out (or in). */ + void setLineOut( boolean value ); + + /** @return Whether this angler's line is out. */ + boolean isLineOut(); +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/INinja.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/INinja.java new file mode 100644 index 0000000..529149a --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/INinja.java @@ -0,0 +1,25 @@ +package fathertoast.specialmobs.common.entity.ai; + +import net.minecraft.block.BlockState; + +import javax.annotation.Nullable; + +/** + * Monsters must implement this interface to use the ninja goal AI. + * This allows get and set methods for the disguise block and immobile state. + */ +public interface INinja { + + /** @return Whether this ninja is currently immobile. */ + boolean isImmobile(); + + /** Sets this ninja's immovable state. When activated, the entity is 'snapped' to the nearest block position. */ + void setImmobile( boolean value ); + + /** @return The block being hidden (rendered) as, or null if not hiding. */ + @Nullable + BlockState getDisguiseBlock(); + + /** Sets the block being hidden (rendered) as, set to null to cancel hiding. */ + void setDisguiseBlock( @Nullable BlockState block ); +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/projectile/BugSpitEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/projectile/BugSpitEntity.java new file mode 100644 index 0000000..7ca4d83 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/projectile/BugSpitEntity.java @@ -0,0 +1,14 @@ +package fathertoast.specialmobs.common.entity.projectile; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.world.World; + +public abstract class BugSpitEntity extends Entity { + + public BugSpitEntity( EntityType entityType, World world ) { + super( entityType, world ); + } + + //TODO +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/projectile/SpecialFishHookEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/projectile/SpecialFishHookEntity.java new file mode 100644 index 0000000..df85c43 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/projectile/SpecialFishHookEntity.java @@ -0,0 +1,14 @@ +package fathertoast.specialmobs.common.entity.projectile; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.world.World; + +public abstract class SpecialFishHookEntity extends Entity { + + public SpecialFishHookEntity( EntityType entityType, World world ) { + super( entityType, world ); + } + + //TODO +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java new file mode 100644 index 0000000..8bf7f15 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java @@ -0,0 +1,99 @@ +package fathertoast.specialmobs.common.entity.silverfish; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.entity.ai.IAngler; +import fathertoast.specialmobs.common.util.AttributeHelper; +import fathertoast.specialmobs.common.util.References; +import fathertoast.specialmobs.datagen.loot.LootEntryItemBuilder; +import fathertoast.specialmobs.datagen.loot.LootPoolBuilder; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.item.Items; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class FishingSilverfishEntity extends _SpecialSilverfishEntity implements IAngler { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + entityType.sized( 0.5F, 0.4F ); + return new BestiaryInfo( 0x2D41F4 ); + //TODO theme - fishing + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return AttributeHelper.of( _SpecialSilverfishEntity.createAttributes() ) + .addAttribute( Attributes.MAX_HEALTH, 4.0 ) + .multAttribute( Attributes.MOVEMENT_SPEED, 0.9 ) + .build(); + } + + @SpecialMob.LanguageProvider + public static String[] getTranslations( String langKey ) { + return References.translations( langKey, "Fishing Silverfish", + "", "", "", "", "", "" );//TODO + } + + @SpecialMob.LootTableProvider + public static void buildLootTable( LootTableBuilder loot ) { + addBaseLoot( loot ); + loot.addPool( new LootPoolBuilder( "common" ) + .addEntry( new LootEntryItemBuilder( Items.COD ).setCount( 0, 2 ).addLootingBonus( 0, 1 ).smeltIfBurning().toLootEntry() ) + .toLootPool() ); + } + + @SpecialMob.Constructor + public FishingSilverfishEntity( EntityType entityType, World world ) { + super( entityType, world ); + getSpecialData().setBaseScale( 1.2F ); + getSpecialData().setCanBreatheInWater( true ); + getSpecialData().setIgnoreWaterPush( true ); + xpReward += 2; + } + + + //--------------- Variant-Specific Implementations ---------------- + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + getSpecialData().rangedAttackSpread = 10.0F; + getSpecialData().rangedAttackCooldown = 32; + getSpecialData().rangedAttackMaxCooldown = 48; + getSpecialData().rangedAttackMaxRange = 10.0F; + + //TODO add angler AI @ 4 + } + + private static final ResourceLocation[] TEXTURES = { + GET_TEXTURE_PATH( "fishing" ) + }; + + /** @return All default textures for this entity. */ + @Override + public ResourceLocation[] getDefaultTextures() { return TEXTURES; } + + + //--------------- IAngler Implementations ---------------- + + /** Sets this angler's line as out (or in). */ + @Override + public void setLineOut( boolean value ) { } + + /** @return Whether this angler's line is out. */ + @Override + public boolean isLineOut() { return false; } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/skeleton/NinjaSkeletonEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/skeleton/NinjaSkeletonEntity.java new file mode 100644 index 0000000..1813c6d --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/skeleton/NinjaSkeletonEntity.java @@ -0,0 +1,222 @@ +package fathertoast.specialmobs.common.entity.skeleton; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.entity.ai.INinja; +import fathertoast.specialmobs.common.util.AttributeHelper; +import fathertoast.specialmobs.common.util.References; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.ActionResultType; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Hand; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.World; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Optional; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class NinjaSkeletonEntity extends _SpecialSkeletonEntity implements INinja { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + return new BestiaryInfo( 0x333366 ); + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return AttributeHelper.of( _SpecialSkeletonEntity.createAttributes() ) + .multAttribute( Attributes.MOVEMENT_SPEED, 1.2 ) + .build(); + } + + @SpecialMob.LanguageProvider + public static String[] getTranslations( String langKey ) { + return References.translations( langKey, "Skeleton Ninja", + "", "", "", "", "", "" );//TODO + } + + @SpecialMob.LootTableProvider + public static void buildLootTable( LootTableBuilder loot ) { + addBaseLoot( loot ); + loot.addUncommonDrop( "uncommon", Blocks.INFESTED_STONE, Blocks.INFESTED_COBBLESTONE, Blocks.INFESTED_STONE_BRICKS, + Blocks.INFESTED_CRACKED_STONE_BRICKS, Blocks.INFESTED_MOSSY_STONE_BRICKS, Blocks.INFESTED_CHISELED_STONE_BRICKS ); + } + + @SpecialMob.Constructor + public NinjaSkeletonEntity( EntityType entityType, World world ) { + super( entityType, world ); + xpReward += 2; + } + + + //--------------- Variant-Specific Implementations ---------------- + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + setRangedAI( 1.0, 10, 9.0F ); + + //TODO AIHelper.insertGoalReverse( goalSelector, getVariantAttackPriority() - 1, null ); + } + + /** Called during spawn finalization to set starting equipment. */ + @Override + protected void populateDefaultEquipmentSlots( DifficultyInstance difficulty ) { + super.populateDefaultEquipmentSlots( difficulty ); + setCanPickUpLoot( true ); + } + + /** Override to change this entity's chance to spawn with a melee weapon. */ + @Override + protected double getVariantMeleeChance() { return 0.5; } + + /** Override to apply effects when this entity hits a target with a melee attack. */ + @Override + protected void onVariantAttack( Entity target ) { + revealTo( target ); + } + + /** @return Attempts to damage this entity; returns true if the hit was successful. */ + @Override + public boolean hurt( DamageSource source, float amount ) { + if( super.hurt( source, amount ) ) { + revealTo( source.getEntity() ); + return true; + } + return false; + } + + /** @return Interacts (right click) with this entity and returns the result. */ + @Override + public ActionResultType mobInteract( PlayerEntity player, Hand hand ) { + // Attack if the player tries to right click the "block" + if( !level.isClientSide() && getDisguiseBlock() != null ) revealTo( player ); + return super.mobInteract( player, hand ); + } + + /** Called each tick to update this entity. */ + @Override + public void tick() { + // TODO can this be moved to the ninja AI? + if( !level.isClientSide() ) { + if( canHide ) { + //EntityAINinja.startHiding( this ); TODO + } + else if( onGround && getDisguiseBlock() == null && + (getTarget() == null || getTarget() instanceof PlayerEntity && ((PlayerEntity) getTarget()).isCreative()) ) { + canHide = true; + } + } + super.tick(); + } + + // // Moves this entity. + // @Override TODO + // public void move( MoverType type, double x, double y, double z ) { + // if( isImmobile() && type != MoverType.PISTON ) { + // motionY = 0.0; + // } + // else { + // super.move( type, x, y, z ); + // } + // } + // + // // Returns true if this entity should push and be pushed by other entities when colliding. + // @Override + // public boolean canBePushed() { return !isImmobile(); } + + /** Sets this entity on fire for a specific duration. */ + @Override + public void setRemainingFireTicks( int ticks ) { + if( !isImmobile() ) super.setRemainingFireTicks( ticks ); + } + + /** Reveals this ninja and sets its target so that it doesn't immediately re-disguise itself. */ + public void revealTo( @Nullable Entity target ) { + setDisguiseBlock( null ); + if( target instanceof LivingEntity ) setTarget( (LivingEntity) target ); + } + + private static final ResourceLocation[] TEXTURES = { + new ResourceLocation( "textures/entity/skeleton/skeleton.png" ), + null, + GET_TEXTURE_PATH( "ninja_overlay" ) + }; + + /** @return All default textures for this entity. */ + @Override + public ResourceLocation[] getDefaultTextures() { return TEXTURES; } + + + //--------------- INinja Implementations ---------------- + + /** The parameter for the ninja immobile state. */ + private static final DataParameter IS_HIDING = EntityDataManager.defineId( NinjaSkeletonEntity.class, DataSerializers.BOOLEAN ); + /** The parameter for the ninja disguise block. */ + private static final DataParameter> HIDING_BLOCK = EntityDataManager.defineId( NinjaSkeletonEntity.class, DataSerializers.BLOCK_STATE ); + + private boolean canHide = true; + + /** Called from the Entity.class constructor to define data watcher variables. */ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + entityData.define( IS_HIDING, false ); + entityData.define( HIDING_BLOCK, Optional.empty() ); + } + + /** @return Whether this ninja is currently immobile. */ + @Override + public boolean isImmobile() { return getEntityData().get( IS_HIDING ); } + + /** Sets this ninja's immovable state. When activated, the entity is 'snapped' to the nearest block position. */ + @Override + public void setImmobile( boolean value ) { + if( value != isImmobile() ) { + getEntityData().set( IS_HIDING, value ); + if( value ) { + clearFire(); + moveTo( Math.floor( getX() ) + 0.5, Math.floor( getY() ), Math.floor( getZ() ) + 0.5 ); + } + } + } + + /** @return The block being hidden (rendered) as, or null if not hiding. */ + @Nullable + @Override + public BlockState getDisguiseBlock() { + if( isAlive() ) return getEntityData().get( HIDING_BLOCK ).orElse( null ); + return null; + } + + /** Sets the block being hidden (rendered) as, set to null to cancel hiding. */ + @Override + public void setDisguiseBlock( @Nullable BlockState block ) { + getEntityData().set( HIDING_BLOCK, Optional.ofNullable( block ) ); + canHide = false; + + // Smoke puff when emerging from disguise + if( block == null ) { + //spawnExplosionParticle(); TODO + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/witherskeleton/NinjaWitherSkeletonEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/witherskeleton/NinjaWitherSkeletonEntity.java new file mode 100644 index 0000000..e2b72bf --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/witherskeleton/NinjaWitherSkeletonEntity.java @@ -0,0 +1,222 @@ +package fathertoast.specialmobs.common.entity.witherskeleton; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.entity.ai.INinja; +import fathertoast.specialmobs.common.util.AttributeHelper; +import fathertoast.specialmobs.common.util.References; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.ActionResultType; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Hand; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.World; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Optional; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class NinjaWitherSkeletonEntity extends _SpecialWitherSkeletonEntity implements INinja { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + return new BestiaryInfo( 0x333366 ); + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return AttributeHelper.of( _SpecialWitherSkeletonEntity.createAttributes() ) + .multAttribute( Attributes.MOVEMENT_SPEED, 1.2 ) + .build(); + } + + @SpecialMob.LanguageProvider + public static String[] getTranslations( String langKey ) { + return References.translations( langKey, "Wither Skeleton Ninja", + "", "", "", "", "", "" );//TODO + } + + @SpecialMob.LootTableProvider + public static void buildLootTable( LootTableBuilder loot ) { + addBaseLoot( loot ); + loot.addUncommonDrop( "uncommon", Blocks.INFESTED_STONE, Blocks.INFESTED_COBBLESTONE, Blocks.INFESTED_STONE_BRICKS, + Blocks.INFESTED_CRACKED_STONE_BRICKS, Blocks.INFESTED_MOSSY_STONE_BRICKS, Blocks.INFESTED_CHISELED_STONE_BRICKS ); + } + + @SpecialMob.Constructor + public NinjaWitherSkeletonEntity( EntityType entityType, World world ) { + super( entityType, world ); + xpReward += 2; + } + + + //--------------- Variant-Specific Implementations ---------------- + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + setRangedAI( 1.0, 10, 9.0F ); + + //TODO AIHelper.insertGoalReverse( goalSelector, getVariantAttackPriority() - 1, null ); + } + + /** Called during spawn finalization to set starting equipment. */ + @Override + protected void populateDefaultEquipmentSlots( DifficultyInstance difficulty ) { + super.populateDefaultEquipmentSlots( difficulty ); + setCanPickUpLoot( true ); + } + + /** Override to change this entity's chance to spawn with a bow. */ + @Override + protected double getVariantBowChance() { return 0.5; } + + /** Override to apply effects when this entity hits a target with a melee attack. */ + @Override + protected void onVariantAttack( Entity target ) { + revealTo( target ); + } + + /** @return Attempts to damage this entity; returns true if the hit was successful. */ + @Override + public boolean hurt( DamageSource source, float amount ) { + if( super.hurt( source, amount ) ) { + revealTo( source.getEntity() ); + return true; + } + return false; + } + + /** @return Interacts (right click) with this entity and returns the result. */ + @Override + public ActionResultType mobInteract( PlayerEntity player, Hand hand ) { + // Attack if the player tries to right click the "block" + if( !level.isClientSide() && getDisguiseBlock() != null ) revealTo( player ); + return super.mobInteract( player, hand ); + } + + /** Called each tick to update this entity. */ + @Override + public void tick() { + // TODO can this be moved to the ninja AI? + if( !level.isClientSide() ) { + if( canHide ) { + //EntityAINinja.startHiding( this ); TODO + } + else if( onGround && getDisguiseBlock() == null && + (getTarget() == null || getTarget() instanceof PlayerEntity && ((PlayerEntity) getTarget()).isCreative()) ) { + canHide = true; + } + } + super.tick(); + } + + // // Moves this entity. + // @Override TODO + // public void move( MoverType type, double x, double y, double z ) { + // if( isImmobile() && type != MoverType.PISTON ) { + // motionY = 0.0; + // } + // else { + // super.move( type, x, y, z ); + // } + // } + // + // // Returns true if this entity should push and be pushed by other entities when colliding. + // @Override + // public boolean canBePushed() { return !isImmobile(); } + + /** Sets this entity on fire for a specific duration. */ + @Override + public void setRemainingFireTicks( int ticks ) { + if( !isImmobile() ) super.setRemainingFireTicks( ticks ); + } + + /** Reveals this ninja and sets its target so that it doesn't immediately re-disguise itself. */ + public void revealTo( @Nullable Entity target ) { + setDisguiseBlock( null ); + if( target instanceof LivingEntity ) setTarget( (LivingEntity) target ); + } + + private static final ResourceLocation[] TEXTURES = { + new ResourceLocation( "textures/entity/skeleton/skeleton.png" ), + null, + GET_TEXTURE_PATH( "ninja_overlay" ) + }; + + /** @return All default textures for this entity. */ + @Override + public ResourceLocation[] getDefaultTextures() { return TEXTURES; } + + + //--------------- INinja Implementations ---------------- + + /** The parameter for the ninja immobile state. */ + private static final DataParameter IS_HIDING = EntityDataManager.defineId( NinjaWitherSkeletonEntity.class, DataSerializers.BOOLEAN ); + /** The parameter for the ninja disguise block. */ + private static final DataParameter> HIDING_BLOCK = EntityDataManager.defineId( NinjaWitherSkeletonEntity.class, DataSerializers.BLOCK_STATE ); + + private boolean canHide = true; + + /** Called from the Entity.class constructor to define data watcher variables. */ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + entityData.define( IS_HIDING, false ); + entityData.define( HIDING_BLOCK, Optional.empty() ); + } + + /** @return Whether this ninja is currently immobile. */ + @Override + public boolean isImmobile() { return getEntityData().get( IS_HIDING ); } + + /** Sets this ninja's immovable state. When activated, the entity is 'snapped' to the nearest block position. */ + @Override + public void setImmobile( boolean value ) { + if( value != isImmobile() ) { + getEntityData().set( IS_HIDING, value ); + if( value ) { + clearFire(); + moveTo( Math.floor( getX() ) + 0.5, Math.floor( getY() ), Math.floor( getZ() ) + 0.5 ); + } + } + } + + /** @return The block being hidden (rendered) as, or null if not hiding. */ + @Nullable + @Override + public BlockState getDisguiseBlock() { + if( isAlive() ) return getEntityData().get( HIDING_BLOCK ).orElse( null ); + return null; + } + + /** Sets the block being hidden (rendered) as, set to null to cancel hiding. */ + @Override + public void setDisguiseBlock( @Nullable BlockState block ) { + getEntityData().set( HIDING_BLOCK, Optional.ofNullable( block ) ); + canHide = false; + + // Smoke puff when emerging from disguise + if( block == null ) { + //spawnExplosionParticle(); TODO + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java new file mode 100644 index 0000000..b349ba5 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java @@ -0,0 +1,141 @@ +package fathertoast.specialmobs.common.entity.zombie; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.entity.ai.IAngler; +import fathertoast.specialmobs.common.util.AttributeHelper; +import fathertoast.specialmobs.common.util.References; +import fathertoast.specialmobs.datagen.loot.LootEntryItemBuilder; +import fathertoast.specialmobs.datagen.loot.LootHelper; +import fathertoast.specialmobs.datagen.loot.LootPoolBuilder; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.inventory.EquipmentSlotType; +import net.minecraft.item.FishingRodItem; +import net.minecraft.item.IDyeableArmorItem; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.World; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class FishingZombieEntity extends _SpecialZombieEntity implements IAngler { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + return new BestiaryInfo( 0x2D41F4 ); + //TODO theme - fishing + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return AttributeHelper.of( _SpecialZombieEntity.createAttributes() ) + .multAttribute( Attributes.MOVEMENT_SPEED, 0.8 ) + .build(); + } + + @SpecialMob.LanguageProvider + public static String[] getTranslations( String langKey ) { + return References.translations( langKey, "Fishing Zombie", + "", "", "", "", "", "" );//TODO + } + + @SpecialMob.LootTableProvider + public static void buildLootTable( LootTableBuilder loot ) { + addBaseLoot( loot ); + loot.addPool( new LootPoolBuilder( "common" ) + .addEntry( new LootEntryItemBuilder( Items.COD ).setCount( 0, 2 ).addLootingBonus( 0, 1 ).smeltIfBurning().toLootEntry() ) + .toLootPool() ); + loot.addPool( new LootPoolBuilder( "rare" ).addConditions( LootHelper.RARE_CONDITIONS ) + .addEntry( new LootEntryItemBuilder( Items.FISHING_ROD ).enchant( 30, true ).toLootEntry() ) + .toLootPool() ); + } + + @SpecialMob.Constructor + public FishingZombieEntity( EntityType entityType, World world ) { + super( entityType, world ); + getSpecialData().setCanBreatheInWater( true ); + getSpecialData().setIgnoreWaterPush( true ); + xpReward += 2; + } + + + //--------------- Variant-Specific Implementations ---------------- + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + getSpecialData().rangedAttackSpread = 10.0F; + getSpecialData().rangedAttackCooldown = 32; + getSpecialData().rangedAttackMaxCooldown = 48; + getSpecialData().rangedAttackMaxRange = 10.0F; + + //TODO add angler AI @ attack priority + } + + /** Called during spawn finalization to set starting equipment. */ + @Override + protected void populateDefaultEquipmentSlots( DifficultyInstance difficulty ) { + super.populateDefaultEquipmentSlots( difficulty ); + + setItemSlot( EquipmentSlotType.MAINHAND, new ItemStack( Items.FISHING_ROD ) ); + if( getItemBySlot( EquipmentSlotType.FEET ).isEmpty() ) { + ItemStack booties = new ItemStack( Items.LEATHER_BOOTS ); + ((IDyeableArmorItem) booties.getItem()).setColor( booties, 0xFFFF00 ); + setItemSlot( EquipmentSlotType.FEET, booties ); + } + setCanPickUpLoot( false ); + } + + /** Override to change this entity's chance to spawn with a bow. */ + @Override + protected double getVariantBowChance() { return 0.0; } + + + //--------------- IAngler Implementations ---------------- + + /** The parameter for baby status. */ + private static final DataParameter IS_LINE_OUT = EntityDataManager.defineId( FishingZombieEntity.class, DataSerializers.BOOLEAN ); + + /** Called from the Entity.class constructor to define data watcher variables. */ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + entityData.define( IS_LINE_OUT, false ); + } + + /** Sets this angler's line as out (or in). */ + @Override + public void setLineOut( boolean value ) { getEntityData().set( IS_LINE_OUT, value ); } + + /** @return Whether this angler's line is out. */ + @Override + public boolean isLineOut() { return getEntityData().get( IS_LINE_OUT ); } + + /** @return The item equipped in a particular slot. */ + @Override + public ItemStack getItemBySlot( EquipmentSlotType slot ) { + // Display a stick in place of the "cast fishing rod" when the fancy render is disabled + if( level.isClientSide() && /*!Config.get().GENERAL.FANCY_FISHING_MOBS &&*/ EquipmentSlotType.MAINHAND.equals( slot ) ) { + final ItemStack held = super.getItemBySlot( slot ); + if( !held.isEmpty() && held.getItem() instanceof FishingRodItem && isLineOut() ) { + return new ItemStack( Items.STICK ); + } + return held; + } + return super.getItemBySlot( slot ); + } +} \ No newline at end of file