401 lines
14 KiB
Java
401 lines
14 KiB
Java
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<BodyRegionV2, ItemStack> 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()
|
|
);
|
|
}
|
|
}
|