package com.tiedup.remake.entities; import com.tiedup.remake.v2.bondage.PoseTypeHelper; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.v2.BodyRegionV2; import java.util.Objects; import javax.annotation.ParametersAreNonnullByDefault; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerScoreboard; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.animal.Turtle; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.Vec3; import net.minecraft.world.scores.PlayerTeam; /** * Invisible proxy entity that follows a player and holds the leash. * * Based on PlayerCollars LeashProxyEntity implementation. * Uses Turtle as base because it's a simple, leashable mob. * * Key features: * - Baby turtle (small hitbox) * - Invisible and invulnerable * - No physics or collision * - Follows target player's position (offset to neck height) * - Leash renders from this entity to the holder */ @ParametersAreNonnullByDefault public final class LeashProxyEntity extends Turtle { /** Team name for preventing collision display */ public static final String TEAM_NAME = "tiedup_leash_proxy"; /** The player this proxy follows */ private final LivingEntity target; /** * Create a new leash proxy for a target player. * * @param target The player to follow */ public LeashProxyEntity(LivingEntity target) { super(EntityType.TURTLE, target.level()); this.target = target; // Make it invisible and invulnerable setHealth(1.0F); setInvulnerable(true); setBaby(true); setInvisible(true); noPhysics = true; // Add to team to prevent collision nameplate display MinecraftServer server = getServer(); if (server != null) { ServerScoreboard scoreboard = server.getScoreboard(); PlayerTeam team = scoreboard.getPlayerTeam(TEAM_NAME); if (team == null) { team = scoreboard.addPlayerTeam(TEAM_NAME); } if (team.getCollisionRule() != PlayerTeam.CollisionRule.NEVER) { team.setCollisionRule(PlayerTeam.CollisionRule.NEVER); } scoreboard.addPlayerToTeam(getScoreboardName(), team); } } // ==================== Position Sync ==================== /** * Update proxy position to match target. * * @return true if proxy should be removed (target invalid) */ private boolean proxyUpdate() { if (proxyIsRemoved()) return false; if (target == null) return true; if (target.level() != level() || !target.isAlive()) return true; // If target is a player who disconnected, remove proxy if ( target instanceof ServerPlayer serverPlayer && serverPlayer.hasDisconnected() ) { return true; } Vec3 posActual = this.position(); // Dynamic Y offset based on bind type // DOGBINDER = lower height for 4-legged pose (0.6) // Default = neck height (1.3) double yOffset = 1.3D; if (target instanceof ServerPlayer player) { IBondageState state = KidnappedHelper.getKidnappedState(player); if (state != null && state.isTiedUp()) { ItemStack bind = state.getEquipment(BodyRegionV2.ARMS); if ( !bind.isEmpty() && PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG ) { yOffset = 0.35D; // Lower for 4-legged dogwalk pose (back/hip level) } } } Vec3 posTarget = target.position().add(0.0D, yOffset, -0.15D); if (!Objects.equals(posActual, posTarget)) { setRot(0.0F, 0.0F); setPos(posTarget.x(), posTarget.y(), posTarget.z()); setBoundingBox( getDimensions(Pose.DYING).makeBoundingBox(posTarget) ); } // Update leash (handles vanilla leash physics) tickLeash(); return false; } @Override public void tick() { if (this.level().isClientSide) return; if (proxyUpdate() && !proxyIsRemoved()) { proxyRemove(); } } // ==================== Lifecycle ==================== /** * Check if this proxy has been removed. */ public boolean proxyIsRemoved() { return this.isRemoved(); } /** * Remove this proxy entity. */ public void proxyRemove() { super.remove(RemovalReason.DISCARDED); } /** * Override remove() to prevent accidental removal. * Use proxyRemove() for intentional removal. */ @Override public void remove(RemovalReason reason) { // Only allow removal via proxyRemove() or if truly killed if ( reason == RemovalReason.KILLED || reason == RemovalReason.CHANGED_DIMENSION ) { super.remove(reason); } // Ignore other removal attempts } // ==================== Override to Prevent Behavior ==================== @Override public float getHealth() { return 1.0F; } @Override public void dropLeash(boolean sendPacket, boolean dropItem) { // Don't drop leash - managed by mixin } @Override public boolean canBeLeashed(Player player) { return false; } @Override protected void registerGoals() { // No AI goals } @Override protected void doPush(Entity entity) { // No pushing } @Override public void push(Entity entity) { // No pushing } @Override public void playerTouch(Player player) { // No interaction } @Override public boolean isPushable() { return false; } @Override protected Vec3 getLeashOffset() { // The proxy is already positioned at the player's neck (Y+1.3) // No additional offset needed - leash attaches exactly at proxy position return Vec3.ZERO; } @Override public boolean isInvulnerableTo( net.minecraft.world.damagesource.DamageSource source ) { return true; } @Override public boolean hurt( net.minecraft.world.damagesource.DamageSource source, float amount ) { return false; } /** * Get the target entity this proxy follows. */ public LivingEntity getTarget() { return target; } }