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). * *

Shared functionality:

* * *

Subclass-specific (NOT in this class):

* * * @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 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 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 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 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 DATA_GENDER = SynchedEntityData.defineId( AbstractTiedUpNpc.class, EntityDataSerializers.STRING ); /** * Is NPC currently in sitting pose? * Synced to client for animation/rendering. */ public static final EntityDataAccessor DATA_SITTING = SynchedEntityData.defineId( AbstractTiedUpNpc.class, EntityDataSerializers.BOOLEAN ); /** * Is NPC currently in kneeling pose? * Synced to client for animation/rendering. */ public static final EntityDataAccessor DATA_KNEELING = SynchedEntityData.defineId( AbstractTiedUpNpc.class, EntityDataSerializers.BOOLEAN ); /** * Is NPC currently struggling against restraints? * Synced to client for struggle animation. */ public static final EntityDataAccessor DATA_STRUGGLING = SynchedEntityData.defineId( AbstractTiedUpNpc.class, EntityDataSerializers.BOOLEAN ); /** * Main hand item. * Synced to client for rendering and behavior determination. */ public static final EntityDataAccessor 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 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 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 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; } }