package net.Chipperfluff.chipi.entity; import net.minecraft.block.BlockState; import net.minecraft.block.SlabBlock; import net.minecraft.block.StairsBlock; import net.minecraft.entity.EntityType; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.ai.goal.ActiveTargetGoal; import net.minecraft.entity.ai.goal.LookAroundGoal; import net.minecraft.entity.ai.goal.LookAtEntityGoal; import net.minecraft.entity.ai.goal.MeleeAttackGoal; import net.minecraft.entity.ai.goal.SwimGoal; import net.minecraft.entity.ai.goal.WanderAroundFarGoal; import net.minecraft.entity.attribute.DefaultAttributeContainer; import net.minecraft.entity.attribute.EntityAttributes; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.mob.PathAwareEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.sound.SoundCategory; import net.minecraft.state.property.Properties; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.Chipperfluff.chipi.item.ModItems; import net.Chipperfluff.chipi.sound.ModSounds; import net.Chipperfluff.chipi.util.TickScheduler; public class MepEntity extends PathAwareEntity { private static final int FORGET_TARGET_AFTER_TICKS = 100; private boolean angryAtPlayer = false; private int ticksSinceLastSeen = 0; public MepEntity(EntityType entityType, World world) { super(entityType, world); } // === AI === @Override protected void initGoals() { this.goalSelector.add(1, new SwimGoal(this)); this.goalSelector.add(2, new MeleeAttackGoal(this, 1.2D, true)); this.goalSelector.add(3, new WanderAroundFarGoal(this, 1.0D)); this.goalSelector.add(4, new LookAtEntityGoal(this, PlayerEntity.class, 8.0F)); this.goalSelector.add(5, new LookAroundGoal(this)); this.targetSelector.add( 1, new ActiveTargetGoal<>( this, PlayerEntity.class, true, target -> target instanceof PlayerEntity player && !isPlayerProtected(player) ) ); } public static DefaultAttributeContainer.Builder createMepAttributes() { return PathAwareEntity.createMobAttributes() .add(EntityAttributes.GENERIC_MAX_HEALTH, 20.0) .add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 4.0) .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25) .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 32.0); } // === Combat memory === @Override public boolean damage(DamageSource source, float amount) { if (source.getAttacker() instanceof PlayerEntity) { angryAtPlayer = true; ticksSinceLastSeen = 0; } return super.damage(source, amount); } @Override public void tick() { super.tick(); LivingEntity target = this.getTarget(); if (!(target instanceof PlayerEntity player)) { angryAtPlayer = false; ticksSinceLastSeen = 0; return; } if (isPlayerProtected(player)) { clearTarget(); return; } if (this.canSee(player)) { ticksSinceLastSeen = 0; angryAtPlayer = true; this.getNavigation().startMovingTo(player, 1.2D); } else { ticksSinceLastSeen++; if (ticksSinceLastSeen <= FORGET_TARGET_AFTER_TICKS) { this.getNavigation().startMovingTo(player, 1.2D); } else { clearTarget(); } } } private void clearTarget() { angryAtPlayer = false; ticksSinceLastSeen = 0; this.setTarget(null); this.getNavigation().stop(); } private static boolean isPlayerProtected(PlayerEntity player) { BlockPos pos = player.getBlockPos(); BlockState state = player.getWorld().getBlockState(pos); if (!state.contains(Properties.WATERLOGGED) || !state.get(Properties.WATERLOGGED)) { return false; } return state.getBlock() instanceof StairsBlock || state.getBlock() instanceof SlabBlock; } // === Despawn prevention === @Override public boolean cannotDespawn() { return true; } @Override public boolean canImmediatelyDespawn(double distanceSquared) { return false; } // === MILKING (FEVER DREAM EDITION) === @Override public ActionResult interactMob(PlayerEntity player, Hand hand) { ItemStack stack = player.getStackInHand(hand); if (stack.isOf(Items.BUCKET) && !this.isBaby()) { if (!player.getWorld().isClient) { stack.decrement(1); player.giveItemStack(new ItemStack(ModItems.MEP_MILK)); // ---- base sound ---- float basePitch = 0.3f + this.random.nextFloat() * 1.9f; float baseVolume = 0.9f + this.random.nextFloat() * 0.6f; this.getWorld().playSound( null, this.getBlockPos(), ModSounds.MEP_MILK, SoundCategory.NEUTRAL, baseVolume, basePitch ); // ---- single delayed echo (10% chance) ---- if (this.random.nextFloat() < 0.10f) { int delay = 10 + this.random.nextInt(21); // 10–30 ticks float echoPitch = basePitch * 0.5f; float echoVolume = baseVolume * 0.5f; TickScheduler.schedule(delay, () -> { if (!this.isAlive()) return; this.getWorld().playSound( null, this.getBlockPos(), ModSounds.MEP_MILK, SoundCategory.NEUTRAL, echoVolume, echoPitch ); }); } } return ActionResult.SUCCESS; } return super.interactMob(player, hand); } }