diff --git a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/EnderCreeperRenderer.java b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/EnderCreeperRenderer.java index 4578e81..ba1df88 100644 --- a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/EnderCreeperRenderer.java +++ b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/EnderCreeperRenderer.java @@ -4,6 +4,7 @@ import fathertoast.specialmobs.client.renderer.entity.family.SpecialCreeperRende import fathertoast.specialmobs.common.entity.creeper.EnderCreeperEntity; import net.minecraft.client.renderer.entity.EntityRendererManager; import net.minecraft.entity.monster.CreeperEntity; +import net.minecraft.entity.monster.EndermanEntity; import net.minecraft.util.math.vector.Vector3d; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -18,6 +19,7 @@ public class EnderCreeperRenderer extends SpecialCreeperRenderer { public EnderCreeperRenderer( EntityRendererManager rendererManager ) { super( rendererManager ); } + /** {@link net.minecraft.client.renderer.entity.EndermanRenderer#getRenderOffset(EndermanEntity, float)} */ @Override public Vector3d getRenderOffset( CreeperEntity entity, float partialTicks ) { if( ((EnderCreeperEntity) entity).isCreepy() ) { @@ -26,5 +28,4 @@ public class EnderCreeperRenderer extends SpecialCreeperRenderer { } return super.getRenderOffset( entity, partialTicks ); } - } \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanModel.java b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanModel.java deleted file mode 100644 index 9221e33..0000000 --- a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanModel.java +++ /dev/null @@ -1,118 +0,0 @@ -package fathertoast.specialmobs.client.renderer.entity.species; - -import fathertoast.specialmobs.common.entity.enderman.RunicEndermanEntity; -import net.minecraft.client.renderer.entity.model.BipedModel; -import net.minecraft.client.renderer.model.ModelRenderer; - -/** Epic copy-paste of {@link net.minecraft.client.renderer.entity.model.EndermanModel} */ -public class RunicEndermanModel extends BipedModel { - - public boolean carrying; - public boolean creepy; - - public RunicEndermanModel(float something) { - super(0.0F, -14.0F, 64, 32); - float f = -14.0F; - this.hat = new ModelRenderer(this, 0, 16); - this.hat.addBox(-4.0F, -8.0F, -4.0F, 8.0F, 8.0F, 8.0F, something - 0.5F); - this.hat.setPos(0.0F, -14.0F, 0.0F); - this.body = new ModelRenderer(this, 32, 16); - this.body.addBox(-4.0F, 0.0F, -2.0F, 8.0F, 12.0F, 4.0F, something); - this.body.setPos(0.0F, -14.0F, 0.0F); - this.rightArm = new ModelRenderer(this, 56, 0); - this.rightArm.addBox(-1.0F, -2.0F, -1.0F, 2.0F, 30.0F, 2.0F, something); - this.rightArm.setPos(-3.0F, -12.0F, 0.0F); - this.leftArm = new ModelRenderer(this, 56, 0); - this.leftArm.mirror = true; - this.leftArm.addBox(-1.0F, -2.0F, -1.0F, 2.0F, 30.0F, 2.0F, something); - this.leftArm.setPos(5.0F, -12.0F, 0.0F); - this.rightLeg = new ModelRenderer(this, 56, 0); - this.rightLeg.addBox(-1.0F, 0.0F, -1.0F, 2.0F, 30.0F, 2.0F, something); - this.rightLeg.setPos(-2.0F, -2.0F, 0.0F); - this.leftLeg = new ModelRenderer(this, 56, 0); - this.leftLeg.mirror = true; - this.leftLeg.addBox(-1.0F, 0.0F, -1.0F, 2.0F, 30.0F, 2.0F, something); - this.leftLeg.setPos(2.0F, -2.0F, 0.0F); - } - @Override - public void setupAnim(T enderman, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { - super.setupAnim(enderman, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch); - - this.head.visible = true; - this.body.xRot = 0.0F; - this.body.y = -14.0F; - this.body.z = -0.0F; - this.rightLeg.xRot -= 0.0F; - this.leftLeg.xRot -= 0.0F; - this.rightArm.xRot = (float)((double)this.rightArm.xRot * 0.5D); - this.leftArm.xRot = (float)((double)this.leftArm.xRot * 0.5D); - this.rightLeg.xRot = (float)((double)this.rightLeg.xRot * 0.5D); - this.leftLeg.xRot = (float)((double)this.leftLeg.xRot * 0.5D); - - if (this.rightArm.xRot > 0.4F) { - this.rightArm.xRot = 0.4F; - } - - if (this.leftArm.xRot > 0.4F) { - this.leftArm.xRot = 0.4F; - } - - if (this.rightArm.xRot < -0.4F) { - this.rightArm.xRot = -0.4F; - } - - if (this.leftArm.xRot < -0.4F) { - this.leftArm.xRot = -0.4F; - } - - if (this.rightLeg.xRot > 0.4F) { - this.rightLeg.xRot = 0.4F; - } - - if (this.leftLeg.xRot > 0.4F) { - this.leftLeg.xRot = 0.4F; - } - - if (this.rightLeg.xRot < -0.4F) { - this.rightLeg.xRot = -0.4F; - } - - if (this.leftLeg.xRot < -0.4F) { - this.leftLeg.xRot = -0.4F; - } - - if (this.carrying) { - this.rightArm.xRot = -0.5F; - this.leftArm.xRot = -0.5F; - this.rightArm.zRot = 0.05F; - this.leftArm.zRot = -0.05F; - } - this.rightArm.z = 0.0F; - this.leftArm.z = 0.0F; - this.rightLeg.z = 0.0F; - this.leftLeg.z = 0.0F; - this.rightLeg.y = -5.0F; - this.leftLeg.y = -5.0F; - this.head.z = -0.0F; - this.head.y = -13.0F; - this.hat.x = this.head.x; - this.hat.y = this.head.y; - this.hat.z = this.head.z; - this.hat.xRot = this.head.xRot; - this.hat.yRot = this.head.yRot; - this.hat.zRot = this.head.zRot; - - if (this.creepy) { - this.head.y -= 5.0F; - } - this.rightArm.setPos(-5.0F, -12.0F, 0.0F); - this.leftArm.setPos(5.0F, -12.0F, 0.0F); - - if (enderman.getBeamTargetId().isPresent()) { - leftArm.xRot = -0.5F; - rightArm.xRot = -0.5F; - leftArm.zRot = -0.25F; - rightArm.zRot = 0.25F; - } - } -} diff --git a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanRenderer.java b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanRenderer.java index a1539dc..7ef4300 100644 --- a/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanRenderer.java +++ b/src/main/java/fathertoast/specialmobs/client/renderer/entity/species/RunicEndermanRenderer.java @@ -2,107 +2,146 @@ package fathertoast.specialmobs.client.renderer.entity.species; import com.mojang.blaze3d.matrix.MatrixStack; import com.mojang.blaze3d.vertex.IVertexBuilder; -import fathertoast.specialmobs.client.renderer.entity.layers.SpecialMobEyesLayer; -import fathertoast.specialmobs.client.renderer.entity.layers.SpecialMobOverlayLayer; -import fathertoast.specialmobs.common.entity.ISpecialMob; +import fathertoast.specialmobs.client.renderer.entity.family.SpecialEndermanRenderer; import fathertoast.specialmobs.common.entity.enderman.RunicEndermanEntity; import net.minecraft.client.renderer.IRenderTypeBuffer; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.entity.EnderDragonRenderer; import net.minecraft.client.renderer.entity.EntityRendererManager; -import net.minecraft.client.renderer.entity.MobRenderer; +import net.minecraft.client.renderer.entity.model.EndermanModel; import net.minecraft.client.renderer.texture.OverlayTexture; -import net.minecraft.entity.Entity; +import net.minecraft.entity.monster.EndermanEntity; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RayTraceContext; +import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.math.vector.Matrix3f; import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.util.math.vector.Vector3d; import net.minecraft.util.math.vector.Vector3f; -public class RunicEndermanRenderer extends MobRenderer> { - - public static final ResourceLocation CRYSTAL_BEAM_LOCATION = new ResourceLocation("textures/entity/end_crystal/end_crystal_beam.png"); - private static final RenderType BEAM = RenderType.entitySmoothCutout(CRYSTAL_BEAM_LOCATION); - - private final float baseShadowRadius; - - - public RunicEndermanRenderer(EntityRendererManager rendererManager) { - super(rendererManager, new RunicEndermanModel<>(0.0F), 0.5F); - - baseShadowRadius = shadowRadius; - addLayer( new SpecialMobEyesLayer<>( this ) ); - addLayer( new SpecialMobOverlayLayer<>( this, new RunicEndermanModel<>( 0.25F ) ) ); - } - +public class RunicEndermanRenderer extends SpecialEndermanRenderer { + public static final ResourceLocation BEAM_TEXTURE_LOCATION = new ResourceLocation( "textures/entity/end_crystal/end_crystal_beam.png" ); + private static final RenderType BEAM = RenderType.entitySmoothCutout( BEAM_TEXTURE_LOCATION ); + + public RunicEndermanRenderer( EntityRendererManager rendererManager ) { super( rendererManager ); } + @Override - public void render(RunicEndermanEntity enderman, float rotation, float partialTicks, MatrixStack matrixStack, IRenderTypeBuffer buffer, int packedLight) { - if (enderman.getBeamTargetId().isPresent()) { - Entity entity = enderman.level.getEntity(enderman.getBeamTargetId().getAsInt()); - - if (entity != null) { - matrixStack.pushPose(); - float x = (float)(entity.getX() - MathHelper.lerp(partialTicks, enderman.xo, enderman.getX())); - float y = (float)(entity.getY() - MathHelper.lerp(partialTicks, enderman.yo, enderman.getY())); - float z = (float)(entity.getZ() - MathHelper.lerp(partialTicks, enderman.zo, enderman.getZ())); - renderBeamAttack(x, y, z, partialTicks, enderman.tickCount, matrixStack, buffer, packedLight); - matrixStack.popPose(); + public void render( EndermanEntity entity, float rotation, float partialTicks, + MatrixStack matrixStack, IRenderTypeBuffer buffer, int packedLight ) { + // Beam attack + final RunicEndermanEntity.BeamState beamState = ((RunicEndermanEntity) entity).getBeamState(); + if( beamState != RunicEndermanEntity.BeamState.OFF ) { + matrixStack.pushPose(); + + Vector3d beamVec = entity.getViewVector( partialTicks ).scale( RunicEndermanEntity.BEAM_MAX_RANGE ); + + final Vector3d beamStartPos = entity.getEyePosition( partialTicks ); + final Vector3d beamEndPos = beamStartPos.add( beamVec ); + + final RayTraceResult blockRayTrace = entity.level.clip( + new RayTraceContext( beamStartPos, beamEndPos, + RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, entity ) ); + if( blockRayTrace.getType() != RayTraceResult.Type.MISS ) { + beamVec = blockRayTrace.getLocation().subtract( beamStartPos ); } + + renderBeamAttack( beamState, entity.getEyeHeight(), (float) beamVec.x, (float) beamVec.y, (float) beamVec.z, + partialTicks, entity.tickCount, matrixStack, buffer, packedLight ); + + matrixStack.popPose(); } - - super.render(enderman, rotation, partialTicks, matrixStack, buffer, packedLight); + + super.render( entity, rotation, partialTicks, matrixStack, buffer, packedLight ); } - + @Override - public ResourceLocation getTextureLocation(RunicEndermanEntity enderman) { - return ((ISpecialMob) enderman).getSpecialData().getTexture(); + protected boolean isBodyVisible( EndermanEntity entity ) { + // This is called right after EntityModel#setupAnim, so we add on our own animation here as a hacky way to avoid a lot of extra stuff + final RunicEndermanEntity.BeamState beamState = ((RunicEndermanEntity) entity).getBeamState(); + if( beamState != RunicEndermanEntity.BeamState.OFF ) { + final float zRot = beamState == RunicEndermanEntity.BeamState.DAMAGING ? 0.25F : 0.6F; + final EndermanModel model = getModel(); + model.leftArm.xRot = -0.6F; + model.rightArm.xRot = -0.6F; + model.leftArm.zRot = -zRot; + model.rightArm.zRot = zRot; + } + + return super.isBodyVisible( entity ); } - - @Override - protected void scale(RunicEndermanEntity entity, MatrixStack matrixStack, float partialTick ) { - super.scale( entity, matrixStack, partialTick ); - - final float scale = ((ISpecialMob) entity).getSpecialData().getRenderScale(); - shadowRadius = baseShadowRadius * scale; - matrixStack.scale( scale, scale, scale ); - } - - /** Copy-paste Ender Crystal render code. {@link EnderDragonRenderer#renderCrystalBeams(float, float, float, float, int, MatrixStack, IRenderTypeBuffer, int)} */ - private void renderBeamAttack(float x, float y, float z, float partialTicks, int tickCount, MatrixStack matrixStack, IRenderTypeBuffer buffer, int packedLight) { - float f = MathHelper.sqrt(x * x + z * z); - float xyzMul = x * x + y * y + z * z; - float f1 = MathHelper.sqrt(xyzMul); - + + /** + * Copy-paste Ender Crystal render code. + * {@link EnderDragonRenderer#renderCrystalBeams(float, float, float, float, int, MatrixStack, IRenderTypeBuffer, int)} + */ + private void renderBeamAttack( RunicEndermanEntity.BeamState beamState, float offsetY, float dX, float dY, float dZ, + float partialTicks, int tickCount, MatrixStack matrixStack, IRenderTypeBuffer buffer, int packedLight ) { matrixStack.pushPose(); - matrixStack.translate(0.0D, 2.0D, 0.0D); - matrixStack.mulPose(Vector3f.YP.rotation((float)(-Math.atan2(z, x)) - ((float)Math.PI / 2F))); - matrixStack.mulPose(Vector3f.XP.rotation((float)(-Math.atan2(f, y)) - ((float)Math.PI / 2F))); - - IVertexBuilder ivertexbuilder = buffer.getBuffer(BEAM); - - float f2 = 0.0F - ((float)tickCount + partialTicks) * 0.01F; - float f3 = MathHelper.sqrt(xyzMul) / 32.0F - ((float)tickCount + partialTicks) * 0.01F; - float f4 = 0.0F; - float f5 = 0.75F; - float f6 = 0.0F; - - MatrixStack.Entry entry = matrixStack.last(); - Matrix4f matrix4f = entry.pose(); - Matrix3f matrix3f = entry.normal(); - - for(int j = 1; j <= 8; ++j) { - float f7 = MathHelper.sin((float)j * ((float)Math.PI * 2F) / 8.0F) * 0.75F; - float f8 = MathHelper.cos((float)j * ((float)Math.PI * 2F) / 8.0F) * 0.75F; - float f9 = (float)j / 8.0F; - - ivertexbuilder.vertex(matrix4f, f4 * 0.2F, f5 * 0.2F, 0.0F).color(0, 0, 0, 255).uv(f6, f2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(packedLight).normal(matrix3f, 0.0F, -1.0F, 0.0F).endVertex(); - ivertexbuilder.vertex(matrix4f, f4, f5, f1).color(255, 100, 255, 255).uv(f6, f3).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(packedLight).normal(matrix3f, 0.0F, -1.0F, 0.0F).endVertex(); - ivertexbuilder.vertex(matrix4f, f7, f8, f1).color(255, 100, 255, 255).uv(f9, f3).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(packedLight).normal(matrix3f, 0.0F, -1.0F, 0.0F).endVertex(); - ivertexbuilder.vertex(matrix4f, f7 * 0.2F, f8 * 0.2F, 0.0F).color(0, 0, 0, 255).uv(f9, f2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(packedLight).normal(matrix3f, 0.0F, -1.0F, 0.0F).endVertex(); - f4 = f7; - f5 = f8; - f6 = f9; + + final float dH = MathHelper.sqrt( dX * dX + dZ * dZ ); + final float length = MathHelper.sqrt( dX * dX + dY * dY + dZ * dZ ); + + matrixStack.translate( 0.0, offsetY, 0.0 ); + matrixStack.mulPose( Vector3f.YP.rotation( (float) -Math.atan2( dZ, dX ) - (float) Math.PI / 2.0F ) ); + matrixStack.mulPose( Vector3f.XP.rotation( (float) -Math.atan2( dH, dY ) - (float) Math.PI / 2.0F ) ); + + final IVertexBuilder vertexBuilder = buffer.getBuffer( BEAM ); + final MatrixStack.Entry matrixEntry = matrixStack.last(); + final Matrix4f pose = matrixEntry.pose(); + final Matrix3f normal = matrixEntry.normal(); + + final int eRB; + final int alpha; + final float eScale; + final float v1; + if( beamState == RunicEndermanEntity.BeamState.DAMAGING ) { + eRB = 255; + alpha = 255; + eScale = 1.0F; + v1 = (tickCount + partialTicks) * -0.01F; } + else { + eRB = 100; + alpha = 170; + eScale = 0.2F; + v1 = (tickCount + partialTicks) * 0.006F; + } + final float v2 = length / 32.0F + v1; + + float u1 = 0.0F; + float x1 = 0.0F; + float y1 = 0.75F; + + final int resolution = 8; + for( int n = 1; n <= resolution; n++ ) { + final float u2 = (float) n / resolution; + final float angle = u2 * (float) Math.PI * 2.0F; + final float x2 = MathHelper.sin( angle ) * 0.75F; + final float y2 = MathHelper.cos( angle ) * 0.75F; + + vertexBuilder.vertex( pose, x1 * 0.2F, y1 * 0.2F, 0.0F ) + .color( 0, 0, 0, alpha ) + .uv( u1, v1 ).overlayCoords( OverlayTexture.NO_OVERLAY ).uv2( packedLight ) + .normal( normal, 0.0F, -1.0F, 0.0F ).endVertex(); + vertexBuilder.vertex( pose, x1 * eScale, y1 * eScale, length ) + .color( eRB, 100, eRB, alpha ) + .uv( u1, v2 ).overlayCoords( OverlayTexture.NO_OVERLAY ).uv2( packedLight ) + .normal( normal, 0.0F, -1.0F, 0.0F ).endVertex(); + vertexBuilder.vertex( pose, x2 * eScale, y2 * eScale, length ) + .color( eRB, 100, eRB, alpha ) + .uv( u2, v2 ).overlayCoords( OverlayTexture.NO_OVERLAY ).uv2( packedLight ) + .normal( normal, 0.0F, -1.0F, 0.0F ).endVertex(); + vertexBuilder.vertex( pose, x2 * 0.2F, y2 * 0.2F, 0.0F ) + .color( 0, 0, 0, alpha ) + .uv( u2, v1 ).overlayCoords( OverlayTexture.NO_OVERLAY ).uv2( packedLight ) + .normal( normal, 0.0F, -1.0F, 0.0F ).endVertex(); + + x1 = x2; + y1 = y2; + u1 = u2; + } + matrixStack.popPose(); } -} +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java b/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java index b93e98e..405b8d7 100644 --- a/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java +++ b/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java @@ -276,12 +276,8 @@ public class BestiaryInfo { immuneToStickyBlocks.addAll( parent.immuneToStickyBlocks.getEntries() ); immuneToPotions.addAll( parent.immuneToPotions.getEntries() ); - rangedAttackDamage = parent.rangedAttackDamage; - rangedAttackSpread = parent.rangedAttackSpread; - rangedWalkSpeed = parent.rangedWalkSpeed; - rangedAttackCooldown = parent.rangedAttackCooldown; - rangedAttackMaxCooldown = parent.rangedAttackMaxCooldown; - rangedAttackMaxRange = parent.rangedAttackMaxRange; + setAllRangedStats( parent.rangedAttackDamage, parent.rangedAttackSpread, parent.rangedWalkSpeed, + parent.rangedAttackCooldown, parent.rangedAttackMaxCooldown, parent.rangedAttackMaxRange ); } } @@ -593,18 +589,14 @@ public class BestiaryInfo { .multiplyRangedCooldown( cooldown ).multiplyRangedMaxCooldown( cooldown ).multiplyRangedMaxRange( range ); } - /** Converts the entity to a fishing rod user by disabling unused ranged attack stats (for a bow user). */ - public Builder convertBowToFishing() { return rangedDamage( -1.0 ).rangedWalkSpeed( -1.0 ); } + /** Converts the entity's ranged attack stats to those of a fishing rod user. */ + public Builder convertRangedAttackToFishing( double spread, int cooldown, double range ) { + return setAllRangedStats( -1.0, spread, -1.0, cooldown, -1, range ); + } - /** Converts the entity to a fishing rod user by disabling unused ranged attack stats (for a throwing item user). */ - public Builder convertThrowToFishing() { return rangedWalkSpeed( -1.0 ); } - - /** Converts the entity to a fishing rod user by disabling unused ranged attack stats (for a spit shooter). */ - public Builder convertSpitToFishing() { return rangedDamage( -1.0 ).rangedMaxCooldown( -1 ); } - - /** Sets the species fishing rod user stats. */ - public Builder fishingAttack( double spread, int cooldown, double range ) { - return rangedSpread( spread ).rangedCooldown( cooldown ).rangedMaxRange( range ); + /** Converts the entity's ranged attack stats to those of a beam attack user. */ + public Builder convertRangedAttackToBeam( double damage, double turnSpeed, int charge, int duration, double range ) { + return setAllRangedStats( damage, -1.0, turnSpeed, charge, charge + duration, range ); } /** Sets the species as unable to use ranged attacks (for any ranged user). */ @@ -679,6 +671,17 @@ public class BestiaryInfo { return this; } + /** Converts ALL the entity's ranged attack stats. This method allows enabling/disabling stats on variants. */ + private Builder setAllRangedStats( double damage, double spread, double walkSpeed, int cooldown, int maxCooldown, double range ) { + rangedAttackDamage = damage; + rangedAttackSpread = spread; + rangedWalkSpeed = walkSpeed; + rangedAttackCooldown = cooldown; + rangedAttackMaxCooldown = maxCooldown; + rangedAttackMaxRange = range; + return this; + } + //--------------- Attribute Changes ---------------- diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/RunicEndermanBeamAttackGoal.java b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/RunicEndermanBeamAttackGoal.java index cc39393..826606a 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/RunicEndermanBeamAttackGoal.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/ai/goal/RunicEndermanBeamAttackGoal.java @@ -1,68 +1,159 @@ package fathertoast.specialmobs.common.entity.ai.goal; +import fathertoast.specialmobs.common.entity.MobHelper; import fathertoast.specialmobs.common.entity.enderman.RunicEndermanEntity; +import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.ai.goal.Goal; import net.minecraft.util.DamageSource; +import net.minecraft.util.EntityDamageSource; +import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RayTraceContext; +import net.minecraft.util.math.RayTraceResult; +import net.minecraft.util.math.vector.Vector3d; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; public class RunicEndermanBeamAttackGoal extends Goal { - - private final RunicEndermanEntity runicEnderman; - private final int maxAttackTime; - private final double baseDamage; - + private final RunicEndermanEntity mob; + private final DamageSource beamDamageSource; + + private Vector3d targetPos; private int attackTime; - - public RunicEndermanBeamAttackGoal(RunicEndermanEntity runicEnderman, int maxAttackTime, double baseDamage ) { - this.runicEnderman = runicEnderman; - this.maxAttackTime = maxAttackTime; - this.baseDamage = baseDamage; + + public RunicEndermanBeamAttackGoal( RunicEndermanEntity entity ) { + mob = entity; + beamDamageSource = new EntityDamageSource( DamageSource.MAGIC.getMsgId(), entity ); + setFlags( EnumSet.of( Goal.Flag.MOVE, Flag.LOOK ) ); } - + + /** @return Returns true if this AI can be activated. */ @Override public boolean canUse() { - if (runicEnderman.isVehicle()) - return false; - - LivingEntity target = runicEnderman.getTarget(); - - return target != null - && target.isAlive() - && runicEnderman.getSensing().canSee(target) - && (runicEnderman.tickCount % 8 == 0 && runicEnderman.getRandom().nextInt(10) == 0); + if( mob.isVehicle() ) return false; + + final LivingEntity target = mob.getTarget(); + if( target != null && target.isAlive() && mob.getSensing().canSee( target ) && + mob.tickCount % 8 == 0 && mob.getRandom().nextInt( 10 ) == 0 && target.distanceToSqr( mob ) <= + mob.getSpecialData().getRangedAttackMaxRange() * mob.getSpecialData().getRangedAttackMaxRange() ) { + setTargetPos( target ); + attackTime = 0; + mob.setBeamState( RunicEndermanEntity.BeamState.CHARGING ); + return true; + } + return false; } - + + /** @return Called each update while active and returns true if this AI can remain active. */ @Override public boolean canContinueToUse() { - LivingEntity target = runicEnderman.getTarget(); - - return attackTime > 0 && target != null && target.isAlive() && runicEnderman.getSensing().canSee(target); + final LivingEntity target = mob.getTarget(); + return (mob.getBeamState() == RunicEndermanEntity.BeamState.DAMAGING || target != null && target.isAlive()) && + attackTime < mob.getSpecialData().getRangedAttackMaxCooldown(); } - - @Override - public void start() { - runicEnderman.setBeamTargetId(runicEnderman.getTarget().getId()); - attackTime = maxAttackTime; - } - + + /** Called when this AI is deactivated. */ @Override public void stop() { - runicEnderman.clearBeamTargetId(); - attackTime = 0; + targetPos = null; + mob.setBeamState( RunicEndermanEntity.BeamState.OFF ); } - - @SuppressWarnings("ConstantConditions") + + /** Called each tick while this AI is active. */ @Override public void tick() { - --attackTime; - - if (attackTime <= 0) { - LivingEntity target = runicEnderman.getTarget(); - double xRatio = MathHelper.sin(runicEnderman.yRot * ((float)Math.PI / 180F)); - double zRatio = -MathHelper.cos(runicEnderman.yRot * ((float)Math.PI / 180F)); - target.knockback(5, xRatio, zRatio); - target.hurt(DamageSource.mobAttack(runicEnderman), (float) baseDamage); + attackTime++; + switch( mob.getBeamState() ) { + case CHARGING: + chargingTick(); + break; + case DAMAGING: + damagingTick(); + break; } } -} + + /** Called each tick while this AI is active and in the charging phase. */ + private void chargingTick() { + mob.getLookControl().setLookAt( targetPos.x, targetPos.y, targetPos.z, + 30.0F, mob.getMaxHeadXRot() ); + + if( attackTime >= mob.getSpecialData().getRangedAttackCooldown() ) { + mob.setBeamState( RunicEndermanEntity.BeamState.DAMAGING ); + } + } + + /** Called each tick while this AI is active and in the damaging phase. */ + private void damagingTick() { + final LivingEntity target = mob.getTarget(); + if( target != null ) setTargetPos( target ); + + lookTowardTarget( 1.666F * mob.getSpecialData().getRangedWalkSpeed() ); // Use move speed as a turn rate modifier + final Vector3d viewVec = mob.getViewVector( 1.0F ).scale( RunicEndermanEntity.BEAM_MAX_RANGE ); + + final Vector3d beamStartPos = mob.getEyePosition( 1.0F ); + Vector3d beamEndPos = beamStartPos.add( viewVec ); + + final RayTraceResult blockRayTrace = mob.level.clip( + new RayTraceContext( beamStartPos, beamEndPos, + RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, mob ) ); + if( blockRayTrace.getType() != RayTraceResult.Type.MISS ) { + beamEndPos = blockRayTrace.getLocation(); + } + + final List hitEntities = rayTraceEntities( beamStartPos, beamEndPos, + mob.getBoundingBox().expandTowards( viewVec ).inflate( 1.0 ) ); + for( Entity entity : hitEntities ) { + if( entity.hurt( beamDamageSource, mob.getSpecialData().getRangedAttackDamage() ) && + entity instanceof LivingEntity ) { + MobHelper.knockback( (LivingEntity) entity, 1.0F, 1.0F, + -viewVec.x, -viewVec.z, 1.0 ); + } + } + } + + /** Updates the target position based on an entity we want to hit. */ + private void setTargetPos( LivingEntity target ) { + targetPos = new Vector3d( target.getX(), target.getY( 0.5 ), target.getZ() ); + } + + /** Sets the target look position. Modifies the target position to limit vertical look speed. */ + private void lookTowardTarget( float speed ) { + final double dX = targetPos.x - mob.getX(); + final double dY = targetPos.y - mob.getEyeY(); + final double dZ = targetPos.z - mob.getZ(); + final double dH = MathHelper.sqrt( dX * dX + dZ * dZ ); + final float targetXRot = (float) MathHelper.atan2( dY, dH ); + + final float clampedXRot = (mob.xRot + MathHelper.clamp( MathHelper.degreesDifference( mob.xRot, + targetXRot * -57.2957763671875F ), -speed, speed )) / -57.2957763671875F; + final double clampedDY = dH * MathHelper.sin( clampedXRot ) / MathHelper.cos( clampedXRot ); + targetPos = targetPos.add( 0.0, clampedDY - dY, 0.0 ); + + mob.getLookControl().setLookAt( targetPos.x, targetPos.y, targetPos.z, + speed, mob.getMaxHeadXRot() ); + } + + /** @return A list of all entities between the two vector positions and within the search area, in no particular order. */ + private List rayTraceEntities( Vector3d from, Vector3d to, AxisAlignedBB searchArea ) { + final List entitiesHit = new ArrayList<>(); + for( Entity entity : mob.level.getEntities( mob, searchArea, this::canBeamHitTarget ) ) { + final Optional hitPos = entity.getBoundingBox().inflate( 0.3 ).clip( from, to ); + if( hitPos.isPresent() ) { + entitiesHit.add( entity ); + } + } + return entitiesHit; + } + + /** @return True if the beam can hit the target. */ + private boolean canBeamHitTarget( @Nullable Entity entity ) { + return entity != null && !entity.isSpectator() && entity.isAlive() && entity.isPickable() && + !entity.isPassengerOfSameVehicle( mob ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/drowned/FishingDrownedEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/drowned/FishingDrownedEntity.java index 384c09f..1f86520 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/drowned/FishingDrownedEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/drowned/FishingDrownedEntity.java @@ -42,7 +42,7 @@ public class FishingDrownedEntity extends _SpecialDrownedEntity implements IAngl public static void getBestiaryInfo( BestiaryInfo.Builder bestiaryInfo ) { bestiaryInfo.color( 0x2D41F4 ).weight( BestiaryInfo.DefaultWeight.LOW ) .addExperience( 2 ).drownImmune().fluidPushImmune() - .convertThrowToFishing().fishingAttack( 1.0, 40, 15.0 ) + .convertRangedAttackToFishing( 1.0, 40, 15.0 ) .multiplyAttribute( Attributes.MOVEMENT_SPEED, 0.8 ); } diff --git a/src/main/java/fathertoast/specialmobs/common/entity/enderman/RunicEndermanEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/enderman/RunicEndermanEntity.java index 83c2220..61f9abf 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/enderman/RunicEndermanEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/enderman/RunicEndermanEntity.java @@ -4,6 +4,7 @@ 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.RunicEndermanBeamAttackGoal; import fathertoast.specialmobs.common.util.References; import fathertoast.specialmobs.datagen.loot.LootTableBuilder; @@ -16,9 +17,10 @@ import net.minecraft.network.datasync.DataParameter; import net.minecraft.network.datasync.DataSerializers; import net.minecraft.network.datasync.EntityDataManager; import net.minecraft.potion.Effects; +import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.world.World; - -import java.util.OptionalInt; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; @SpecialMob public class RunicEndermanEntity extends _SpecialEndermanEntity { @@ -33,10 +35,11 @@ public class RunicEndermanEntity extends _SpecialEndermanEntity { bestiaryInfo.color( 0xE42281 ).weight( BestiaryInfo.DefaultWeight.LOW ).theme( BestiaryInfo.Theme.MOUNTAIN ) .uniqueTextureWithEyes() .addExperience( 2 ).fallImmune().burnImmune() + .convertRangedAttackToBeam( 2.0, 1.0, 60, 100, 16.0 ) .addToAttribute( Attributes.ARMOR, 10.0 ) .addToAttribute( Attributes.ATTACK_DAMAGE, 1.0 ) .multiplyAttribute( Attributes.MOVEMENT_SPEED, 0.8 ); - + } @SpecialMob.LanguageProvider @@ -62,42 +65,53 @@ public class RunicEndermanEntity extends _SpecialEndermanEntity { //--------------- Variant-Specific Implementations ---------------- - - public static final DataParameter BEAMING = EntityDataManager.defineId(RunicEndermanEntity.class, DataSerializers.BOOLEAN); - public static final DataParameter TARGET_ID = EntityDataManager.defineId(RunicEndermanEntity.class, DataSerializers.OPTIONAL_UNSIGNED_INT); - + + /** The actual range of the beam when fired. Roughly based on end crystal max range. */ + public static final double BEAM_MAX_RANGE = 32.0; + /** The parameter for beam attack state. */ + private static final DataParameter BEAM_STATE = EntityDataManager.defineId( RunicEndermanEntity.class, DataSerializers.BYTE ); + public RunicEndermanEntity( EntityType entityType, World world ) { super( entityType, world ); } - + + /** Called from the Entity.class constructor to define data watcher variables. */ @Override protected void defineSynchedData() { super.defineSynchedData(); - entityData.define( BEAMING, false ); - entityData.define( TARGET_ID, OptionalInt.empty() ); + entityData.define( BEAM_STATE, BeamState.OFF.id() ); } - - public OptionalInt getBeamTargetId() { - return entityData.get( TARGET_ID ); + + /** Override to change this entity's AI goals. */ + @Override + protected void registerVariantGoals() { + AIHelper.insertGoal( goalSelector, 2, new RunicEndermanBeamAttackGoal( this ) ); } - - public void setBeamTargetId(int targetId) { - entityData.set( TARGET_ID, OptionalInt.of(targetId) ); - } - - public void clearBeamTargetId() { - entityData.set( TARGET_ID, OptionalInt.empty() ); - } - + /** Override to apply effects when this entity hits a target with a melee attack. */ @Override protected void onVariantAttack( LivingEntity target ) { MobHelper.applyEffect( target, Effects.LEVITATION ); MobHelper.knockback( this, target, 2.0F, 0.0F ); } - - /** Override to change this entity's AI goals. */ + + /** @return True if this enderman is using its beam attack. */ + public BeamState getBeamState() { return BeamState.of( entityData.get( BEAM_STATE ) ); } + + /** Sets whether this enderman is using its beam attack. */ + public void setBeamState( BeamState value ) { entityData.set( BEAM_STATE, value.id() ); } + + /** @return The bounding box to use for frustum culling. */ + @OnlyIn( Dist.CLIENT ) @Override - protected void registerVariantGoals() { - super.registerVariantGoals(); - goalSelector.addGoal( 2, new RunicEndermanBeamAttackGoal(this, 50, 2.0D)); + public AxisAlignedBB getBoundingBoxForCulling() { + return getBeamState() == BeamState.OFF ? super.getBoundingBoxForCulling() : + super.getBoundingBoxForCulling().expandTowards( getViewVector( 1.0F ).scale( BEAM_MAX_RANGE ) ); + } + + public enum BeamState { + OFF, CHARGING, DAMAGING; + + public byte id() { return (byte) ordinal(); } + + public static BeamState of( byte id ) { return values()[id]; } } } \ 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 5414294..9bdd3be 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/silverfish/FishingSilverfishEntity.java @@ -35,7 +35,7 @@ public class FishingSilverfishEntity extends AmphibiousSilverfishEntity implemen .uniqueTextureBaseOnly() .size( 1.2F, 0.5F, 0.4F ) .addExperience( 2 ).drownImmune().fluidPushImmune() - .convertSpitToFishing().fishingAttack( 1.0, 40, 10.0 ) + .convertRangedAttackToFishing( 1.0, 40, 10.0 ) .addToAttribute( Attributes.MAX_HEALTH, 4.0 ) .multiplyAttribute( Attributes.MOVEMENT_SPEED, 0.8 ); } diff --git a/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java index 6a464da..c4a1461 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/zombie/FishingZombieEntity.java @@ -46,7 +46,7 @@ public class FishingZombieEntity extends _SpecialZombieEntity implements IAngler public static void getBestiaryInfo( BestiaryInfo.Builder bestiaryInfo ) { bestiaryInfo.color( 0x2D41F4 ).weight( BestiaryInfo.DefaultWeight.LOW ).theme( BestiaryInfo.Theme.FISHING ) .addExperience( 2 ).drownImmune().fluidPushImmune() - .convertBowToFishing().fishingAttack( 1.0, 40, 15.0 ) + .convertRangedAttackToFishing( 1.0, 40, 15.0 ) .multiplyAttribute( Attributes.MOVEMENT_SPEED, 0.8 ); } diff --git a/src/main/java/fathertoast/specialmobs/common/entity/zombifiedpiglin/FishingZombifiedPiglinEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/zombifiedpiglin/FishingZombifiedPiglinEntity.java index 2406f7a..9459255 100644 --- a/src/main/java/fathertoast/specialmobs/common/entity/zombifiedpiglin/FishingZombifiedPiglinEntity.java +++ b/src/main/java/fathertoast/specialmobs/common/entity/zombifiedpiglin/FishingZombifiedPiglinEntity.java @@ -44,7 +44,7 @@ public class FishingZombifiedPiglinEntity extends _SpecialZombifiedPiglinEntity public static void getBestiaryInfo( BestiaryInfo.Builder bestiaryInfo ) { bestiaryInfo.color( 0x2D41F4 ).weight( BestiaryInfo.DefaultWeight.LOW ).theme( BestiaryInfo.Theme.FISHING ) .addExperience( 2 ).drownImmune().fluidPushImmune() - .convertBowToFishing().fishingAttack( 1.0, 40, 15.0 ) + .convertRangedAttackToFishing( 1.0, 40, 15.0 ) .multiplyAttribute( Attributes.MOVEMENT_SPEED, 0.8 ); }