Files
TiedUp-/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java
NotEvil 099cd0d984 feat(D-01/D): V1 cleanup — delete 28 files, ~5400 lines removed
D1: ThreadLocal alert suppression moved from ItemCollar to CollarHelper.
    onCollarRemoved() logic (kidnapper alert) moved to CollarHelper.

D2+D3: Deleted 17 V1 item classes + 4 V1-only interfaces:
  ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, ItemMittens,
  ItemColor, ItemClassicCollar, ItemShockCollar, ItemShockCollarAuto,
  ItemGpsCollar, ItemChokeCollar, ItemHood, ItemMedicalGag,
  IBondageItem, IHasGaggingEffect, IHasBlindingEffect, IAdjustable

D4: KidnapperTheme/KidnapperItemSelector/DispenserBehaviors migrated
    from variant enums to string-based DataDrivenItemRegistry IDs.

D5: Deleted 11 variant enums + Generic* factories + ItemBallGag3D:
  BindVariant, GagVariant, BlindfoldVariant, EarplugsVariant, MittensVariant,
  GenericBind, GenericGag, GenericBlindfold, GenericEarplugs, GenericMittens

D6: ModItems cleaned — all V1 bondage registrations removed.
D7: ModCreativeTabs rewritten — iterates DataDrivenItemRegistry.
D8+D9: All V2 helpers cleaned (V1 fallbacks removed), orphan imports removed.

Zero V1 bondage code references remain (only Javadoc comments).
All bondage items are now data-driven via 47 JSON definitions.
2026-04-15 01:55:16 +02:00

1268 lines
36 KiB
Java

