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 REGIONS = Collections.unmodifiableSet(EnumSet.of(BodyRegionV2.TORSO)); @Override public Set getOccupiedRegions() { return REGIONS; } @Override @Nullable public ResourceLocation getModelLocation() { return null; // Clothes use URL-texture rendering, not GLB models } @Override public int getPosePriority() { return 0; // Cosmetic item, never forces a pose } @Override public int getEscapeDifficulty() { return 0; // Cosmetic, no struggle resistance } @Override public boolean supportsColor() { return false; // Color is handled via URL texture, not variant system } @Override public boolean supportsSlimModel() { return false; // Slim/wide is handled via NBT smallArms flag, not model variants } @Override public boolean canEquip(ItemStack stack, LivingEntity entity) { return true; } @Override public boolean canUnequip(ItemStack stack, LivingEntity entity) { return true; } // ========== ILockable Implementation ========== @Override public ItemStack setLocked(ItemStack stack, boolean state) { stack.getOrCreateTag().putBoolean(NBT_LOCKED, state); if (!state) { // When unlocking, clear lock-related data setJammed(stack, false); } return stack; } @Override public boolean isLocked(ItemStack stack) { CompoundTag tag = stack.getTag(); return tag != null && tag.getBoolean(NBT_LOCKED); } @Override public ItemStack setLockable(ItemStack stack, boolean state) { stack.getOrCreateTag().putBoolean(NBT_LOCKABLE, state); return stack; } @Override public boolean isLockable(ItemStack stack) { CompoundTag tag = stack.getTag(); // Default to true if not set return ( tag == null || !tag.contains(NBT_LOCKABLE) || tag.getBoolean(NBT_LOCKABLE) ); } @Override @Nullable public UUID getLockedByKeyUUID(ItemStack stack) { CompoundTag tag = stack.getTag(); if (tag != null && tag.hasUUID(NBT_LOCKED_BY_KEY_UUID)) { return tag.getUUID(NBT_LOCKED_BY_KEY_UUID); } return null; } @Override public void setLockedByKeyUUID(ItemStack stack, @Nullable UUID keyUUID) { CompoundTag tag = stack.getOrCreateTag(); if (keyUUID != null) { tag.putUUID(NBT_LOCKED_BY_KEY_UUID, keyUUID); setLocked(stack, true); } else { tag.remove(NBT_LOCKED_BY_KEY_UUID); setLocked(stack, false); } } // ========== Tooltip ========== @Override public void appendHoverText( ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag ) { super.appendHoverText(stack, level, tooltip, flag); // Dynamic texture info String url = getDynamicTextureUrl(stack); if (url != null) { tooltip.add( Component.translatable( "item.tiedup.clothes.tooltip.has_url" ).withStyle(ChatFormatting.GREEN) ); if (isFullSkinEnabled(stack)) { tooltip.add( Component.translatable( "item.tiedup.clothes.tooltip.full_skin" ).withStyle(ChatFormatting.AQUA) ); } if (shouldForceSmallArms(stack)) { tooltip.add( Component.translatable( "item.tiedup.clothes.tooltip.small_arms" ).withStyle(ChatFormatting.AQUA) ); } } else { tooltip.add( Component.translatable( "item.tiedup.clothes.tooltip.no_url" ).withStyle(ChatFormatting.GRAY) ); } // Layer visibility info CompoundTag layers = getLayerVisibility(stack); if (layers != null) { StringBuilder disabled = new StringBuilder(); if (!isLayerEnabled(stack, LAYER_HEAD)) disabled.append("head "); if (!isLayerEnabled(stack, LAYER_BODY)) disabled.append("body "); if (!isLayerEnabled(stack, LAYER_LEFT_ARM)) disabled.append( "L.arm " ); if (!isLayerEnabled(stack, LAYER_RIGHT_ARM)) disabled.append( "R.arm " ); if (!isLayerEnabled(stack, LAYER_LEFT_LEG)) disabled.append( "L.leg " ); if (!isLayerEnabled(stack, LAYER_RIGHT_LEG)) disabled.append( "R.leg " ); if (!disabled.isEmpty()) { tooltip.add( Component.translatable( "item.tiedup.clothes.tooltip.layers_disabled", disabled.toString().trim() ).withStyle(ChatFormatting.YELLOW) ); } } // Lock info if (isLocked(stack)) { tooltip.add( Component.translatable("item.tiedup.locked").withStyle( ChatFormatting.RED ) ); } } }