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.
245 lines
6.8 KiB
Java
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;
|
|
}
|
|
}
|