package com.tiedup.remake.entities;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.util.tasks.ItemTask;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.util.teleport.TeleportHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.IV2EquipmentHolder;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.api.layered.IAnimation;
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* AbstractTiedUpNpc - Base class for all TiedUp! humanoid NPCs.
*
* so that EntityKidnapper can extend this directly (instead of incorrectly extending EntityDamsel).
*
* <p><b>Shared functionality:</b></p>
* <ul>
* <li>IRestrainable bondage delegation (via DamselBondageManager)</li>
* <li>V2 equipment system (IV2EquipmentHolder)</li>
* <li>Animation system (IAnimatedPlayer via DamselAnimationController)</li>
* <li>Inventory/equipment slots (via DamselInventoryManager)</li>
* <li>Pose state (sitting, kneeling, dog, struggling)</li>
* <li>Name system (getNpcName/setNpcName)</li>
* <li>Gender/slim arms</li>
* <li>Despawn protection</li>
* <li>Leash offset</li>
* </ul>
*
* <p><b>Subclass-specific (NOT in this class):</b></p>
* <ul>
* <li>Personality system (EntityDamsel)</li>
* <li>Variant/appearance (each subclass has its own)</li>
* <li>AI goals (each subclass registers its own)</li>
* <li>Dialogue handler (EntityDamsel)</li>
* <li>MenuProvider (EntityDamsel)</li>
* </ul>
*
* @see EntityDamsel
* @see EntityKidnapper
*/
public abstract class AbstractTiedUpNpc
extends PathfinderMob
implements
IRestrainable,
ISkinnedEntity,
IAnimatedPlayer,
com.tiedup.remake.dialogue.IDialogueSpeaker,
IV2EquipmentHolder
{
// DATA SYNC (Client-Server) - Shared across all NPC types
/**
* NPC's custom name.
* Synced to client for display.
*/
public static final EntityDataAccessor<String> DATA_DAMSEL_NAME =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.STRING
);
/**
* Variant ID (e.g., "classic_1", "guest_fuya_kitty").
* Synced to client for texture selection.
*/
public static final EntityDataAccessor<String> DATA_VARIANT_ID =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.STRING
);
/**
* V2 bondage equipment -- single CompoundTag replacing 7 individual ItemStack accessors.
* Epic 4B: Serialized from internal V2BondageEquipment via copy-on-write pattern.
* Synced to client for rendering (client deserializes in onSyncedDataUpdated).
*/
public static final EntityDataAccessor<CompoundTag> DATA_V2_EQUIPMENT =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.COMPOUND_TAG
);
/**
* Whether this entity uses slim arm model.
* Synced to client for correct model rendering.
*/
public static final EntityDataAccessor<Boolean> DATA_SLIM_ARMS =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.BOOLEAN
);
/**
* Gender of the entity (MALE/FEMALE).
* Synced to client for voice, behavior, and model rendering.
*/
public static final EntityDataAccessor<String> DATA_GENDER =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.STRING
);
/**
* Is NPC currently in sitting pose?
* Synced to client for animation/rendering.
*/
public static final EntityDataAccessor<Boolean> DATA_SITTING =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.BOOLEAN
);
/**
* Is NPC currently in kneeling pose?
* Synced to client for animation/rendering.
*/
public static final EntityDataAccessor<Boolean> DATA_KNEELING =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.BOOLEAN
);
/**
* Is NPC currently struggling against restraints?
* Synced to client for struggle animation.
*/
public static final EntityDataAccessor<Boolean> DATA_STRUGGLING =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.BOOLEAN
);
/**
* Main hand item.
* Synced to client for rendering and behavior determination.
*/
public static final EntityDataAccessor<ItemStack> DATA_MAIN_HAND =
SynchedEntityData.defineId(
AbstractTiedUpNpc.class,
EntityDataSerializers.ITEM_STACK
);
// COMPONENTS (shared across all NPC types)
/**
* Manages inventory, equipment, and feeding.
*/
private final DamselInventoryManager inventoryManager;
/**
* Manages all bondage and captivity mechanics.
*/
private final DamselBondageManager bondageManager;
/**
* V2 bondage equipment storage (internal, not a Forge capability for entities).
* Epic 4B: Replaces 7 EntityDataAccessor<ItemStack> with single CompoundTag sync.
*/
private final com.tiedup.remake.v2.bondage.capability.V2BondageEquipment v2Equipment =
new com.tiedup.remake.v2.bondage.capability.V2BondageEquipment();
/**
* Manages all animation-related systems:
* - IAnimatedPlayer implementation
* - Animation stack management
* - Pose management (sitting, kneeling, dog, struggling, trembling)
* - Dog pose rotation smoothing
*/
private DamselAnimationController animationController;
// CONSTRUCTOR
public AbstractTiedUpNpc(
EntityType<? extends AbstractTiedUpNpc> type,
Level level
) {
super(type, level);
this.setCanPickUpLoot(false);
// Initialize inventory component
this.inventoryManager = new DamselInventoryManager(this);
// Initialize bondage component
this.bondageManager = new DamselBondageManager(
this,
createBondageHost()
);
// Initialize animation controller component
this.animationController = new DamselAnimationController(
createAnimationHost()
);
// Allow NPC to pathfind through doors
if (
this.getNavigation() instanceof
net.minecraft.world.entity.ai.navigation.GroundPathNavigation groundNav
) {
groundNav.setCanOpenDoors(true);
groundNav.setCanPassDoors(true);
}
// Allow pathfinding through decorative blocks (snow, leaves, etc.)
this.setPathfindingMalus(
net.minecraft.world.level.pathfinder.BlockPathTypes.POWDER_SNOW,
0.0f
);
this.setPathfindingMalus(
net.minecraft.world.level.pathfinder.BlockPathTypes.LEAVES,
0.0f
);
this.setPathfindingMalus(
net.minecraft.world.level.pathfinder.BlockPathTypes.DANGER_POWDER_SNOW,
0.0f
);
}
/**
* Create the bondage host for this NPC type.
* Subclasses MUST override this because the default BondageHost
* requires EntityDamsel-specific methods (personality, dialogue).
*
* NOTE: Called during super() constructor -- subclass fields are NOT initialized yet.
* The host implementation must only store the reference, not access fields.
*/
protected abstract IBondageHost createBondageHost();
/**
* Create the animation host for this NPC type.
* Subclasses can override if they need different host behavior.
* Default creates an AnimationHost that delegates to this entity.
*/
protected IAnimationHost createAnimationHost() {
return new com.tiedup.remake.entities.damsel.hosts.AnimationHost(this);
}
// ABSTRACT METHODS - Subclasses must implement
/**
* Get the skin texture for this entity.
* Each NPC type has its own variant/skin system.
*/
@Override
public abstract ResourceLocation getSkinTexture();
/**
* Register AI goals specific to this NPC type.
* Called after all components are initialized.
* Avoids name clash with Mob.registerGoals().
*/
protected abstract void registerNpcGoals();
/**
* Get the speaker type for dialogue routing.
* Each NPC type returns its own SpeakerType.
*/
@Override
public abstract com.tiedup.remake.dialogue.SpeakerType getSpeakerType();
// ENTITY INITIALIZATION
@Override
protected void defineSynchedData() {
super.defineSynchedData();
this.entityData.define(DATA_DAMSEL_NAME, "");
this.entityData.define(DATA_VARIANT_ID, "");
this.entityData.define(DATA_V2_EQUIPMENT, new CompoundTag());
this.entityData.define(DATA_SLIM_ARMS, false);
this.entityData.define(DATA_GENDER, Gender.FEMALE.getSerializedName());
// Pose states
this.entityData.define(DATA_SITTING, false);
this.entityData.define(DATA_KNEELING, false);
this.entityData.define(DATA_STRUGGLING, false);
// Equipment
this.entityData.define(DATA_MAIN_HAND, ItemStack.EMPTY);
}
@Override
public void onSyncedDataUpdated(EntityDataAccessor<?> key) {
super.onSyncedDataUpdated(key);
// Epic 4B: Deserialize V2 equipment on client when synced data changes
if (DATA_V2_EQUIPMENT.equals(key)) {
if (
this.v2Equipment != null &&
this.level() != null &&
this.level().isClientSide
) {
CompoundTag tag = this.entityData.get(DATA_V2_EQUIPMENT);
this.v2Equipment.deserializeNBT(tag);
}
}
}
// DESPAWN PROTECTION
/**
* Prevent NPCs from despawning.
* These are important NPCs that should persist in the world.
*
* @param distanceToClosestPlayer Distance to nearest player
* @return false to never despawn
*/
@Override
public boolean removeWhenFarAway(double distanceToClosestPlayer) {
return false; // Never despawn
}
/**
* Check if this entity should be saved to disk.
* Always true for NPCs - they are persistent.
*
* @return true to always save
*/
@Override
public boolean isPersistenceRequired() {
return true; // Always save to disk
}
// TICK - PERIODIC UPDATES
@Override
public void tick() {
// DOG pose transition detection (before super.tick)
animationController.tickAnimationBeforeSuperTick();
super.tick();
// DOG pose rotation smoothing AFTER super.tick (runs on both client/server)
// Must be before tickAnimationStack() which returns early on client
animationController.tickDogPoseRotationSmoothing();
// Animation stack tick (client-side only, returns early if client)
if (animationController.tickAnimationStack()) {
return; // Client-side, animation stack handled
}
// Server-side only below
// Restore pending captor reference
bondageManager.restoreCaptorFromUUID();
// Subclass-specific server tick behavior
tickSubclass();
}
/**
* Override point for subclass-specific tick logic.
* Called every server tick after animation and bondage restoration.
* Default is empty -- subclasses override as needed.
*/
protected void tickSubclass() {
// Default: no-op
}
// BEHAVIORAL FLAGS (overridable by subclasses)
/**
* Check if this entity can call for help when captured.
* Damsels call for help, kidnappers don't.
*
* @return true if can call for help (default: true)
*/
public boolean canCallForHelp() {
return true;
}
/**
* Check if this entity can panic.
* Damsels panic, kidnappers don't.
*
* @return true if can panic (default: true)
*/
public boolean canPanic() {
return true;
}
// POSE STATE
/**
* Check if NPC is currently sitting.
* @return true if in sitting pose
*/
public boolean isSitting() {
return this.entityData.get(DATA_SITTING);
}
/**
* Set sitting pose state.
* @param sitting true to enter sitting pose
*/
public void setSitting(boolean sitting) {
this.entityData.set(DATA_SITTING, sitting);
// Clear kneeling if sitting
if (sitting) {
this.entityData.set(DATA_KNEELING, false);
}
}
/**
* Check if NPC is currently kneeling.
* @return true if in kneeling pose
*/
public boolean isKneeling() {
return this.entityData.get(DATA_KNEELING);
}
/**
* Set kneeling pose state.
* @param kneeling true to enter kneeling pose
*/
public void setKneeling(boolean kneeling) {
this.entityData.set(DATA_KNEELING, kneeling);
// Clear sitting if kneeling
if (kneeling) {
this.entityData.set(DATA_SITTING, false);
}
}
/**
* Check if NPC is in any pose (sitting or kneeling).
* @return true if in a pose
*/
public boolean isInPose() {
return this.isSitting() || this.isKneeling() || this.isDogPose();
}
/**
* Check if NPC is in DOG pose (based on equipped bind).
* @return true if equipped bind has DOG pose type
*/
public boolean isDogPose() {
ItemStack bind = this.getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
return PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG;
}
/**
* Check if NPC is currently struggling against restraints.
* Used for animation sync.
* @return true if struggling
*/
public boolean isStruggling() {
return this.entityData.get(DATA_STRUGGLING);
}
/**
* Set struggling state for animation.
* @param struggling true if starting struggle animation
*/
public void setStruggling(boolean struggling) {
this.entityData.set(DATA_STRUGGLING, struggling);
}
// NAME SYSTEM
/**
* Get NPC's custom name.
*/
public String getNpcName() {
return this.entityData.get(DATA_DAMSEL_NAME);
}
/**
* Set NPC's custom name.
* Also updates Minecraft's custom name display.
*/
public void setNpcName(String name) {
this.entityData.set(DATA_DAMSEL_NAME, name);
// Make the name visible in-game
this.setCustomName(net.minecraft.network.chat.Component.literal(name));
this.setCustomNameVisible(true);
}
/**
* @deprecated Use {@link #getNpcName()} instead. Will be removed in a future version.
*/
@Deprecated(forRemoval = true)
public String getDamselName() {
return getNpcName();
}
/**
* @deprecated Use {@link #setNpcName(String)} instead. Will be removed in a future version.
*/
@Deprecated(forRemoval = true)
public void setDamselName(String name) {
setNpcName(name);
}
// GENDER / SLIM ARMS
/**
* Check if this NPC uses slim arms model.
*/
public boolean hasSlimArms() {
return this.entityData.get(DATA_SLIM_ARMS);
}
/**
* Set slim arms flag directly.
* Used by subclasses that have their own variant systems.
*/
public void setSlimArms(boolean slimArms) {
this.entityData.set(DATA_SLIM_ARMS, slimArms);
}
/**
* Set gender.
*/
public void setGender(Gender gender) {
this.entityData.set(DATA_GENDER, gender.getSerializedName());
}
/**
* Get gender.
*/
public Gender getGender() {
return Gender.fromName(this.entityData.get(DATA_GENDER));
}
// EQUIPMENT SLOTS (Delegated to DamselInventoryManager)
@Override
public ItemStack getItemBySlot(EquipmentSlot slot) {
return this.inventoryManager.getItemBySlot(slot);
}
@Override
public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
this.verifyEquippedItem(stack);
this.inventoryManager.setItemSlot(slot, stack);
}
@Override
public Iterable<ItemStack> getArmorSlots() {
return this.inventoryManager.getArmorSlots();
}
@Override
public ItemStack getMainHandItem() {
return this.inventoryManager.getMainHandItem();
}
/**
* Set the main hand item.
*
* @param stack Item to hold
*/
public void setMainHandItem(ItemStack stack) {
this.inventoryManager.setMainHandItem(stack);
}
// IRestrainable - BONDAGE STATE QUERIES (Delegated to DamselBondageManager)
@Override
public boolean isTiedUp() {
return bondageManager.isTiedUp();
}
@Override
public boolean isGagged() {
return bondageManager.isGagged();
}
@Override
public boolean isBlindfolded() {
return bondageManager.isBlindfolded();
}
@Override
public boolean hasEarplugs() {
return bondageManager.hasEarplugs();
}
@Override
public boolean hasCollar() {
return bondageManager.hasCollar();
}
/**
* Check if a player is an owner of this NPC's collar.
*/
public boolean isCollarOwner(Player player) {
return bondageManager.isCollarOwner(player);
}
@Override
public boolean hasClothes() {
return bondageManager.hasClothes();
}
@Override
public boolean hasMittens() {
return bondageManager.hasMittens();
}
@Override
public boolean isBoundAndGagged() {
return bondageManager.isBoundAndGagged();
}
// V2 Region-Based Equipment Access (Delegated to DamselBondageManager)
@Override
public ItemStack getEquipment(BodyRegionV2 region) {
return bondageManager.getEquipment(region);
}
@Override
public void equip(BodyRegionV2 region, ItemStack stack) {
bondageManager.equip(region, stack);
}
// IRestrainable - EQUIPMENT UNEQUIP (Delegated to DamselBondageManager)
@Override
public ItemStack unequip(BodyRegionV2 region) {
return bondageManager.unequip(region);
}
@Override
public ItemStack forceUnequip(BodyRegionV2 region) {
return bondageManager.forceUnequip(region);
}
// IRestrainable - ENSLAVEMENT
@Override
public boolean isCaptive() {
return this.isLeashed();
}
@Override
public boolean isEnslavable() {
if (this.isLeashed()) return false;
return this.isTiedUp();
}
@Override
public boolean canBeLeashed(Player player) {
if (this.isLeashed()) return false;
// Standard check: must be tied up
if (this.isTiedUp()) return true;
// Exception: collar owner can leash even if not tied
if (this.hasCollar()) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (CollarHelper.isOwner(collar, player)) {
return true;
}
}
return false;
}
@Override
public ICaptor getCaptor() {
return bondageManager.getCaptor();
}
@Override
public boolean getCapturedBy(ICaptor newCaptor) {
return bondageManager.getCapturedBy(newCaptor);
}
/**
* Force capture for managed camp operations (dogwalk, extract).
* Bypasses canCapture() PrisonerManager state check.
*/
public boolean forceCapturedBy(ICaptor newCaptor) {
return bondageManager.forceCapturedBy(newCaptor);
}
@Override
public void free() {
bondageManager.free();
}
@Override
public void free(boolean transportState) {
bondageManager.free(transportState);
}
// IRestrainable - UTILITY (Delegated to DamselBondageManager)
@Override
public UUID getKidnappedUniqueId() {
return bondageManager.getKidnappedUniqueId();
}
@Override
public String getKidnappedName() {
return bondageManager.getKidnappedName();
}
@Override
public LivingEntity asLivingEntity() {
return bondageManager.asLivingEntity();
}
@Override
public void kidnappedDropItem(ItemStack stack) {
bondageManager.kidnappedDropItem(stack);
}
// IRestrainable - STATE QUERIES (Advanced) (Delegated to DamselBondageManager)
@Override
public boolean canBeTiedUp() {
return bondageManager.canBeTiedUp();
}
@Override
public boolean isTiedToPole() {
return bondageManager.isTiedToPole();
}
@Override
public boolean tieToClosestPole(int searchRadius) {
return bondageManager.tieToClosestPole(searchRadius);
}
@Override
public boolean isForSell() {
return bondageManager.isForSell();
}
@Override
@Nullable
public ItemTask getSalePrice() {
return bondageManager.getSalePrice();
}
@Override
public void putForSale(ItemTask price) {
bondageManager.putForSale(price);
}
@Override
public void cancelSale() {
bondageManager.cancelSale();
}
@Override
public boolean canBeKidnappedByEvents() {
return bondageManager.canBeKidnappedByEvents();
}
@Override
public boolean hasLockedCollar() {
return bondageManager.hasLockedCollar();
}
@Override
public boolean hasNamedCollar() {
return bondageManager.hasNamedCollar();
}
@Override
public boolean hasClothesWithSmallArms() {
return bondageManager.hasClothesWithSmallArms();
}
@Override
public boolean hasGaggingEffect() {
ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false;
return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false;
return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null;
}
@Override
public boolean hasKnives() {
// NPCs don't have inventories for knives
return false;
}
// IRestrainable - REPLACE (Delegated to DamselBondageManager)
@Override
public ItemStack replaceEquipment(
BodyRegionV2 region,
ItemStack newStack,
boolean force
) {
return bondageManager.replaceEquipment(region, newStack, force);
}
// IRestrainable - BULK OPERATIONS (Delegated to DamselBondageManager)
@Override
public void applyBondage(
ItemStack bind,
ItemStack gag,
ItemStack blindfold,
ItemStack earplugs,
ItemStack collar,
ItemStack clothes
) {
bondageManager.applyBondage(
bind,
gag,
blindfold,
earplugs,
collar,
clothes
);
}
@Override
public void untie(boolean drop) {
bondageManager.untie(drop);
}
@Override
public void dropBondageItems(boolean drop) {
bondageManager.dropBondageItems(drop);
}
@Override
public void dropBondageItems(boolean drop, boolean dropBind) {
bondageManager.dropBondageItems(drop, dropBind);
}
@Override
public void dropBondageItems(
boolean drop,
boolean dropBind,
boolean dropGag,
boolean dropBlindfold,
boolean dropEarplugs,
boolean dropCollar,
boolean dropClothes
) {
bondageManager.dropBondageItems(
drop,
dropBind,
dropGag,
dropBlindfold,
dropEarplugs,
dropCollar,
dropClothes
);
}
@Override
public void dropClothes() {
bondageManager.dropClothes();
}
@Override
public int getBondageItemsWhichCanBeRemovedCount() {
return bondageManager.getBondageItemsWhichCanBeRemovedCount();
}
// IRestrainable - PERMISSIONS (Delegated to DamselBondageManager)
@Override
public boolean canTakeOffClothes(Player player) {
return bondageManager.canTakeOffClothes(player);
}
@Override
public boolean canChangeClothes(Player player) {
return bondageManager.canChangeClothes(player);
}
@Override
public boolean canChangeClothes() {
return bondageManager.canChangeClothes();
}
// IRestrainable - SPECIAL INTERACTIONS (Delegated to DamselBondageManager)
@Override
public void tighten(Player tightener) {
bondageManager.tighten(tightener);
}
@Override
public void applyChloroform(int duration) {
bondageManager.applyChloroform(duration);
}
@Override
public void shockKidnapped() {
bondageManager.shockKidnapped();
}
@Override
public void shockKidnapped(String messageAddon, float damage) {
bondageManager.shockKidnapped(messageAddon, damage);
}
@Override
public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
bondageManager.takeBondageItemBy(taker, slotIndex);
}
// IRestrainable - CAPTIVITY TRANSFER (Delegated to DamselBondageManager)
@Override
public void transferCaptivityTo(ICaptor newCaptor) {
bondageManager.transferCaptivityTo(newCaptor);
}
// IRestrainable - METADATA
@Override
public String getNameFromCollar() {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) {
return this.getNpcName();
}
if (collar.hasCustomHoverName()) {
return collar.getHoverName().getString();
}
return this.getNpcName();
}
@Override
public net.minecraft.world.entity.Entity getTransport() {
// NPCs use vanilla leash directly, not a transport entity
return null;
}
// IRestrainable - TELEPORT / DEATH
@Override
public void teleportToPosition(Position position) {
if (position == null || this.level().isClientSide) return;
TeleportHelper.teleportEntity(this, position);
}
@Override
public boolean onDeathKidnapped(Level world) {
if (world.isClientSide) return false;
// Check if we have a collar with cell configured
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
if (!CollarHelper.isCollar(collar)) return false;
java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return false;
// Get cell position from registry
if (
!(this.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel)
) return false;
com.tiedup.remake.cells.CellDataV2 cell =
com.tiedup.remake.cells.CellRegistryV2.get(serverLevel).getCell(
cellId
);
if (cell == null) return false;
Position cellPosition = new Position(
cell.getSpawnPoint().above(),
serverLevel.dimension()
);
// We have a cell - respawn there instead of dying
// 1. Free from captivity if captured
if (this.isCaptive()) {
this.free(false);
}
// 2. Unlock all locked items
unlockAllItems();
// 3. Heal to full
this.setHealth(this.getMaxHealth());
// 4. Teleport to cell
this.teleportToPosition(cellPosition);
TiedUpMod.LOGGER.info(
"[AbstractTiedUpNpc] {} respawned at cell instead of dying",
this.getName().getString()
);
return true; // Cancel death
}
/**
* Unlock all locked bondage items on this entity.
* Used before respawning at prison.
* Epic 4B: Reads from V2 regions, syncs once at end after mutations.
*/
private void unlockAllItems() {
com.tiedup.remake.v2.BodyRegionV2[] regions = {
com.tiedup.remake.v2.BodyRegionV2.ARMS,
com.tiedup.remake.v2.BodyRegionV2.MOUTH,
com.tiedup.remake.v2.BodyRegionV2.EYES,
com.tiedup.remake.v2.BodyRegionV2.EARS,
com.tiedup.remake.v2.BodyRegionV2.NECK,
};
boolean changed = false;
for (com.tiedup.remake.v2.BodyRegionV2 region : regions) {
ItemStack stack = v2Equipment.getInRegion(region);
if (
!stack.isEmpty() &&
stack.getItem() instanceof ILockable lockable &&
lockable.isLocked(stack)
) {
lockable.setLocked(stack, false);
changed = true;
}
}
if (changed) {
syncV2Equipment();
}
}
// IV2EquipmentHolder IMPLEMENTATION (Epic 4B)
@Override
public IV2BondageEquipment getV2Equipment() {
return v2Equipment;
}
/**
* Serialize V2 equipment to the synched entity data.
* Called by DamselBondageManager after every mutation (copy-on-write pattern).
*/
public void syncV2Equipment() {
this.entityData.set(DATA_V2_EQUIPMENT, this.v2Equipment.serializeNBT());
}
@Override
public void syncEquipmentToData() {
syncV2Equipment();
}
// SHOCK COLLAR CHECK
/**
* Check if this NPC has a shock collar equipped.
* Shock collars activate automatically when fear reaches flee threshold.
*
* @return true if wearing a shock collar
*/
public boolean hasShockCollar() {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
return com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar);
}
// BONDAGE SERVICE (delegated to BondageManager)
/**
* Check if bondage service is enabled for this NPC.
*/
public boolean isBondageServiceEnabled() {
return bondageManager.isBondageServiceEnabled();
}
/**
* Get the custom bondage service message from the collar.
*/
public String getBondageServiceMessage() {
return bondageManager.getBondageServiceMessage();
}
// COMPONENT ACCESSORS
/**
* Get inventory manager component.
*/
public DamselInventoryManager getInventoryManager() {
return inventoryManager;
}
/**
* Get bondage manager component.
*/
public DamselBondageManager getBondageManager() {
return bondageManager;
}
/**
* Get animation controller component.
*/
public DamselAnimationController getAnimationController() {
return animationController;
}
// LEASH ATTACHMENT POINT
/**
* Override leash offset to attach at neck level instead of eye level.
* Vanilla default is (0, eyeHeight, bbWidth*0.4) which is too high for humanoids.
*/
@Override
protected net.minecraft.world.phys.Vec3 getLeashOffset() {
// Neck height (~1.3 blocks from feet)
// Slight forward offset to prevent clipping with body
return new net.minecraft.world.phys.Vec3(
0.0,
1.3,
this.getBbWidth() * 0.2
);
}
// IANIMATEDPLAYER INTERFACE (PlayerAnimator)
/**
* Get the animation stack for this entity.
* Required by IAnimatedPlayer interface.
*
* @return AnimationStack containing all animation layers
*/
@Override
public AnimationStack getAnimationStack() {
return animationController.getAnimationStack();
}
/**
* Get the animation applier for model rendering.
* Required by IAnimatedPlayer interface.
*
* @return AnimationApplier for this entity
*/
@Override
public AnimationApplier playerAnimator_getAnimation() {
return animationController.playerAnimator_getAnimation();
}
/**
* Get a stored animation by ID.
* Required by IAnimatedPlayer interface.
*
* @param id Animation identifier
* @return The stored animation, or null if not found
*/
@Override
@Nullable
public IAnimation playerAnimator_getAnimation(ResourceLocation id) {
return animationController.playerAnimator_getAnimation(id);
}
/**
* Store an animation by ID.
* Required by IAnimatedPlayer interface.
*
* @param id Animation identifier
* @param animation Animation to store, or null to remove
* @return The previously stored animation, or null
*/
@Override
@Nullable
public IAnimation playerAnimator_setAnimation(
ResourceLocation id,
@Nullable IAnimation animation
) {
return animationController.playerAnimator_setAnimation(id, animation);
}
// IDIALOGUESPEAKER PARTIAL IMPLEMENTATION
/**
* Get the name displayed in dialogue chat messages.
*/
@Override
public String getDialogueName() {
return getNpcName();
}
/**
* Get this speaker as a LivingEntity.
*/
@Override
public LivingEntity asEntity() {
return this;
}
/**
* Check if dialogue should be processed through gag filter.
*/
@Override
public boolean isDialogueGagged() {
return isGagged();
}
// NBT PERSISTENCE (shared portion)
/**
* Save shared NPC data to NBT.
* Subclasses MUST call super.addAdditionalSaveData(tag) then add their own data.
*
* Bondage and inventory are saved here. Damsel-specific data (appearance, personality,
* rewards) is saved by DamselDataSerializer via EntityDamsel.addAdditionalSaveData().
*/
@Override
public void addAdditionalSaveData(CompoundTag tag) {
super.addAdditionalSaveData(tag);
// Bondage (restraints, captor, sale state)
bondageManager.saveToTag(tag);
// Inventory (NPC inventory, equipment)
inventoryManager.saveToTag(tag);
}
/**
* Load shared NPC data from NBT.
* Subclasses MUST call super.readAdditionalSaveData(tag) then load their own data.
*/
@Override
public void readAdditionalSaveData(CompoundTag tag) {
super.readAdditionalSaveData(tag);
// Bondage (restraints, captor, sale state)
bondageManager.loadFromTag(tag);
// Inventory (NPC inventory, equipment)
inventoryManager.loadFromTag(tag);
}
/**
* Check if this NPC is currently in a training session.
* Always returns false -- training sessions have been removed.
*
* @return false
*/
public boolean isInTrainingSession() {
return false;
}
}