package com.tiedup.remake.tasks; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.entities.EntityDamsel; import com.tiedup.remake.entities.NpcTypeHelper; import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.action.PacketUntying; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import java.util.Map; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; /** * * Based on original UntyingPlayerTask from 1.12.2 * * This task: * 1. Tracks progress as the helper repeatedly right-clicks the tied target * 2. Sends progress updates to both helper and target clients (if player) * 3. Removes all restraints and drops items when the timer completes * 4. Sets up target's restraint state for client-side visualization (if player) * * Epic 5F: Uses V2EquipmentHelper/BodyRegionV2. */ public class UntyingPlayerTask extends UntyingTask { /** The player performing the untying action. */ private Player helper; /** * Create a new untying task. * * @param targetState The target's IBondageState state * @param targetEntity The target entity (Player or NPC) * @param seconds Total duration in seconds * @param level The world */ public UntyingPlayerTask( IBondageState targetState, LivingEntity targetEntity, int seconds, Level level ) { super(targetState, targetEntity, seconds, level); } /** * Create a new untying task with helper reference. * * @param targetState The target's IBondageState state * @param targetEntity The target entity (Player or NPC) * @param seconds Total duration in seconds * @param level The world * @param helper The player performing the untying */ public UntyingPlayerTask( IBondageState targetState, LivingEntity targetEntity, int seconds, Level level, Player helper ) { super(targetState, targetEntity, seconds, level); this.helper = helper; } /** * Set the helper (the player doing the untying). */ public void setHelper(Player helper) { this.helper = helper; } /** * Update the untying progress. * Called each time the helper right-clicks the tied target. * * In the new progress-based system, this method only: * 1. Marks this tick as "active" (progress will increase in tick()) * 2. Validates helper is still close to target * * The actual progress increment and completion check happen in tick(). */ @Override public synchronized void update() { // SECURITY: Validate helper is still close to target if (helper != null && targetEntity != null) { double distance = helper.distanceTo(targetEntity); if (distance > 4.0) { TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Helper {} moved too far from target ({} blocks), cancelling", helper.getName().getString(), String.format("%.1f", distance) ); stop(); return; } // Check line-of-sight if (!helper.hasLineOfSight(targetEntity)) { TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Helper {} lost line of sight to target, cancelling", helper.getName().getString() ); stop(); return; } } // Mark this tick as active (player is clicking on target) super.update(); } /** * Send progress packets to both helper and target (if players). * Called every tick from RestraintTaskTickHandler.onPlayerTick(). */ @Override public void sendProgressPackets() { if (stopped) return; String helperName = helper != null ? helper.getName().getString() : "Someone"; String targetName = targetEntity.getName().getString(); // Packet to victim: isHelper=false, shows helper's name if (targetEntity instanceof ServerPlayer serverTarget) { PacketUntying victimPacket = new PacketUntying( this.getState(), this.getMaxSeconds(), false, helperName ); ModNetwork.sendToPlayer(victimPacket, serverTarget); } // Packet to helper: isHelper=true, shows target's name if (helper instanceof ServerPlayer serverHelper) { PacketUntying helperPacket = new PacketUntying( this.getState(), this.getMaxSeconds(), true, targetName ); ModNetwork.sendToPlayer(helperPacket, serverHelper); } } /** * Called when the task completes successfully. * Drops bondage items, frees the target, and sends completion packets. */ @Override protected void onComplete() { // Verify target entity still exists and is alive if (!isTargetValid()) { TiedUpMod.LOGGER.warn( "[UntyingPlayerTask] Target entity no longer valid, cancelling task" ); stop(); return; } TiedUpMod.LOGGER.info( "[UntyingPlayerTask] Untying complete for {}", targetEntity.getName().getString() ); // Drop all bondage items on the ground dropBondageItems(); // Remove all restraints from target untieTarget(); // Handle Damsel-specific rewards if ( targetEntity instanceof EntityDamsel damsel && NpcTypeHelper.isDamselOnly(targetEntity) && helper != null ) { // Reward the savior (gives emeralds and marks player as savior) damsel.rewardSavior(helper); } // Mark task as stopped stop(); // Send completion packets to both parties String helperName = helper != null ? helper.getName().getString() : "Someone"; String targetName = targetEntity.getName().getString(); if (targetEntity instanceof ServerPlayer serverTarget) { PacketUntying completionPacket = new PacketUntying( -1, this.getMaxSeconds(), false, helperName ); ModNetwork.sendToPlayer(completionPacket, serverTarget); PlayerBindState playerState = PlayerBindState.getInstance( serverTarget ); if (playerState != null) { playerState.tasks().setRestrainedState(null); } } if (helper instanceof ServerPlayer serverHelper) { PacketUntying completionPacket = new PacketUntying( -1, this.getMaxSeconds(), true, targetName ); ModNetwork.sendToPlayer(completionPacket, serverHelper); } } /** * Set up the target's restraint state for client-side progress tracking. * Only applies to player targets (NPCs don't need client-side state). * * Note: On dedicated servers, this is a no-op. The client receives * progress packets (PacketUntying) which create the PlayerStateTask locally. */ @Override public void setUpTargetState() { // Only set up state for player targets if (!(targetEntity instanceof Player targetPlayer)) { return; } // Server-side: nothing to do - client handles its own PlayerStateTask via packets if (!targetPlayer.level().isClientSide) { return; } // Client-side: set up local progress tracking (only reached in single-player/integrated server) // Note: This code path is rarely used since packets handle client-side state } /** * Drop all bondage items on the ground. * Works for both players (via V2EquipmentHelper) and NPCs (via IBondageState). */ private void dropBondageItems() { // For player targets: use V2EquipmentHelper to get all equipped items if (targetEntity instanceof Player player) { Map equipped = V2EquipmentHelper.getAllEquipped(player); for (Map.Entry< BodyRegionV2, ItemStack > entry : equipped.entrySet()) { ItemStack stack = entry.getValue(); if (!stack.isEmpty()) { // Drop item at player's position ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), stack.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped {} from region {}", stack.getHoverName().getString(), entry.getKey().name() ); } } } else { // For NPC targets: use IBondageState interface // Drop bind if present ItemStack bind = targetState.getEquipment(BodyRegionV2.ARMS); if (bind != null && !bind.isEmpty()) { ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), bind.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped bind: {}", bind.getHoverName().getString() ); } // Drop gag if present ItemStack gag = targetState.getEquipment(BodyRegionV2.MOUTH); if (gag != null && !gag.isEmpty()) { ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), gag.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped gag: {}", gag.getHoverName().getString() ); } // Drop blindfold if present ItemStack blindfold = targetState.getEquipment(BodyRegionV2.EYES); if (blindfold != null && !blindfold.isEmpty()) { ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), blindfold.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped blindfold: {}", blindfold.getHoverName().getString() ); } // Drop earplugs if present ItemStack earplugs = targetState.getEquipment(BodyRegionV2.EARS); if (earplugs != null && !earplugs.isEmpty()) { ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), earplugs.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped earplugs: {}", earplugs.getHoverName().getString() ); } // Drop mittens if present ItemStack mittens = targetState.getEquipment(BodyRegionV2.HANDS); if (mittens != null && !mittens.isEmpty()) { ItemEntity itemEntity = new ItemEntity( targetEntity.level(), targetEntity.getX(), targetEntity.getY(), targetEntity.getZ(), mittens.copy() ); targetEntity.level().addFreshEntity(itemEntity); TiedUpMod.LOGGER.debug( "[UntyingPlayerTask] Dropped mittens: {}", mittens.getHoverName().getString() ); } } } /** * Remove all restraints from the target entity. * Works for both players and NPCs via IBondageState interface. */ private void untieTarget() { // For player targets: clear all V2 equipment (fires onUnequipped callbacks) if (targetEntity instanceof Player player) { V2EquipmentHelper.clearAll(player); PlayerBindState playerState = PlayerBindState.getInstance(player); if (playerState != null && playerState.isCaptive()) { playerState.free(); } } else { // For NPC targets: use IBondageState interface directly targetState.unequip(BodyRegionV2.ARMS); targetState.unequip(BodyRegionV2.MOUTH); targetState.unequip(BodyRegionV2.EYES); targetState.unequip(BodyRegionV2.EARS); targetState.unequip(BodyRegionV2.HANDS); } TiedUpMod.LOGGER.info( "[UntyingPlayerTask] Fully untied {}", targetEntity.getName().getString() ); } }