From 737a4fd59bc233f9595f6774a96ed934493b2fb3 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:35:31 +0200 Subject: [PATCH] feat(D-01/A): interaction routing + TyingInteractionHelper (A8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataDrivenBondageItem.use(): shift+click cycles bind mode for ARMS items - DataDrivenBondageItem.interactLivingEntity(): region-based routing - ARMS → TyingInteractionHelper (tying task with progress bar) - NECK → deferred to Branch C (no V2 collar JSONs yet) - Other regions → instant equip via parent AbstractV2BondageItem - TyingInteractionHelper: extracted tying flow using V2TyingPlayerTask - Distance/LoS validation, swap if already tied, task lifecycle --- .../v2/bondage/TyingInteractionHelper.java | 107 ++++++++++++++++++ .../datadriven/DataDrivenBondageItem.java | 58 ++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java diff --git a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java new file mode 100644 index 0000000..02e9939 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -0,0 +1,107 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.core.SettingsAccessor; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.state.IBondageState; +import com.tiedup.remake.state.PlayerBindState; +import com.tiedup.remake.tasks.TyingTask; +import com.tiedup.remake.tasks.V2TyingPlayerTask; +import com.tiedup.remake.util.KidnappedHelper; +import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +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.ItemStack; + +/** + * Handles the tying interaction flow for V2 data-driven ARMS items. + * + *

Extracted from {@code ItemBind.interactLivingEntity()} to support + * the same tying task flow for data-driven items.

+ */ +public final class TyingInteractionHelper { + + private TyingInteractionHelper() {} + + /** + * Handle right-click tying of a target entity with a V2 ARMS item. + * Creates/continues a V2TyingPlayerTask. + */ + public static InteractionResult handleTying( + ServerPlayer player, + LivingEntity target, + ItemStack stack, + InteractionHand hand + ) { + IBondageState targetState = KidnappedHelper.getKidnappedState(target); + if (targetState == null) return InteractionResult.PASS; + + // Kidnapper can't be tied themselves + IBondageState kidnapperState = KidnappedHelper.getKidnappedState(player); + if (kidnapperState != null && kidnapperState.isTiedUp()) { + return InteractionResult.PASS; + } + + // Already tied — try to swap + if (targetState.isTiedUp()) { + if (stack.isEmpty()) return InteractionResult.PASS; + ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS); + if (!oldBind.isEmpty()) { + V2EquipmentHelper.equipItem(target, stack.copy()); + stack.shrink(1); + target.spawnAtLocation(oldBind); + TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString()); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + // Distance + line-of-sight (skip for self-tying) + boolean isSelfTying = player.equals(target); + if (!isSelfTying) { + if (player.distanceTo(target) > 4.0 || !player.hasLineOfSight(target)) { + return InteractionResult.PASS; + } + } + + // Create/continue tying task + PlayerBindState playerState = PlayerBindState.getInstance(player); + if (playerState == null) return InteractionResult.PASS; + + int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules()); + + V2TyingPlayerTask newTask = new V2TyingPlayerTask( + stack.copy(), + stack, + targetState, + target, + tyingSeconds, + player.level(), + player + ); + + TyingTask currentTask = playerState.getCurrentTyingTask(); + if (currentTask == null + || !currentTask.isSameTarget(target) + || currentTask.isOutdated() + || !ItemStack.matches(currentTask.getBind(), stack)) { + playerState.setCurrentTyingTask(newTask); + newTask.start(); + } else { + newTask = (V2TyingPlayerTask) currentTask; + } + + newTask.update(); + + if (newTask.isStopped()) { + stack.shrink(1); + playerState.setCurrentTyingTask(null); + TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString()); + } + + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index b37d114..7dd6269 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -1,7 +1,10 @@ package com.tiedup.remake.v2.bondage.datadriven; +import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.IV2BondageEquipment; +import com.tiedup.remake.v2.bondage.TyingInteractionHelper; import com.tiedup.remake.v2.bondage.V2BondageItems; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import com.tiedup.remake.v2.bondage.component.ComponentHolder; @@ -17,7 +20,13 @@ import java.util.stream.Collectors; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.Level; @@ -116,6 +125,55 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return def != null && def.supportsColor(); } + // ===== INTERACTION ROUTING ===== + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + Set regions = getOccupiedRegions(stack); + + // ARMS items: shift+click cycles bind mode + if (regions.contains(BodyRegionV2.ARMS) && player.isShiftKeyDown() && !level.isClientSide) { + BindModeHelper.cycleBindModeId(stack); + player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f); + player.displayClientMessage( + Component.translatable( + "tiedup.message.bindmode_changed", + Component.translatable(BindModeHelper.getBindModeTranslationKey(stack)) + ), + true + ); + return InteractionResultHolder.success(stack); + } + return super.use(level, player, hand); + } + + @Override + public InteractionResult interactLivingEntity( + ItemStack stack, Player player, LivingEntity target, InteractionHand hand + ) { + // Client: arm swing + if (player.level().isClientSide) { + return InteractionResult.SUCCESS; + } + + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def == null) return InteractionResult.PASS; + + Set regions = def.occupiedRegions(); + + // ARMS: tying flow (do NOT call super — avoids double equip) + if (regions.contains(BodyRegionV2.ARMS) && player instanceof ServerPlayer serverPlayer) { + return TyingInteractionHelper.handleTying(serverPlayer, target, stack, hand); + } + + // NECK: collar equip blocked for now — V2 collar JSONs don't exist in Branch A. + // Full collar equip flow (add owner, register, sound) wired in Branch C. + + // All other regions (MOUTH, EYES, EARS, HANDS): instant equip via parent + return super.interactLivingEntity(stack, player, target, hand); + } + // ===== IHasResistance IMPLEMENTATION ===== @Override