package com.tiedup.remake.items; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.IKnife; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.KnifeVariant; import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import java.util.List; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResultHolder; 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.item.UseAnim; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; /** * Generic knife item created from KnifeVariant enum. * Replaces individual knife classes (ItemStoneKnife, ItemIronKnife, ItemGoldenKnife). * * v2.5 Changes: * - Added active cutting mechanic (hold right-click) * - Per-tier cutting speed: Stone=5, Iron=8, Golden=12 resistance/second * - Durability consumed per second = cutting speed (1 dura = 1 resistance) * - Can cut binds directly or locked accessories */ public class GenericKnife extends Item implements IKnife { private final KnifeVariant variant; public GenericKnife(KnifeVariant variant) { super( new Item.Properties() .stacksTo(1) .durability(variant.getDurability()) ); this.variant = variant; } /** * Get the variant this knife was created from. */ public KnifeVariant getVariant() { return variant; } // ==================== TOOLTIP ==================== @Override public void appendHoverText( ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag ) { super.appendHoverText(stack, level, tooltip, flag); int remaining = stack.getMaxDamage() - stack.getDamageValue(); int speed = variant.getCuttingSpeed(); int cuttingSeconds = remaining / speed; // Show cutting speed tooltip.add( Component.literal("Cutting speed: " + speed + " res/s").withStyle( ChatFormatting.GRAY ) ); // Show cutting time remaining tooltip.add( Component.literal( "Cutting time: " + cuttingSeconds + "s (" + remaining + " total res)" ).withStyle(ChatFormatting.DARK_GRAY) ); } // ==================== USE MECHANICS ==================== @Override public int getUseDuration(ItemStack stack) { // Max use time: 5 minutes (very long, will stop naturally when bind breaks) return 20 * 60 * 5; } @Override public UseAnim getUseAnimation(ItemStack stack) { return UseAnim.BOW; // Shows a "using" animation } /** * Called when player right-clicks with knife. * Starts cutting if: * - Player is tied up (cuts bind) * - Player has a knife cut target set (cuts accessory lock) */ @Override public InteractionResultHolder use( Level level, Player player, InteractionHand hand ) { ItemStack stack = player.getItemInHand(hand); // Only check on server side for actual state if (!level.isClientSide) { PlayerBindState state = PlayerBindState.getInstance(player); if (state == null) { return InteractionResultHolder.pass(stack); } // v2.5: Block knife usage if wearing mittens if (state.hasMittens()) { TiedUpMod.LOGGER.debug( "[GenericKnife] {} cannot use knife - wearing mittens", player.getName().getString() ); return InteractionResultHolder.fail(stack); } // Priority 1: If tied up, cut the bind if (state.isTiedUp()) { player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } // Priority 2: If accessory target selected (via StruggleChoiceScreen) if (state.getKnifeCutTarget() != null) { player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } // Priority 3: If wearing a collar (not tied), auto-target the collar if (state.hasCollar()) { state.setKnifeCutTarget(BodyRegionV2.NECK); player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } // Priority 4: Check other accessories (gag, blindfold, etc.) if (state.isGagged()) { state.setKnifeCutTarget(BodyRegionV2.MOUTH); player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } if (state.isBlindfolded()) { state.setKnifeCutTarget(BodyRegionV2.EYES); player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } if (state.hasEarplugs()) { state.setKnifeCutTarget(BodyRegionV2.EARS); player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } // Note: Don't auto-target mittens since you need hands to use knife // Nothing to cut return InteractionResultHolder.pass(stack); } // Client side - check mittens and allow use if valid target or has accessories PlayerBindState state = PlayerBindState.getInstance(player); if ( state != null && !state.hasMittens() && (state.isTiedUp() || state.getKnifeCutTarget() != null || state.hasCollar() || state.isGagged() || state.isBlindfolded() || state.hasEarplugs()) ) { player.startUsingItem(hand); return InteractionResultHolder.consume(stack); } return InteractionResultHolder.pass(stack); } /** * Called every tick while player holds right-click. * Performs the actual cutting logic. */ @Override public void onUseTick( Level level, LivingEntity entity, ItemStack stack, int remainingTicks ) { if (level.isClientSide || !(entity instanceof ServerPlayer player)) { return; } // Calculate how many ticks have been used int usedTicks = getUseDuration(stack) - remainingTicks; // Only process every 20 ticks (1 second) if (usedTicks > 0 && usedTicks % 20 == 0) { performCutTick(player, stack); } } /** * Called when player releases right-click or item breaks. */ @Override public void releaseUsing( ItemStack stack, Level level, LivingEntity entity, int remainingTicks ) { if (!level.isClientSide && entity instanceof ServerPlayer player) { TiedUpMod.LOGGER.debug( "[GenericKnife] {} stopped cutting", player.getName().getString() ); } } /** * Perform one "tick" of cutting (called every second while held). * Consumes durability and removes resistance based on variant's cutting speed. */ private void performCutTick(ServerPlayer player, ItemStack stack) { PlayerBindState state = PlayerBindState.getInstance(player); if (state == null) { player.stopUsingItem(); return; } int speed = variant.getCuttingSpeed(); // Determine what to cut if (state.isTiedUp()) { // Cut BIND cutBind(player, state, stack, speed); } else if (state.getKnifeCutTarget() != null) { // Cut ACCESSORY cutAccessory(player, state, stack, speed); } else { // Nothing to cut player.stopUsingItem(); return; } // Play cutting sound player .level() .playSound( null, player.blockPosition(), SoundEvents.SHEEP_SHEAR, SoundSource.PLAYERS, 0.5f, 1.2f ); // Consume durability equal to cutting speed stack.hurtAndBreak(speed, player, p -> p.broadcastBreakEvent(p.getUsedItemHand()) ); // Notify nearby guards (Kidnappers, Maids, Traders) about cutting noise com.tiedup.remake.minigame.GuardNotificationHelper.notifyNearbyGuards( player ); // Force inventory sync so durability bar updates in real-time player.inventoryMenu.broadcastChanges(); // Sync state to clients SyncManager.syncBindState(player); } /** * Cut the bind directly. */ private void cutBind( ServerPlayer player, PlayerBindState state, ItemStack knifeStack, int speed ) { // Get bind stack for ILockable check ItemStack bindStack = V2EquipmentHelper.getInRegion( player, BodyRegionV2.ARMS ); if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { player.stopUsingItem(); return; } // Reduce resistance by cutting speed int currentRes = state.getCurrentBindResistance(); int newRes = Math.max(0, currentRes - speed); state.setCurrentBindResistance(newRes); TiedUpMod.LOGGER.debug( "[GenericKnife] {} cutting bind: resistance {} -> {}", player.getName().getString(), currentRes, newRes ); // Check if escaped if (newRes <= 0) { state.getStruggleBinds().successActionExternal(state); player.stopUsingItem(); TiedUpMod.LOGGER.info( "[GenericKnife] {} escaped by cutting bind!", player.getName().getString() ); } } /** * Cut an accessory - either removes lock resistance (if locked) or removes the item directly (if unlocked). */ private void cutAccessory( ServerPlayer player, PlayerBindState state, ItemStack knifeStack, int speed ) { BodyRegionV2 target = state.getKnifeCutTarget(); if (target == null) { player.stopUsingItem(); return; } ItemStack accessory = V2EquipmentHelper.getInRegion(player, target); if (accessory.isEmpty()) { // Target doesn't exist state.clearKnifeCutTarget(); player.stopUsingItem(); return; } // Check if the accessory is locked boolean isLocked = false; if (accessory.getItem() instanceof ILockable lockable) { isLocked = lockable.isLocked(accessory); } if (!isLocked) { // NOT locked - directly cut and remove the accessory IBondageState kidnapped = KidnappedHelper.getKidnappedState(player); if (kidnapped != null) { ItemStack removed = removeAccessory(kidnapped, target); if (!removed.isEmpty()) { // Drop the removed accessory kidnapped.kidnappedDropItem(removed); TiedUpMod.LOGGER.info( "[GenericKnife] {} cut off unlocked {}", player.getName().getString(), target ); } } state.clearKnifeCutTarget(); player.stopUsingItem(); return; } // Accessory IS locked - reduce lock resistance via knife cut progress ILockable lockable = (ILockable) accessory.getItem(); int cutProgress = com.tiedup.remake.util.ItemNBTHelper.getInt(accessory, "knifeCutProgress"); int newProgress = cutProgress + speed; int lockResistance = com.tiedup.remake.core.SettingsAccessor.getPadlockResistance(null); com.tiedup.remake.util.ItemNBTHelper.setInt(accessory, "knifeCutProgress", newProgress); TiedUpMod.LOGGER.debug( "[GenericKnife] {} cutting {} lock: progress {} / {}", player.getName().getString(), target, newProgress, lockResistance ); // Check if lock is destroyed if (newProgress >= lockResistance) { // Destroy the lock (remove padlock, clear lock state) lockable.setLockedByKeyUUID(accessory, null); // Unlocks and clears locked state lockable.setLockable(accessory, false); // Remove padlock entirely com.tiedup.remake.util.ItemNBTHelper.remove(accessory, "knifeCutProgress"); lockable.setJammed(accessory, false); state.clearKnifeCutTarget(); player.stopUsingItem(); TiedUpMod.LOGGER.info( "[GenericKnife] {} cut through {} lock!", player.getName().getString(), target ); } } /** * Remove an accessory from the player and return it. */ private ItemStack removeAccessory( IBondageState kidnapped, BodyRegionV2 target ) { switch (target) { case NECK -> { ItemStack collar = kidnapped.getEquipment(BodyRegionV2.NECK); if (collar != null && !collar.isEmpty()) { kidnapped.unequip(BodyRegionV2.NECK); return collar; } } case MOUTH -> { ItemStack gag = kidnapped.getEquipment(BodyRegionV2.MOUTH); if (gag != null && !gag.isEmpty()) { kidnapped.unequip(BodyRegionV2.MOUTH); return gag; } } case EYES -> { ItemStack blindfold = kidnapped.getEquipment(BodyRegionV2.EYES); if (blindfold != null && !blindfold.isEmpty()) { kidnapped.unequip(BodyRegionV2.EYES); return blindfold; } } case EARS -> { ItemStack earplugs = kidnapped.getEquipment(BodyRegionV2.EARS); if (earplugs != null && !earplugs.isEmpty()) { kidnapped.unequip(BodyRegionV2.EARS); return earplugs; } } case HANDS -> { ItemStack mittens = kidnapped.getEquipment(BodyRegionV2.HANDS); if (mittens != null && !mittens.isEmpty()) { kidnapped.unequip(BodyRegionV2.HANDS); return mittens; } } default -> { } } return ItemStack.EMPTY; } /** * Find a knife in the player's inventory. * * @param player The player to search * @return The knife ItemStack, or empty if not found */ public static ItemStack findKnifeInInventory(Player player) { // Check main hand first ItemStack mainHand = player.getMainHandItem(); if (mainHand.getItem() instanceof IKnife) { return mainHand; } // Check off hand ItemStack offHand = player.getOffhandItem(); if (offHand.getItem() instanceof IKnife) { return offHand; } // Check inventory for (int i = 0; i < player.getInventory().getContainerSize(); i++) { ItemStack stack = player.getInventory().getItem(i); if (stack.getItem() instanceof IKnife) { return stack; } } return ItemStack.EMPTY; } }