Files
TiedUp-/src/main/java/com/tiedup/remake/entities/LeashProxyEntity.java
NotEvil 449178f57b feat(D-01/C): consumer migration — 85 files migrated to V2 helpers
Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
  PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
  handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
  SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
  BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
  PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
  PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
  KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
  BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
  BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
  MovementStyleResolver, CampLifecycleManager

Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
2026-04-15 00:16:50 +02:00

245 lines
6.8 KiB
Java

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