package com.tiedup.remake.items; import com.tiedup.remake.core.ModConfig; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import java.util.List; import java.util.Random; import java.util.UUID; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResultHolder; 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 net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.jetbrains.annotations.Nullable; /** * Lockpick item for picking locks on bondage restraints. * * * Behavior: * - 25% chance of success per attempt * - SUCCESS: Instant unlock, padlock PRESERVED (lockable=true) * - FAIL: * - 2.5% chance to JAM the lock (blocks future lockpick attempts) * - 15% chance to break the lockpick * - If shock collar equipped: SHOCK + notify owners * - Cannot be used while wearing mittens * - Durability: 10 uses */ public class ItemLockpick extends Item { private static final Random random = new Random(); public ItemLockpick() { super(new Item.Properties().durability(5)); // 5 tentatives max } @Override public void appendHoverText( ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag ) { super.appendHoverText(stack, level, tooltip, flag); tooltip.add( Component.translatable("item.tiedup.lockpick.tooltip").withStyle( ChatFormatting.GRAY ) ); int remaining = stack.getMaxDamage() - stack.getDamageValue(); tooltip.add( Component.literal( "Uses: " + remaining + "/" + stack.getMaxDamage() ).withStyle(ChatFormatting.DARK_GRAY) ); // LOW FIX: Removed server config access from client tooltip (desync issue) // Success/break chances depend on server config, not client config // Displaying client config values here would be misleading in multiplayer tooltip.add( Component.literal("Success/break chances: Check server config") .withStyle(ChatFormatting.GRAY) .withStyle(ChatFormatting.ITALIC) ); } /** * v2.5: Right-click with lockpick opens the struggle choice screen. * This allows the player to choose which locked item to pick. */ @Override public InteractionResultHolder use( Level level, Player player, InteractionHand hand ) { ItemStack stack = player.getItemInHand(hand); PlayerBindState state = PlayerBindState.getInstance(player); if (state == null) { return InteractionResultHolder.pass(stack); } // Block mittens if (state.hasMittens()) { if (!level.isClientSide) { SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS ); } return InteractionResultHolder.fail(stack); } // Client side: open the unified bondage screen if (level.isClientSide) { openUnifiedBondageScreen(); return InteractionResultHolder.success(stack); } return InteractionResultHolder.consume(stack); } /** * Client-only method to open the unified bondage screen. * Separated to avoid classloading issues on server. * Uses fully qualified names to prevent class loading on server. */ @OnlyIn(Dist.CLIENT) private void openUnifiedBondageScreen() { net.minecraft.client.Minecraft.getInstance().setScreen( new com.tiedup.remake.client.gui.screens.UnifiedBondageScreen() ); } /** * Check if this lockpick can be used (has durability remaining). */ public static boolean canUse(ItemStack stack) { if (stack.isEmpty() || !(stack.getItem() instanceof ItemLockpick)) { return false; } return stack.getDamageValue() < stack.getMaxDamage(); } /** * Result of a lockpick attempt. */ public enum PickResult { /** Successfully picked the lock - item unlocked, padlock preserved */ SUCCESS, /** Failed but lock still pickable */ FAIL, /** Failed and jammed the lock - lockpick no longer usable on this item */ JAMMED, /** Lockpick broke during attempt */ BROKE, /** Cannot attempt - mittens equipped */ BLOCKED_MITTENS, /** Cannot attempt - lock is jammed */ BLOCKED_JAMMED, /** Cannot attempt - item not locked */ NOT_LOCKED, } /** * Attempt to pick a lock on a target item. * * @param player The player attempting to pick * @param state The player's bind state * @param lockpickStack The lockpick being used * @param targetStack The item to pick * @param targetRegion The V2 body region of the target item * @return The result of the pick attempt */ public static PickResult attemptPick( Player player, PlayerBindState state, ItemStack lockpickStack, ItemStack targetStack, BodyRegionV2 targetRegion ) { // Check if lockpick is usable if (!canUse(lockpickStack)) { return PickResult.BROKE; } // Check if wearing mittens if (state.hasMittens()) { SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS ); return PickResult.BLOCKED_MITTENS; } // Check if target is lockable and locked if (!(targetStack.getItem() instanceof ILockable lockable)) { return PickResult.NOT_LOCKED; } if (!lockable.isLocked(targetStack)) { return PickResult.NOT_LOCKED; } // Check if lock is jammed if (lockable.isJammed(targetStack)) { SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.ERROR, "This lock is jammed! Use struggle instead." ); return PickResult.BLOCKED_JAMMED; } // Roll for success boolean success = random.nextInt(100) < ModConfig.SERVER.lockpickSuccessChance.get(); if (success) { // SUCCESS: Unlock the item, PRESERVE the padlock lockable.setLockedByKeyUUID(targetStack, null); // Unlock // lockable stays true - padlock preserved! SystemMessageManager.sendToPlayer( player, "Lock picked!", ChatFormatting.GREEN ); // Damage lockpick damageLockpick(lockpickStack); TiedUpMod.LOGGER.info( "[LOCKPICK] {} successfully picked lock on {} ({})", player.getName().getString(), targetStack.getDisplayName().getString(), targetRegion ); return PickResult.SUCCESS; } else { // FAIL: Various bad things can happen // 1. Check for shock collar and trigger shock triggerShockIfCollar(player, state); // 2. Check for jam boolean jammed = random.nextDouble() * 100 < ModConfig.SERVER.lockpickJamChance.get(); if (jammed) { lockable.setJammed(targetStack, true); SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.ERROR, "The lock jammed! Only struggle can open it now." ); TiedUpMod.LOGGER.info( "[LOCKPICK] {} jammed the lock on {} ({})", player.getName().getString(), targetStack.getDisplayName().getString(), targetRegion ); // Damage lockpick boolean broke = damageLockpick(lockpickStack); return broke ? PickResult.BROKE : PickResult.JAMMED; } // 3. Check for break boolean broke = random.nextInt(100) < ModConfig.SERVER.lockpickBreakChance.get(); if (broke) { lockpickStack.shrink(1); SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.ERROR, "Lockpick broke!" ); TiedUpMod.LOGGER.info( "[LOCKPICK] {}'s lockpick broke while picking {} ({})", player.getName().getString(), targetStack.getDisplayName().getString(), targetRegion ); return PickResult.BROKE; } // 4. Normal fail - just damage lockpick damageLockpick(lockpickStack); SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.WARNING, "Lockpick slipped..." ); TiedUpMod.LOGGER.debug( "[LOCKPICK] {} failed to pick lock on {} ({})", player.getName().getString(), targetStack.getDisplayName().getString(), targetRegion ); return PickResult.FAIL; } } /** * Damage the lockpick by 1 use. * @return true if the lockpick broke (ran out of durability) */ private static boolean damageLockpick(ItemStack stack) { stack.setDamageValue(stack.getDamageValue() + 1); if (stack.getDamageValue() >= stack.getMaxDamage()) { stack.shrink(1); return true; } return false; } /** * Trigger shock collar if player has one equipped. * Also notifies the collar owners. */ private static void triggerShockIfCollar( Player player, PlayerBindState state ) { ItemStack collar = V2EquipmentHelper.getInRegion( player, BodyRegionV2.NECK ); if (collar.isEmpty()) return; if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) { // Shock the player state.shockKidnapped(" (Failed lockpick attempt)", 2.0f); // Notify owners notifyOwnersLockpickAttempt(player, collar); TiedUpMod.LOGGER.info( "[LOCKPICK] {} was shocked for failed lockpick attempt", player.getName().getString() ); } } /** * Notify shock collar owners about the lockpick attempt. */ private static void notifyOwnersLockpickAttempt( Player player, ItemStack collar ) { if (player.getServer() == null) return; Component warning = Component.literal("ALERT: ") .withStyle(ChatFormatting.RED, ChatFormatting.BOLD) .append( Component.literal( player.getName().getString() + " tried to pick a lock!" ).withStyle(ChatFormatting.GOLD) ); List owners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar); for (UUID ownerId : owners) { ServerPlayer owner = player .getServer() .getPlayerList() .getPlayer(ownerId); if (owner != null) { owner.sendSystemMessage(warning); } } } /** * Find a lockpick in the player's inventory. * @return The first usable lockpick found, or EMPTY if none */ public static ItemStack findLockpickInInventory(Player player) { for (ItemStack stack : player.getInventory().items) { if (canUse(stack)) { return stack; } } return ItemStack.EMPTY; } }