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.
1268 lines
36 KiB
Java
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;
|
|
}
|
|
}
|