package com.tiedup.remake.items.clothes; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.IV2BondageItem; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.UUID; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; /** * Generic clothes item with full NBT-based configuration. * *
Clothes are cosmetic items that can: *
Unlike other bondage items, clothes have NO gameplay effects - they are purely visual.
*/
public class GenericClothes extends Item implements ILockable, IV2BondageItem {
// ========== NBT KEYS ==========
public static final String NBT_DYNAMIC_TEXTURE = "dynamicTexture";
public static final String NBT_FULL_SKIN = "fullSkin";
public static final String NBT_SMALL_ARMS = "smallArms";
public static final String NBT_KEEP_HEAD = "keepHead";
public static final String NBT_LAYER_VISIBILITY = "layerVisibility";
public static final String NBT_LOCKED = "locked";
public static final String NBT_LOCKABLE = "lockable";
public static final String NBT_LOCKED_BY_KEY_UUID = "lockedByKeyUUID";
// Layer visibility keys
public static final String LAYER_HEAD = "head";
public static final String LAYER_BODY = "body";
public static final String LAYER_LEFT_ARM = "leftArm";
public static final String LAYER_RIGHT_ARM = "rightArm";
public static final String LAYER_LEFT_LEG = "leftLeg";
public static final String LAYER_RIGHT_LEG = "rightLeg";
public GenericClothes() {
super(new Item.Properties().stacksTo(16));
}
// ========== Lifecycle Hooks ==========
@Override
public void onEquipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special equip effects - purely cosmetic
}
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special unequip effects
}
/**
* Called when player right-clicks another entity with clothes.
* Allows putting clothes on tied-up entities (Players and NPCs).
*
* Unlike other bondage items, clothes can also be put on non-tied players
* if game rules allow it (roleplay scenarios).
*
* @param stack The item stack
* @param player The player using the item
* @param target The entity being interacted with
* @param hand The hand holding the item
* @return SUCCESS if clothes equipped/replaced, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Check if target can wear clothes (Player, EntityDamsel, EntityKidnapper)
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS; // Entity cannot wear clothes
}
// Unlike gags/blindfolds, clothes can be put on non-tied players too
// But if tied, always allowed. If not tied, check if target allows it.
if (!targetState.isTiedUp() && !targetState.canChangeClothes(player)) {
return InteractionResult.PASS;
}
// Case 1: No clothes yet - equip new one
if (!targetState.hasClothes()) {
ItemStack clothesCopy = stack.copyWithCount(1);
targetState.equip(BodyRegionV2.TORSO, clothesCopy);
stack.shrink(1);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} put clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Case 2: Already has clothes - replace them
else {
ItemStack clothesCopy = stack.copyWithCount(1);
ItemStack oldClothes = targetState.replaceEquipment(
BodyRegionV2.TORSO,
clothesCopy,
false
);
if (!oldClothes.isEmpty()) {
stack.shrink(1);
targetState.kidnappedDropItem(oldClothes);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} replaced clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
}
return InteractionResult.PASS;
}
// ========== Dynamic Texture Methods ==========
/**
* Get the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to check
* @return The URL string, or null if not set
*/
@Nullable
public String getDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_DYNAMIC_TEXTURE)) {
String url = tag.getString(NBT_DYNAMIC_TEXTURE);
return url.isEmpty() ? null : url;
}
return null;
}
/**
* Set the dynamic texture URL for this clothes item.
*
* @param stack The ItemStack to modify
* @param url The URL to set
*/
public void setDynamicTextureUrl(ItemStack stack, String url) {
if (url != null && !url.isEmpty()) {
stack.getOrCreateTag().putString(NBT_DYNAMIC_TEXTURE, url);
}
}
/**
* Remove the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to modify
*/
public void removeDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null) {
tag.remove(NBT_DYNAMIC_TEXTURE);
}
}
/**
* Check if this clothes item has a dynamic texture URL set.
*
* @param stack The ItemStack to check
* @return true if a URL is set
*/
public boolean hasDynamicTextureUrl(ItemStack stack) {
return getDynamicTextureUrl(stack) != null;
}
// ========== Full Skin / Small Arms Methods ==========
/**
* Check if full-skin mode is enabled.
* In full-skin mode, the clothes texture replaces the entire player skin.
*
* @param stack The ItemStack to check
* @return true if full-skin mode is enabled
*/
public boolean isFullSkinEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_FULL_SKIN);
}
/**
* Set full-skin mode.
*
* @param stack The ItemStack to modify
* @param enabled true to enable full-skin mode
*/
public void setFullSkinEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_FULL_SKIN, enabled);
}
/**
* Check if small arms (slim model) should be forced.
*
* @param stack The ItemStack to check
* @return true if small arms should be forced
*/
public boolean shouldForceSmallArms(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_SMALL_ARMS);
}
/**
* Set whether small arms (slim model) should be forced.
*
* @param stack The ItemStack to modify
* @param enabled true to force small arms
*/
public void setForceSmallArms(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_SMALL_ARMS, enabled);
}
/**
* Check if keep head mode is enabled.
* When enabled, the wearer's head/hat layers are preserved instead of being
* replaced by the clothes texture. Useful for keeping the original face.
*
* @param stack The ItemStack to check
* @return true if keep head mode is enabled
*/
public boolean isKeepHeadEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_KEEP_HEAD);
}
/**
* Set keep head mode.
* When enabled, the wearer's head/hat layers are preserved.
*
* @param stack The ItemStack to modify
* @param enabled true to keep the wearer's head
*/
public void setKeepHeadEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_KEEP_HEAD, enabled);
}
// ========== Layer Visibility Methods ==========
/**
* Check if a specific body layer is enabled (visible on wearer).
* Defaults to true (visible) if not set.
*
* @param stack The ItemStack to check
* @param layer The layer key (use LAYER_* constants)
* @return true if the layer is visible
*/
public boolean isLayerEnabled(ItemStack stack, String layer) {
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(NBT_LAYER_VISIBILITY)) {
return true; // Default: all layers visible
}
CompoundTag layers = tag.getCompound(NBT_LAYER_VISIBILITY);
// If not specified, default to visible
return !layers.contains(layer) || layers.getBoolean(layer);
}
/**
* Set the visibility of a specific body layer on the wearer.
*
* @param stack The ItemStack to modify
* @param layer The layer key (use LAYER_* constants)
* @param enabled true to show the layer, false to hide it
*/
public void setLayerEnabled(
ItemStack stack,
String layer,
boolean enabled
) {
CompoundTag tag = stack.getOrCreateTag();
CompoundTag layers = tag.contains(NBT_LAYER_VISIBILITY)
? tag.getCompound(NBT_LAYER_VISIBILITY)
: new CompoundTag();
layers.putBoolean(layer, enabled);
tag.put(NBT_LAYER_VISIBILITY, layers);
}
/**
* Get all layer visibility settings as a compound tag.
*
* @param stack The ItemStack to check
* @return The layer visibility compound, or null if not set
*/
@Nullable
public CompoundTag getLayerVisibility(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_LAYER_VISIBILITY)) {
return tag.getCompound(NBT_LAYER_VISIBILITY);
}
return null;
}
// ========== IV2BondageItem Implementation ==========
private static final Set