diff --git a/src/main/java/fathertoast/specialmobs/client/ClientRegister.java b/src/main/java/fathertoast/specialmobs/client/ClientRegister.java index 04dfe88..ad29efe 100644 --- a/src/main/java/fathertoast/specialmobs/client/ClientRegister.java +++ b/src/main/java/fathertoast/specialmobs/client/ClientRegister.java @@ -3,15 +3,13 @@ package fathertoast.specialmobs.client; import fathertoast.specialmobs.client.renderer.entity.family.*; import fathertoast.specialmobs.client.renderer.entity.projectile.BugSpitRenderer; import fathertoast.specialmobs.client.renderer.entity.projectile.SpecialFishingBobberRenderer; -import fathertoast.specialmobs.client.renderer.entity.species.CorporealShiftGhastRenderer; -import fathertoast.specialmobs.client.renderer.entity.species.NinjaSkeletonRenderer; -import fathertoast.specialmobs.client.renderer.entity.species.PotionSlimeRenderer; -import fathertoast.specialmobs.client.renderer.entity.species.SpecialZombieVillagerRenderer; +import fathertoast.specialmobs.client.renderer.entity.species.*; import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.config.Config; import fathertoast.specialmobs.common.core.SpecialMobs; import fathertoast.specialmobs.common.core.register.SMEntities; import fathertoast.specialmobs.common.entity.ghast.CorporealShiftGhastEntity; +import fathertoast.specialmobs.common.entity.silverfish.PufferSilverfishEntity; import fathertoast.specialmobs.common.entity.skeleton.NinjaSkeletonEntity; import fathertoast.specialmobs.common.entity.slime.PotionSlimeEntity; import fathertoast.specialmobs.common.entity.witherskeleton.NinjaWitherSkeletonEntity; @@ -77,6 +75,8 @@ public class ClientRegister { registerSpeciesRenderer( PotionSlimeEntity.SPECIES, PotionSlimeRenderer::new ); + registerSpeciesRenderer( PufferSilverfishEntity.SPECIES, ShortSilverfishRenderer::new ); + registerSpeciesRenderer( CorporealShiftGhastEntity.SPECIES, CorporealShiftGhastRenderer::new ); // Other diff --git a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/ShortSilverfishRenderer.java b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/ShortSilverfishRenderer.java new file mode 100644 index 0000000..b4e4fd3 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/ShortSilverfishRenderer.java @@ -0,0 +1,27 @@ +package fathertoast.specialmobs.client.renderer.entity.species; + +import fathertoast.specialmobs.client.renderer.entity.family.SpecialSilverfishRenderer; +import net.minecraft.client.renderer.entity.EntityRendererManager; +import net.minecraft.entity.monster.SilverfishEntity; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +@OnlyIn( Dist.CLIENT ) +public class ShortSilverfishRenderer extends SpecialSilverfishRenderer { + + private static final float FORWARD_OFFSET = 0.4F; + + public ShortSilverfishRenderer( EntityRendererManager rendererManager ) { + super( rendererManager ); + } + + @Override + public Vector3d getRenderOffset( SilverfishEntity entity, float partialTicks ) { + final float angle = MathHelper.lerp( partialTicks, entity.yRotO, entity.yRot ) * (float) Math.PI / 180.0F; + final float forwardX = -MathHelper.sin( angle ); + final float forwardZ = MathHelper.cos( angle ); + return new Vector3d( forwardX * FORWARD_OFFSET, 0.0, forwardZ * FORWARD_OFFSET ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java b/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java index b695d24..fbc5cd0 100644 --- a/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java +++ b/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java @@ -86,7 +86,7 @@ public class MobFamily { public static final MobFamily SILVERFISH = new MobFamily<>( SilverfishFamilyConfig::new, "Silverfish", "silverfish", 0x6E6E6E, new EntityType[] { EntityType.SILVERFISH }, "Albino", "Blinding", "Desiccated", "Fire", "Fishing", "Flying", "Poison", "Puffer", "Tough" - );//TODO puffer + ); public static final MobFamily ENDERMAN = new MobFamily<>( FamilyConfig::new, "Enderman", "endermen", 0x161616, new EntityType[] { EntityType.ENDERMAN }, diff --git a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java index 53d8fcd..7a8d174 100644 --- a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java +++ b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java @@ -82,7 +82,6 @@ public class SpecialMobs { * + natural spawning * - silverfish * - ranged attack AI (spitter) - * + puffer * - endermen * - witches * - ability to equip held items (wonky) diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/AmphibiousMovementController.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/AmphibiousMovementController.java new file mode 100644 index 0000000..7e40a66 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/AmphibiousMovementController.java @@ -0,0 +1,57 @@ +package fathertoast.specialmobs.common.entity.ai; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.MobEntity; +import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.entity.ai.controller.MovementController; +import net.minecraft.util.math.MathHelper; + +/** + * The drowned movement controller repurposed for use on other mobs. + *

+ * {@link net.minecraft.entity.monster.DrownedEntity.MoveHelperController} + */ +public class AmphibiousMovementController extends MovementController { + + private final T owner; + + public AmphibiousMovementController( T entity ) { + super( entity ); + owner = entity; + } + + public void tick() { + final LivingEntity target = owner.getTarget(); + if( owner.shouldSwim() && owner.isInWater() ) { + if( target != null && target.getY() > owner.getY() || owner.isSwimmingUp() ) { + owner.setDeltaMovement( owner.getDeltaMovement().add( 0.0, 0.002, 0.0 ) ); + } + + if( operation != MovementController.Action.MOVE_TO || owner.getNavigation().isDone() ) { + owner.setSpeed( 0.0F ); + return; + } + + final double dX = wantedX - owner.getX(); + final double dY = wantedY - owner.getY(); + final double dZ = wantedZ - owner.getZ(); + final double distance = MathHelper.sqrt( dX * dX + dY * dY + dZ * dZ ); + + final float targetYRot = (float) MathHelper.atan2( dZ, dX ) * 180.0F / (float) Math.PI - 90.0F; + owner.yRot = rotlerp( owner.yRot, targetYRot, 90.0F ); + owner.yBodyRot = owner.yRot; + + final float maxSpeed = (float) (speedModifier * owner.getAttributeValue( Attributes.MOVEMENT_SPEED )); + final float speed = MathHelper.lerp( 0.125F, owner.getSpeed(), maxSpeed ); + owner.setSpeed( speed ); + owner.setDeltaMovement( owner.getDeltaMovement().add( + speed * dX * 0.005, speed * dY / distance * 0.1, speed * dZ * 0.005 ) ); + } + else { + if( !owner.isOnGround() ) { + owner.setDeltaMovement( owner.getDeltaMovement().add( 0.0, -0.008, 0.0 ) ); + } + super.tick(); + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/IAmphibiousMob.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/IAmphibiousMob.java new file mode 100644 index 0000000..ecf11a0 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/IAmphibiousMob.java @@ -0,0 +1,21 @@ +package fathertoast.specialmobs.common.entity.ai; + +/** + * Monsters must implement this interface to use the amphibious AI. + */ +public interface IAmphibiousMob { + /** @return True if this mob should use its swimming navigator for its current goal. */ + default boolean shouldSwim() { return isSwimmingUp(); } + + /** Sets whether this mob should swim upward. */ + void setSwimmingUp( boolean value ); + + /** @return True if this mob should swim upward. */ + boolean isSwimmingUp(); + + /** Sets this mob's current navigator to swimming mode. */ + void setNavigatorToSwim(); + + /** Sets this mob's current navigator to ground mode. */ + void setNavigatorToGround(); +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToShoreGoal.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToShoreGoal.java new file mode 100644 index 0000000..6d012e2 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToShoreGoal.java @@ -0,0 +1,51 @@ +package fathertoast.specialmobs.common.entity.ai.goal; + +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import net.minecraft.entity.CreatureEntity; +import net.minecraft.entity.ai.goal.MoveToBlockGoal; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IWorldReader; + +/** + * The drowned "go to beach" goal repurposed for use on other mobs. + *

+ * {@link net.minecraft.entity.monster.DrownedEntity.GoToBeachGoal} + */ +public class AmphibiousGoToShoreGoal extends MoveToBlockGoal { + + private final T amphibiousMob; + + private boolean disableAtDay = true; + + public AmphibiousGoToShoreGoal( T entity, double speed ) { + super( entity, speed, 8, 2 ); + amphibiousMob = entity; + } + + /** Builder that allows this goal to run during the day. */ + public AmphibiousGoToShoreGoal alwaysEnabled() { + disableAtDay = false; + return this; + } + + /** @return Returns true if this AI can be activated. */ + @Override + public boolean canUse() { + return super.canUse() && !(disableAtDay && mob.level.isDay()) && mob.isInWater() && mob.getY() >= mob.level.getSeaLevel() - 3; + } + + /** @return True if the position is valid to move to. */ + @Override + protected boolean isValidTarget( IWorldReader world, BlockPos targetPos ) { + return world.isEmptyBlock( targetPos.above() ) && world.isEmptyBlock( targetPos.above( 2 ) ) && + world.getBlockState( targetPos ).entityCanStandOn( world, targetPos, mob ); + } + + /** Called when this AI is activated. */ + @Override + public void start() { + amphibiousMob.setSwimmingUp( false ); + amphibiousMob.setNavigatorToGround(); + super.start(); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToWaterGoal.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToWaterGoal.java new file mode 100644 index 0000000..201d53b --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousGoToWaterGoal.java @@ -0,0 +1,81 @@ +package fathertoast.specialmobs.common.entity.ai.goal; + +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import net.minecraft.block.Blocks; +import net.minecraft.entity.MobEntity; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; + +import javax.annotation.Nullable; +import java.util.EnumSet; +import java.util.Random; + +/** + * The drowned "go to water" goal repurposed for use on other mobs. + *

+ * {@link net.minecraft.entity.monster.DrownedEntity.GoToWaterGoal} + */ +public class AmphibiousGoToWaterGoal extends Goal { + + private final T mob; + private final double speedModifier; + + private boolean disableAtNight = true; + + private double wantedX; + private double wantedY; + private double wantedZ; + + public AmphibiousGoToWaterGoal( T entity, double speed ) { + mob = entity; + speedModifier = speed; + setFlags( EnumSet.of( Goal.Flag.MOVE ) ); + } + + /** Builder that allows this goal to run during the night. */ + public AmphibiousGoToWaterGoal alwaysEnabled() { + disableAtNight = false; + return this; + } + + /** @return Returns true if this AI can be activated. */ + @Override + public boolean canUse() { + if( disableAtNight && !mob.level.isDay() || mob.isInWater() ) return false; + + final Vector3d targetPos = findWaterPos(); + if( targetPos == null ) return false; + + wantedX = targetPos.x; + wantedY = targetPos.y; + wantedZ = targetPos.z; + return true; + } + + /** @return Called each update while active and returns true if this AI can remain active. */ + @Override + public boolean canContinueToUse() { return !mob.getNavigation().isDone(); } + + /** Called when this AI is activated. */ + @Override + public void start() { mob.getNavigation().moveTo( wantedX, wantedY, wantedZ, speedModifier ); } + + /** @return A random nearby position of water, or null if none is found after a few tries. */ + @Nullable + private Vector3d findWaterPos() { + final Random random = mob.getRandom(); + final BlockPos origin = mob.blockPosition(); + + for( int i = 0; i < 10; i++ ) { + final BlockPos target = origin.offset( + random.nextInt( 20 ) - 10, + 2 - random.nextInt( 8 ), + random.nextInt( 20 ) - 10 ); + if( mob.level.getBlockState( target ).is( Blocks.WATER ) ) { + return Vector3d.atBottomCenterOf( target ); + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousMeleeAttackGoal.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousMeleeAttackGoal.java new file mode 100644 index 0000000..3815033 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousMeleeAttackGoal.java @@ -0,0 +1,47 @@ +package fathertoast.specialmobs.common.entity.ai.goal; + +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import net.minecraft.entity.CreatureEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.goal.MeleeAttackGoal; + +import javax.annotation.Nullable; + +/** + * The drowned "attack" goal repurposed for use on other mobs. + * Prevents mobs from attacking targets outside of water during the day. + *

+ * {@link net.minecraft.entity.monster.DrownedEntity.AttackGoal} + */ +public class AmphibiousMeleeAttackGoal extends MeleeAttackGoal { + + /** @return True if the target is valid. */ + public static boolean isValidTarget( @Nullable LivingEntity target ) { + return target != null && (!target.level.isDay() || target.isInWater()); + } + + public AmphibiousMeleeAttackGoal( T entity, double speed, boolean longMemory ) { super( entity, speed, longMemory ); } + + /** @return Returns true if this AI can be activated. */ + @Override + public boolean canUse() { return super.canUse() && isValidTarget( mob.getTarget() ); } + + /** @return Called each update while active and returns true if this AI can remain active. */ + @Override + public boolean canContinueToUse() { return super.canContinueToUse() && isValidTarget( mob.getTarget() ); } + + // Uncomment if ever needed + // /** A zombie attack version of the normal amphibious melee goal. */ + // public static class Zombie extends ZombieAttackGoal { + // + // public Zombie( T entity, double speed, boolean longMemory ) { super( entity, speed, longMemory ); } + // + // /** @return Returns true if this AI can be activated. */ + // @Override + // public boolean canUse() { return super.canUse() && isValidTarget( mob.getTarget() ); } + // + // /** @return Called each update while active and returns true if this AI can remain active. */ + // @Override + // public boolean canContinueToUse() { return super.canContinueToUse() && isValidTarget( mob.getTarget() ); } + // } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousSwimUpGoal.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousSwimUpGoal.java new file mode 100644 index 0000000..900a01c --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/AmphibiousSwimUpGoal.java @@ -0,0 +1,82 @@ +package fathertoast.specialmobs.common.entity.ai.goal; + +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import net.minecraft.entity.CreatureEntity; +import net.minecraft.entity.ai.RandomPositionGenerator; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.pathfinding.Path; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; + +/** + * The drowned "swim up" goal repurposed for use on other mobs. + *

+ * {@link net.minecraft.entity.monster.DrownedEntity.SwimUpGoal} + */ +public class AmphibiousSwimUpGoal extends Goal { + + private final T mob; + private final double speedModifier; + private final int seaLevel; + + private boolean disableAtDay = true; + + private boolean stuck; + + public AmphibiousSwimUpGoal( T entity, double speed ) { + mob = entity; + speedModifier = speed; + seaLevel = entity.level.getSeaLevel() - 1; + } + + /** Builder that allows this goal to run during the day. */ + public AmphibiousSwimUpGoal alwaysEnabled() { + disableAtDay = false; + return this; + } + + /** @return Returns true if this AI can be activated. */ + @Override + public boolean canUse() { return !(disableAtDay && mob.level.isDay()) && mob.isInWater() && mob.getY() < seaLevel - 1; } + + /** @return Called each update while active and returns true if this AI can remain active. */ + @Override + public boolean canContinueToUse() { return canUse() && !stuck; } + + /** Called when this AI is activated. */ + @Override + public void start() { + mob.setSwimmingUp( true ); + stuck = false; + } + + /** Called when this AI is deactivated. */ + @Override + public void stop() { mob.setSwimmingUp( false ); } + + /** Called each tick while this AI is active. */ + @Override + public void tick() { + if( mob.getY() < seaLevel && (mob.getNavigation().isDone() || closeToNextPos()) ) { + final Vector3d pos = RandomPositionGenerator.getPosTowards( mob, 4, 8, + new Vector3d( mob.getX(), seaLevel, mob.getZ() ) ); + if( pos == null ) { + stuck = true; + return; + } + + mob.getNavigation().moveTo( pos.x, pos.y, pos.z, speedModifier ); + } + + } + + /** @return True if the entity is within 2 blocks of its pathing target. */ + private boolean closeToNextPos() { + final Path path = mob.getNavigation().getPath(); + if( path != null ) { + final BlockPos pos = path.getTarget(); + return mob.distanceToSqr( pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5 ) < 4.0; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/creeper/DrowningCreeperEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/creeper/DrowningCreeperEntity.java index 1494442..06e7f8c 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/creeper/DrowningCreeperEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/creeper/DrowningCreeperEntity.java @@ -3,6 +3,11 @@ package fathertoast.specialmobs.common.entity.creeper; import fathertoast.specialmobs.common.bestiary.BestiaryInfo; import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.entity.ai.AIHelper; +import fathertoast.specialmobs.common.entity.ai.AmphibiousMovementController; +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import fathertoast.specialmobs.common.entity.ai.goal.AmphibiousGoToWaterGoal; +import fathertoast.specialmobs.common.entity.ai.goal.AmphibiousSwimUpGoal; import fathertoast.specialmobs.common.util.ExplosionHelper; import fathertoast.specialmobs.common.util.References; import fathertoast.specialmobs.datagen.loot.LootEntryItemBuilder; @@ -11,18 +16,26 @@ import fathertoast.specialmobs.datagen.loot.LootTableBuilder; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.MoverType; import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.entity.ai.goal.SwimGoal; import net.minecraft.entity.passive.fish.PufferfishEntity; import net.minecraft.item.Items; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.pathfinding.GroundPathNavigator; +import net.minecraft.pathfinding.PathNodeType; +import net.minecraft.pathfinding.SwimmerPathNavigator; import net.minecraft.state.properties.BlockStateProperties; import net.minecraft.tags.BlockTags; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; import net.minecraft.world.Explosion; import net.minecraft.world.IServerWorld; import net.minecraft.world.World; @SpecialMob -public class DrowningCreeperEntity extends _SpecialCreeperEntity { +public class DrowningCreeperEntity extends _SpecialCreeperEntity implements IAmphibiousMob { //--------------- Static Special Mob Hooks ---------------- @@ -66,7 +79,23 @@ public class DrowningCreeperEntity extends _SpecialCreeperEntity { //--------------- Variant-Specific Implementations ---------------- - public DrowningCreeperEntity( EntityType entityType, World world ) { super( entityType, world ); } + public DrowningCreeperEntity( EntityType entityType, World world ) { + super( entityType, world ); + moveControl = new AmphibiousMovementController<>( this ); + waterNavigation = new SwimmerPathNavigator( this, world ); + groundNavigation = new GroundPathNavigator( this, world ); + maxUpStep = 1.0F; + setPathfindingMalus( PathNodeType.WATER, PathNodeType.WALKABLE.getMalus() ); + } + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + AIHelper.removeGoals( goalSelector, SwimGoal.class ); + AIHelper.insertGoal( goalSelector, 5, new AmphibiousGoToWaterGoal<>( this, 1.0 ).alwaysEnabled() ); + AIHelper.insertGoal( goalSelector, 6, new AmphibiousSwimUpGoal<>( this, 1.0 ) ); + AIHelper.replaceWaterAvoidingRandomWalking( this, 0.8 ); + } /** Override to change this creeper's explosion power multiplier. */ @Override @@ -92,7 +121,7 @@ public class DrowningCreeperEntity extends _SpecialCreeperEntity { final int rMinusOneSq = (radius - 1) * (radius - 1); final BlockPos center = new BlockPos( explosion.getPos() ); - // Track how many pufferfish have been spawned + // Track how many pufferfish have been spawned so we don't spawn a bunch of them spawnPufferfish( center.above( 1 ) ); int pufferCount = 1; @@ -123,8 +152,7 @@ public class DrowningCreeperEntity extends _SpecialCreeperEntity { // Water fill level.setBlock( pos, water, References.SetBlockFlags.DEFAULTS ); - // Prevent greater radiuses from spawning a bazillion pufferfish - if( random.nextFloat() < 0.01F && pufferCount < 10 ) { + if( random.nextFloat() < 0.0075F && pufferCount < 5 ) { spawnPufferfish( pos ); pufferCount++; } @@ -153,4 +181,72 @@ public class DrowningCreeperEntity extends _SpecialCreeperEntity { @Override public boolean isInWaterRainOrBubble() { return true; } + + /** Override to load data from this entity's NBT data. */ + @Override + public void readVariantSaveData( CompoundNBT saveTag ) { + setPathfindingMalus( PathNodeType.WATER, PathNodeType.WALKABLE.getMalus() ); + } + + + //--------------- IAmphibiousMob Implementation ---------------- + + private final SwimmerPathNavigator waterNavigation; + private final GroundPathNavigator groundNavigation; + + private boolean swimmingUp; + + /** Called each tick to update this entity's swimming state. */ + @Override + public void updateSwimming() { + if( !level.isClientSide ) { + if( isEffectiveAi() && isUnderWater() && shouldSwim() ) { + setNavigatorToSwim(); + setSwimming( true ); + } + else { + setNavigatorToGround(); + setSwimming( false ); + } + } + } + + /** Moves this entity in the desired direction. Input magnitude of < 1 scales down movement speed. */ + @Override + public void travel( Vector3d input ) { + if( isEffectiveAi() && isUnderWater() && shouldSwim() ) { + moveRelative( 0.01F, input ); + move( MoverType.SELF, getDeltaMovement() ); + setDeltaMovement( getDeltaMovement().scale( 0.9 ) ); + } + else super.travel( input ); + } + + /** @return Water drag coefficient. */ + @Override + protected float getWaterSlowDown() { return 0.9F; } + + /** @return True if this mob should use its swimming navigator for its current goal. */ + @Override + public boolean shouldSwim() { + if( swimmingUp ) return true; + final LivingEntity target = getTarget(); + return target != null && target.isInWater(); + } + + /** Sets whether this mob should swim upward. */ + @Override + public void setSwimmingUp( boolean value ) { swimmingUp = value; } + + /** @return True if this mob should swim upward. */ + @Override + public boolean isSwimmingUp() { return swimmingUp; } + + /** Sets this mob's current navigator to swimming mode. */ + @Override + public void setNavigatorToSwim() { navigation = waterNavigation; } + + /** Sets this mob's current navigator to ground mode. */ + @Override + public void setNavigatorToGround() { navigation = groundNavigation; } } \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/drowned/_SpecialDrownedEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/drowned/_SpecialDrownedEntity.java index 2e52f3c..7741ac0 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/drowned/_SpecialDrownedEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/drowned/_SpecialDrownedEntity.java @@ -131,6 +131,8 @@ public class _SpecialDrownedEntity extends DrownedEntity implements ISpecialMob< /** The parameter for special mob render scale. */ private static final DataParameter SCALE = EntityDataManager.defineId( _SpecialDrownedEntity.class, DataSerializers.FLOAT ); + private boolean needsToBeDeeper; + public _SpecialDrownedEntity( EntityType entityType, World world ) { super( entityType, world ); recalculateAttackGoal(); @@ -172,6 +174,35 @@ public class _SpecialDrownedEntity extends DrownedEntity implements ISpecialMob< level.addFreshEntity( trident ); } + /** Called each tick to update this entity's swimming state. */ + @Override + public void updateSwimming() { + if( !level.isClientSide && isEffectiveAi() ) needsToBeDeeper = true; + super.updateSwimming(); + } + + /** Moves this entity in the desired direction. Input magnitude of < 1 scales down movement speed. */ + @Override + public void travel( Vector3d input ) { + if( isEffectiveAi() ) needsToBeDeeper = true; + super.travel( input ); + } + + /** @return Water drag coefficient. */ + @Override + protected float getWaterSlowDown() { return 0.9F; } // Improve mobility in shallow water + + /** @return True if this entity is in water. */ + @Override + public boolean isInWater() { + // Hacky way to fix vanilla drowned AI breaking in shallow water + if( needsToBeDeeper ) { + needsToBeDeeper = false; + return isUnderWater(); + } + return super.isInWater(); + } + //--------------- ISpecialMob Implementation ---------------- diff --git a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/AmphibiousSilverfishEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/AmphibiousSilverfishEntity.java new file mode 100644 index 0000000..ded9134 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/AmphibiousSilverfishEntity.java @@ -0,0 +1,97 @@ +package fathertoast.specialmobs.common.entity.silverfish; + +import fathertoast.specialmobs.common.entity.ai.AmphibiousMovementController; +import fathertoast.specialmobs.common.entity.ai.IAmphibiousMob; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.MoverType; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.pathfinding.GroundPathNavigator; +import net.minecraft.pathfinding.PathNodeType; +import net.minecraft.pathfinding.SwimmerPathNavigator; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraft.world.World; + +/** + * A bare-bones implementation of an amphibious silverfish. Just fix the AI and it's good to go. + */ +public abstract class AmphibiousSilverfishEntity extends _SpecialSilverfishEntity implements IAmphibiousMob { + + private final SwimmerPathNavigator waterNavigation; + private final GroundPathNavigator groundNavigation; + + private boolean swimmingUp; + + public AmphibiousSilverfishEntity( EntityType entityType, World world ) { + super( entityType, world ); + moveControl = new AmphibiousMovementController<>( this ); + waterNavigation = new SwimmerPathNavigator( this, world ); + groundNavigation = new GroundPathNavigator( this, world ); + maxUpStep = 1.0F; + setPathfindingMalus( PathNodeType.WATER, PathNodeType.WALKABLE.getMalus() ); + } + + /** Loads data from this entity's base NBT compound that is specific to its subclass. */ + @Override + public void readAdditionalSaveData( CompoundNBT tag ) { + super.readAdditionalSaveData( tag ); + setPathfindingMalus( PathNodeType.WATER, PathNodeType.WALKABLE.getMalus() ); + } + + + //--------------- IAmphibiousMob Implementation ---------------- + + /** Called each tick to update this entity's swimming state. */ + @Override + public void updateSwimming() { + if( !level.isClientSide ) { + if( isEffectiveAi() && isUnderWater() && shouldSwim() ) { + setNavigatorToSwim(); + setSwimming( true ); + } + else { + setNavigatorToGround(); + setSwimming( false ); + } + } + } + + /** Moves this entity in the desired direction. Input magnitude of < 1 scales down movement speed. */ + @Override + public void travel( Vector3d input ) { + if( isEffectiveAi() && isUnderWater() && shouldSwim() ) { + moveRelative( 0.01F, input ); + move( MoverType.SELF, getDeltaMovement() ); + setDeltaMovement( getDeltaMovement().scale( 0.9 ) ); + } + else super.travel( input ); + } + + /** @return Water drag coefficient. */ + @Override + protected float getWaterSlowDown() { return 0.9F; } + + /** @return True if this mob should use its swimming navigator for its current goal. */ + @Override + public boolean shouldSwim() { + if( swimmingUp ) return true; + final LivingEntity target = getTarget(); + return target != null && target.isInWater(); + } + + /** Sets whether this mob should swim upward. */ + @Override + public void setSwimmingUp( boolean value ) { swimmingUp = value; } + + /** @return True if this mob should swim upward. */ + @Override + public boolean isSwimmingUp() { return swimmingUp; } + + /** Sets this mob's current navigator to swimming mode. */ + @Override + public void setNavigatorToSwim() { navigation = waterNavigation; } + + /** Sets this mob's current navigator to ground mode. */ + @Override + public void setNavigatorToGround() { navigation = groundNavigation; } +} \ 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 index 6095dc1..5414294 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java @@ -7,6 +7,8 @@ import fathertoast.specialmobs.common.config.species.SilverfishSpeciesConfig; import fathertoast.specialmobs.common.config.species.SpeciesConfig; import fathertoast.specialmobs.common.entity.ai.AIHelper; import fathertoast.specialmobs.common.entity.ai.IAngler; +import fathertoast.specialmobs.common.entity.ai.goal.AmphibiousGoToShoreGoal; +import fathertoast.specialmobs.common.entity.ai.goal.AmphibiousSwimUpGoal; import fathertoast.specialmobs.common.entity.ai.goal.AnglerGoal; import fathertoast.specialmobs.common.entity.ai.goal.PassiveRangedAttackGoal; import fathertoast.specialmobs.common.util.References; @@ -15,11 +17,12 @@ import fathertoast.specialmobs.datagen.loot.LootPoolBuilder; import fathertoast.specialmobs.datagen.loot.LootTableBuilder; import net.minecraft.entity.EntityType; import net.minecraft.entity.ai.attributes.Attributes; +import net.minecraft.entity.ai.goal.SwimGoal; import net.minecraft.item.Items; import net.minecraft.world.World; @SpecialMob -public class FishingSilverfishEntity extends _SpecialSilverfishEntity implements IAngler { +public class FishingSilverfishEntity extends AmphibiousSilverfishEntity implements IAngler { //--------------- Static Special Mob Hooks ---------------- @@ -77,6 +80,10 @@ public class FishingSilverfishEntity extends _SpecialSilverfishEntity implements protected void registerVariantGoals() { AIHelper.removeGoals( goalSelector, PassiveRangedAttackGoal.class ); // Disable spit attack use goalSelector.addGoal( 4, new AnglerGoal<>( this ) ); + + AIHelper.removeGoals( goalSelector, SwimGoal.class ); + AIHelper.insertGoal( goalSelector, 5, new AmphibiousGoToShoreGoal<>( this, 1.0 ).alwaysEnabled() ); + AIHelper.insertGoal( goalSelector, 6, new AmphibiousSwimUpGoal<>( this, 1.0 ).alwaysEnabled() ); } diff --git a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/PufferSilverfishEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/PufferSilverfishEntity.java index a0fe554..8488c8a 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/PufferSilverfishEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/PufferSilverfishEntity.java @@ -4,16 +4,19 @@ import fathertoast.specialmobs.common.bestiary.BestiaryInfo; import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.bestiary.SpecialMob; import fathertoast.specialmobs.common.entity.MobHelper; +import fathertoast.specialmobs.common.entity.ai.AIHelper; +import fathertoast.specialmobs.common.entity.ai.goal.AmphibiousGoToWaterGoal; import fathertoast.specialmobs.common.util.References; import fathertoast.specialmobs.datagen.loot.LootTableBuilder; import net.minecraft.entity.EntityType; import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.goal.SwimGoal; import net.minecraft.item.Items; import net.minecraft.potion.Effects; import net.minecraft.world.World; @SpecialMob -public class PufferSilverfishEntity extends _SpecialSilverfishEntity { +public class PufferSilverfishEntity extends AmphibiousSilverfishEntity { //--------------- Static Special Mob Hooks ---------------- @@ -23,7 +26,7 @@ public class PufferSilverfishEntity extends _SpecialSilverfishEntity { @SpecialMob.BestiaryInfoSupplier public static void getBestiaryInfo( BestiaryInfo.Builder bestiaryInfo ) { bestiaryInfo.color( 0xE6E861 ).theme( BestiaryInfo.Theme.WATER ) - .uniqueTextureBaseOnly()//TODO Change texture or renderer to fix offset + .uniqueTextureBaseOnly() .addExperience( 1 ).drownImmune().effectImmune( Effects.POISON ); } @@ -52,7 +55,12 @@ public class PufferSilverfishEntity extends _SpecialSilverfishEntity { public PufferSilverfishEntity( EntityType entityType, World world ) { super( entityType, world ); } - //TODO swim behavior + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + AIHelper.removeGoals( goalSelector, SwimGoal.class ); + AIHelper.insertGoal( goalSelector, 5, new AmphibiousGoToWaterGoal<>( this, 1.0 ).alwaysEnabled() ); + } /** Override to change the color of this entity's spit attack. */ @Override diff --git a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/_SpecialSilverfishEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/_SpecialSilverfishEntity.java index 87e0e10..3657031 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/_SpecialSilverfishEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/_SpecialSilverfishEntity.java @@ -89,6 +89,8 @@ public class _SpecialSilverfishEntity extends SilverfishEntity implements IRange super.registerGoals(); goalSelector.addGoal( 4, new PassiveRangedAttackGoal<>( this ) ); AIHelper.replaceHurtByTarget( this, new SpecialHurtByTargetGoal( this, SilverfishEntity.class ).setAlertOthers() ); + // Someday, it would be nice to replace SilverfishEntity.HideInStoneGoal with one that + // expands the allowed stone types and preserves species on hide/reveal registerVariantGoals(); } diff --git a/src/main/java/fathertoast/specialmobs/common/entity/slime/BlueberrySlimeEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/slime/BlueberrySlimeEntity.java index 182b004..1679516 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/slime/BlueberrySlimeEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/slime/BlueberrySlimeEntity.java @@ -3,21 +3,20 @@ package fathertoast.specialmobs.common.entity.slime; import fathertoast.specialmobs.common.bestiary.BestiaryInfo; import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.bestiary.SpecialMob; -import fathertoast.specialmobs.common.entity.MobHelper; import fathertoast.specialmobs.common.entity.ai.AIHelper; -import fathertoast.specialmobs.common.entity.ai.FluidPathNavigator; import fathertoast.specialmobs.common.util.References; import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import net.minecraft.block.FlowingFluidBlock; import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.fluid.Fluid; import net.minecraft.item.Items; import net.minecraft.nbt.CompoundNBT; import net.minecraft.particles.IParticleData; import net.minecraft.particles.ParticleTypes; -import net.minecraft.pathfinding.PathNavigator; import net.minecraft.pathfinding.PathNodeType; import net.minecraft.tags.FluidTags; +import net.minecraft.util.math.shapes.ISelectionContext; import net.minecraft.world.World; @SpecialMob @@ -71,21 +70,28 @@ public class BlueberrySlimeEntity extends _SpecialSlimeEntity { AIHelper.removeGoals( goalSelector, 1 ); // SlimeEntity.FloatGoal } - /** @return A new path navigator for this entity to use. */ - @Override - protected PathNavigator createNavigation( World world ) { - return new FluidPathNavigator( this, world, true, false ); - } - - /** @return Whether this entity can stand on a particular type of fluid. */ - @Override - public boolean canStandOnFluid( Fluid fluid ) { return fluid.is( FluidTags.WATER ); } - /** Called each tick to update this entity. */ @Override public void tick() { super.tick(); - MobHelper.floatInFluid( this, 0.06, FluidTags.WATER ); + + // Hacky way of attacking submerged targets; drop down on them when directly above + double floatAccel = 0.06; + final LivingEntity target = getTarget(); + if( target != null && target.getY( 0.5 ) < getY( 0.5 ) ) { + // + final double dX = target.getX() - getX(); + final double dZ = target.getZ() - getZ(); + final float range = (target.getBbWidth() + getBbWidth() + 0.1F) / 2.0F; + if( dX * dX + dZ * dZ < range * range ) + floatAccel = -0.12; + } + if( tickCount > 1 && getFluidHeight( FluidTags.WATER ) > 0.0 ) { + if( !ISelectionContext.of( this ).isAbove( FlowingFluidBlock.STABLE_SHAPE, blockPosition(), true ) || + level.getFluidState( blockPosition().above() ).is( FluidTags.WATER ) ) { + setDeltaMovement( getDeltaMovement().scale( 0.5 ).add( 0.0, floatAccel, 0.0 ) ); + } + } } // The below two methods are here to effectively override the private Entity#isInRain to always return true (always wet) @@ -95,6 +101,10 @@ public class BlueberrySlimeEntity extends _SpecialSlimeEntity { @Override public boolean isInWaterRainOrBubble() { return true; } + /** @return Water drag coefficient. */ + @Override + protected float getWaterSlowDown() { return 0.9F; } + /** Override to load data from this entity's NBT data. */ @Override public void readVariantSaveData( CompoundNBT saveTag ) {