198 lines
6.2 KiB
Java
198 lines
6.2 KiB
Java
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<? extends PathAwareEntity> 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);
|
||
}
|
||
}
|