Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
1281 lines
36 KiB
Java
1281 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.items.base.ItemCollar;
|
|
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.getItem() instanceof
|
|
com.tiedup.remake.items.base.ItemBind itemBind
|
|
) {
|
|
return (
|
|
itemBind.getPoseType() ==
|
|
com.tiedup.remake.items.base.PoseType.DOG
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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 (collar.getItem() instanceof ItemCollar collarItem) {
|
|
if (collarItem.getOwners(collar).contains(player.getUUID())) {
|
|
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 (
|
|
gag.getItem() instanceof
|
|
com.tiedup.remake.items.base.IHasGaggingEffect
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public boolean hasBlindingEffect() {
|
|
ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES);
|
|
if (blindfold.isEmpty()) return false;
|
|
return (
|
|
blindfold.getItem() instanceof
|
|
com.tiedup.remake.items.base.IHasBlindingEffect
|
|
);
|
|
}
|
|
|
|
@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 (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
|
|
|
|
java.util.UUID cellId = itemCollar.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 (
|
|
collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|