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);
}
}