package com.tiedup.remake.entities; import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.Level; /** * Lightweight fishing bobber entity spawned by NPCs during the FISH command. * *

Floats on the water surface with a sinusoidal bob animation. * After a random delay, triggers a bite event (splash particles + sound). * The owning NPC's goal checks {@link #isBiting()} each tick and reels in * when a bite is detected.

* *

Ephemeral entity: no NBT persistence. Auto-discards after {@value #MAX_LIFE} ticks.

*/ public class NpcFishingBobber extends Entity { private static final EntityDataAccessor DATA_OWNER_ID = SynchedEntityData.defineId( NpcFishingBobber.class, EntityDataSerializers.INT ); private static final EntityDataAccessor DATA_BITING = SynchedEntityData.defineId( NpcFishingBobber.class, EntityDataSerializers.BOOLEAN ); /** Safety removal after 30 seconds */ private static final int MAX_LIFE = 600; /** Minimum ticks before a bite can occur */ private static final int MIN_BITE_TIME = 100; /** Maximum ticks before a bite can occur */ private static final int MAX_BITE_TIME = 300; private int biteTimer; private int lifeTimer; /** * Registration constructor. */ public NpcFishingBobber(EntityType type, Level level) { super(type, level); } /** * Factory method: creates a bobber at the surface of the given water block. * * @param level The server level * @param owner The NPC that owns this bobber * @param waterPos The water block position (bobber sits on top) * @return A new NpcFishingBobber positioned at the water surface */ public static NpcFishingBobber create( Level level, EntityDamsel owner, BlockPos waterPos ) { NpcFishingBobber bobber = new NpcFishingBobber( ModEntities.NPC_FISHING_BOBBER.get(), level ); bobber.setPos( waterPos.getX() + 0.5, waterPos.getY() + 1.0, // surface of water waterPos.getZ() + 0.5 ); bobber.entityData.set(DATA_OWNER_ID, owner.getId()); bobber.biteTimer = MIN_BITE_TIME + owner.getRandom().nextInt(MAX_BITE_TIME - MIN_BITE_TIME); return bobber; } @Override protected void defineSynchedData() { this.entityData.define(DATA_OWNER_ID, 0); this.entityData.define(DATA_BITING, false); } @Override protected void readAdditionalSaveData(CompoundTag tag) { // Ephemeral entity - no persistence } @Override protected void addAdditionalSaveData(CompoundTag tag) { // Ephemeral entity - no persistence } @Override public void tick() { super.tick(); lifeTimer++; if (lifeTimer >= MAX_LIFE) { discard(); return; } // Sinusoidal bob on water surface setDeltaMovement(0, Math.sin(tickCount * 0.15) * 0.01, 0); // Server-side bite logic if (!level().isClientSide && !isBiting()) { biteTimer--; if (biteTimer <= 0) { setBiting(true); // Splash particles if (level() instanceof ServerLevel serverLevel) { serverLevel.sendParticles( ParticleTypes.FISHING, getX(), getY(), getZ(), 5, // count 0.1, 0.0, 0.1, // spread 0.02 // speed ); } // Splash sound level().playSound( null, getX(), getY(), getZ(), SoundEvents.FISHING_BOBBER_SPLASH, SoundSource.NEUTRAL, 1.0f, 1.0f ); } } } /** * Get the NPC that owns this bobber. * * @return The owner EntityDamsel, or null if not found */ public EntityDamsel getOwnerNpc() { Entity entity = level().getEntity(entityData.get(DATA_OWNER_ID)); return entity instanceof EntityDamsel damsel ? damsel : null; } /** * Whether a fish is biting this bobber. */ public boolean isBiting() { return entityData.get(DATA_BITING); } /** * Set the biting state. */ public void setBiting(boolean biting) { entityData.set(DATA_BITING, biting); } @Override public boolean isPickable() { return false; } @Override public Packet getAddEntityPacket() { return new ClientboundAddEntityPacket(this); } }