diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index 1eb0fed..392178f 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -14,8 +14,10 @@ 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.BindModeHelper; import com.tiedup.remake.v2.bondage.IV2BondageItem; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.V2EquipResult; import java.util.function.Supplier; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; @@ -222,69 +224,116 @@ public class PacketSelfBondage { IV2BondageItem v2Item, IBondageState state ) { + java.util.Set regions = v2Item.getOccupiedRegions(stack); + + // Cannot self-collar + if (regions.contains(BodyRegionV2.NECK)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried to self-collar — blocked", player.getName().getString()); + return; + } + + // ARMS: tying task with progress bar + if (regions.contains(BodyRegionV2.ARMS)) { + handleV2SelfBind(player, stack, v2Item, state); + return; + } + + // Accessories (MOUTH, EYES, EARS, HANDS): instant equip + handleV2SelfAccessory(player, stack, v2Item, state); + } + + private static void handleV2SelfBind( + ServerPlayer player, + ItemStack stack, + IV2BondageItem v2Item, + IBondageState state + ) { + // Can't self-tie if already tied (check both V1 state and V2 region to prevent dual-bind) + if (state.isTiedUp() || V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-tie but already tied", player.getName().getString()); + return; + } + // Check if all target regions are already occupied or blocked boolean allBlocked = true; for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) { - if ( - !V2EquipmentHelper.isRegionOccupied(player, region) && - !V2EquipmentHelper.isRegionBlocked(player, region) - ) { + if (!V2EquipmentHelper.isRegionOccupied(player, region) + && !V2EquipmentHelper.isRegionBlocked(player, region)) { allBlocked = false; break; } } if (allBlocked) { - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} tried V2 self-equip but all regions occupied", - player.getName().getString() - ); + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-equip but all regions occupied", player.getName().getString()); return; } PlayerBindState playerState = PlayerBindState.getInstance(player); if (playerState == null) return; - int tyingSeconds = SettingsAccessor.getTyingPlayerTime( - player.level().getGameRules() - ); + int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules()); - // Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn) V2TyingPlayerTask newTask = new V2TyingPlayerTask( - stack.copy(), // copy for display/matching - stack, // live reference for consumption - state, - player, // target is self - tyingSeconds, - player.level(), - player // kidnapper is also self + stack.copy(), stack, state, player, tyingSeconds, player.level(), player ); TyingTask currentTask = playerState.getCurrentTyingTask(); - - if ( - currentTask == null || - !currentTask.isSameTarget(player) || - currentTask.isOutdated() || - !ItemStack.matches(currentTask.getBind(), stack) - ) { - // Start new task + if (currentTask == null + || !currentTask.isSameTarget(player) + || currentTask.isOutdated() + || !ItemStack.matches(currentTask.getBind(), stack)) { playerState.setCurrentTyingTask(newTask); newTask.start(); - - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} started V2 self-tying ({} seconds)", - player.getName().getString(), - tyingSeconds - ); } else { - // Continue existing task — just mark active currentTask.update(); } - // If we started a new task, mark it active too if (playerState.getCurrentTyingTask() == newTask) { newTask.update(); } + + // Clear completed task to prevent blocking future tying interactions + TyingTask activeTask = playerState.getCurrentTyingTask(); + if (activeTask != null && activeTask.isStopped()) { + playerState.setCurrentTyingTask(null); + } + } + + private static void handleV2SelfAccessory( + ServerPlayer player, + ItemStack stack, + IV2BondageItem v2Item, + IBondageState state + ) { + // Can't equip accessories if arms are fully bound + ItemStack currentBind = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS); + if (!currentBind.isEmpty() && BindModeHelper.hasArmsBound(currentBind)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} can't self-accessory — arms bound", player.getName().getString()); + return; + } + + // Check if region is occupied — try to swap + for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) { + if (V2EquipmentHelper.isRegionOccupied(player, region)) { + ItemStack existing = V2EquipmentHelper.getInRegion(player, region); + // Can't swap if locked + if (existing.getItem() instanceof ILockable lockable && lockable.isLocked(existing)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} can't swap — current is locked", player.getName().getString()); + return; + } + } + } + + // Equip via V2EquipmentHelper (handles conflict resolution, displaced items) + V2EquipResult result = V2EquipmentHelper.equipItem(player, stack.copy()); + if (result.isSuccess()) { + for (ItemStack displaced : result.displaced()) { + player.spawnAtLocation(displaced); + } + stack.shrink(1); + SyncManager.syncInventory(player); + TiedUpMod.LOGGER.info("[SelfBondage] {} self-equipped V2 accessory", player.getName().getString()); + } } /** diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java index a25bfad..8294d3a 100644 --- a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java +++ b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java @@ -320,10 +320,12 @@ public class PlayerEquipment { player, BodyRegionV2.ARMS ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind) - ) return 0; - return bind.getCurrentResistance(stack, player); + if (stack.isEmpty()) return 0; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) { + return resistance.getCurrentResistance(stack, player); + } + return 0; } /** @@ -334,10 +336,11 @@ public class PlayerEquipment { player, BodyRegionV2.ARMS ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind) - ) return; - bind.setCurrentResistance(stack, resistance); + if (stack.isEmpty()) return; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(stack, resistance); + } } /** @@ -348,10 +351,12 @@ public class PlayerEquipment { player, BodyRegionV2.NECK ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar) - ) return 0; - return collar.getCurrentResistance(stack, player); + if (stack.isEmpty()) return 0; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) { + return resistance.getCurrentResistance(stack, player); + } + return 0; } /** @@ -362,10 +367,11 @@ public class PlayerEquipment { player, BodyRegionV2.NECK ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar) - ) return; - collar.setCurrentResistance(stack, resistance); + if (stack.isEmpty()) return; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(stack, resistance); + } } // ========== Helper Methods ========== diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java index f32c357..e1bddf6 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java @@ -4,12 +4,18 @@ import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.items.base.IHasResistance; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.ItemBind; import com.tiedup.remake.state.IPlayerLeashAccess; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.BindModeHelper; +import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.ResistanceComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -75,16 +81,17 @@ public class StruggleBinds extends StruggleState { player, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { + if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { return false; } // The locked check has been moved to struggle() where decrease is reduced - return bind.canBeStruggledOut(bindStack); + // Check canBeStruggledOut — works for both V1 and V2 via IHasResistance + if (bindStack.getItem() instanceof IHasResistance resistance) { + return resistance.canBeStruggledOut(bindStack); + } + return true; } /** @@ -103,14 +110,13 @@ public class StruggleBinds extends StruggleState { player, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { - return false; - } + if (bindStack.isEmpty()) return false; - return bind.isLocked(bindStack); + // Works for both V1 (ItemBind) and V2 (DataDrivenBondageItem) via ILockable + if (bindStack.getItem() instanceof ILockable lockable) { + return lockable.isLocked(bindStack); + } + return false; } /** @@ -148,14 +154,18 @@ public class StruggleBinds extends StruggleState { BodyRegionV2.NECK ); - if ( - !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar - ) { - return shockCollar.notifyStruggle(player, collar); + if (!collar.isEmpty() && CollarHelper.canShock(collar)) { + // V1 shock collar + if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { + return shockCollar.notifyStruggle(player, collar); + } + // V2 shock collar — notify via IHasResistance if available + if (collar.getItem() instanceof IHasResistance resistance) { + resistance.notifyStruggle(player); + } + return true; } - return true; // No collar, proceed normally + return true; // No shock collar, proceed normally } /** @@ -317,18 +327,23 @@ public class StruggleBinds extends StruggleState { target, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { + if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { return; } - // Get base resistance from config (BUG-003 fix: was using ModGameRules which - // only knew 4 types and returned hardcoded 100 for the other 10) - int baseResistance = SettingsAccessor.getBindResistance( - bind.getItemName() + // Get base resistance: V2 reads from ResistanceComponent directly, + // V1 reads from SettingsAccessor via item name (BUG-003 fix) + int baseResistance; + ResistanceComponent comp = DataDrivenBondageItem.getComponent( + bindStack, ComponentType.RESISTANCE, ResistanceComponent.class ); + if (comp != null) { + baseResistance = comp.getBaseResistance(); + } else if (bindStack.getItem() instanceof ItemBind bind) { + baseResistance = SettingsAccessor.getBindResistance(bind.getItemName()); + } else { + baseResistance = 100; + } // Set current resistance to base (full restore) setResistanceState(state, baseResistance); diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java index ec46cad..f8a6b9c 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java @@ -2,10 +2,16 @@ package com.tiedup.remake.state.struggle; import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.items.base.IHasResistance; +import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.ResistanceComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -44,19 +50,15 @@ public class StruggleCollar extends StruggleState { @Override protected int getResistanceState(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + if (player == null) return 0; + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { - return 0; + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return 0; + + if (collar.getItem() instanceof IHasResistance resistance) { + return resistance.getCurrentResistance(collar, player); } - - return collarItem.getCurrentResistance(collar, player); + return 0; } /** @@ -68,19 +70,14 @@ public class StruggleCollar extends StruggleState { @Override protected void setResistanceState(PlayerBindState state, int resistance) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + if (player == null) return; + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { - return; + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return; + + if (collar.getItem() instanceof IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(collar, resistance); } - - collarItem.setCurrentResistance(collar, resistance); } /** @@ -104,31 +101,29 @@ public class StruggleCollar extends StruggleState { return false; } - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped"); return false; } - // Check if locked - if (!collarItem.isLocked(collar)) { - TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked"); + // Check if locked (works for V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + if (!lockable.isLocked(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked"); + return false; + } + } else { return false; } - // Check if struggle is enabled - if (!collarItem.canBeStruggledOut(collar)) { - TiedUpMod.LOGGER.debug( - "[StruggleCollar] Collar struggle is disabled" - ); - return false; + // Check if struggle is enabled (works for V1 and V2 via IHasResistance) + if (collar.getItem() instanceof IHasResistance resistance) { + if (!resistance.canBeStruggledOut(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar struggle is disabled"); + return false; + } } return true; @@ -141,17 +136,17 @@ public class StruggleCollar extends StruggleState { @Override protected boolean onAttempt(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar - ) { - return shockCollar.notifyStruggle(player, collar); + if (!collar.isEmpty() && CollarHelper.canShock(collar)) { + // V1 shock collar + if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { + return shockCollar.notifyStruggle(player, collar); + } + // V2 shock collar — notify via IHasResistance + if (collar.getItem() instanceof IHasResistance resistance) { + resistance.notifyStruggle(player); + } } return true; } @@ -167,31 +162,19 @@ public class StruggleCollar extends StruggleState { @Override protected void successAction(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { - TiedUpMod.LOGGER.warn( - "[StruggleCollar] successAction called but no collar equipped" - ); + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { + TiedUpMod.LOGGER.warn("[StruggleCollar] successAction called but no collar equipped"); return; } - // Unlock the collar - collarItem.setLocked(collar, false); + // Unlock the collar (works for V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + lockable.setLocked(collar, false); + } - TiedUpMod.LOGGER.info( - "[StruggleCollar] {} unlocked their collar!", - player.getName().getString() - ); - - // Note: Collar is NOT removed, just unlocked - // Player can now manually remove it + TiedUpMod.LOGGER.info("[StruggleCollar] {} unlocked their collar!", player.getName().getString()); } @Override @@ -230,30 +213,36 @@ public class StruggleCollar extends StruggleState { return; } - ItemStack collar = V2EquipmentHelper.getInRegion( - target, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten"); return; } - // Check if collar is locked - if (!collarItem.isLocked(collar)) { - TiedUpMod.LOGGER.debug( - "[StruggleCollar] Collar must be locked to tighten" - ); + // Check if collar is locked (V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + if (!lockable.isLocked(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar must be locked to tighten"); + return; + } + } else { return; } - // Get base resistance from GameRules - int baseResistance = collarItem.getBaseResistance(target); - int currentResistance = collarItem.getCurrentResistance(collar, target); + // Get resistance — V2: read ResistanceComponent directly (avoids MAX-scan bug), + // V1: use IHasResistance.getBaseResistance() + if (!(collar.getItem() instanceof IHasResistance resistanceItem)) return; + int baseResistance; + ResistanceComponent comp = DataDrivenBondageItem.getComponent( + collar, ComponentType.RESISTANCE, ResistanceComponent.class + ); + if (comp != null) { + baseResistance = comp.getBaseResistance(); + } else { + baseResistance = resistanceItem.getBaseResistance(target); + } + int currentResistance = resistanceItem.getCurrentResistance(collar, target); // Only tighten if current resistance is lower than base if (currentResistance >= baseResistance) { @@ -264,7 +253,7 @@ public class StruggleCollar extends StruggleState { } // Restore to base resistance - collarItem.setCurrentResistance(collar, baseResistance); + resistanceItem.setCurrentResistance(collar, baseResistance); TiedUpMod.LOGGER.info( "[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})", diff --git a/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java new file mode 100644 index 0000000..bbfe851 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java @@ -0,0 +1,95 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.base.ItemBind; +import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; +import java.util.Map; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; + +/** + * Static utilities for bind mode operations on any bondage item stack (V1 or V2). + * + *

Bind mode determines whether a bind restrains arms, legs, or both. + * The mode is stored in the stack's NBT tag {@code "bindMode"}.

+ */ +public final class BindModeHelper { + + private BindModeHelper() {} + + private static final String NBT_BIND_MODE = "bindMode"; + + public static final String MODE_FULL = "full"; + public static final String MODE_ARMS = "arms"; + public static final String MODE_LEGS = "legs"; + + private static final String[] MODE_CYCLE = { MODE_FULL, MODE_ARMS, MODE_LEGS }; + + private static final Map MODE_TRANSLATION_KEYS = Map.of( + MODE_FULL, "tiedup.bindmode.full", + MODE_ARMS, "tiedup.bindmode.arms", + MODE_LEGS, "tiedup.bindmode.legs" + ); + + /** + * Check if the given stack is a bind item (V2 ARMS-region item or V1 ItemBind). + */ + public static boolean isBindItem(ItemStack stack) { + if (stack.isEmpty()) return false; + // V2: check data-driven definition + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def != null) { + return def.occupiedRegions().contains(BodyRegionV2.ARMS); + } + // V1 fallback + return stack.getItem() instanceof ItemBind; + } + + /** + * Get the bind mode ID from the stack's NBT. + * @return "full", "arms", or "legs" (defaults to "full") + */ + public static String getBindModeId(ItemStack stack) { + if (stack.isEmpty()) return MODE_FULL; + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL; + String value = tag.getString(NBT_BIND_MODE); + if (MODE_ARMS.equals(value) || MODE_LEGS.equals(value)) return value; + return MODE_FULL; + } + + /** True if arms are restrained (mode is "arms" or "full"). */ + public static boolean hasArmsBound(ItemStack stack) { + String mode = getBindModeId(stack); + return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode); + } + + /** True if legs are restrained (mode is "legs" or "full"). */ + public static boolean hasLegsBound(ItemStack stack) { + String mode = getBindModeId(stack); + return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode); + } + + /** + * Cycle bind mode: full → arms → legs → full. + * @return the new mode ID + */ + public static String cycleBindModeId(ItemStack stack) { + String current = getBindModeId(stack); + String next = MODE_FULL; + for (int i = 0; i < MODE_CYCLE.length; i++) { + if (MODE_CYCLE[i].equals(current)) { + next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length]; + break; + } + } + stack.getOrCreateTag().putString(NBT_BIND_MODE, next); + return next; + } + + /** Get the translation key for the current bind mode. */ + public static String getBindModeTranslationKey(ItemStack stack) { + return MODE_TRANSLATION_KEYS.getOrDefault(getBindModeId(stack), "tiedup.bindmode.full"); + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java new file mode 100644 index 0000000..c85641d --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java @@ -0,0 +1,371 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.ItemChokeCollar; +import com.tiedup.remake.items.ItemGpsCollar; +import com.tiedup.remake.items.ItemShockCollar; +import com.tiedup.remake.items.ItemShockCollarAuto; +import com.tiedup.remake.items.base.ItemCollar; +import com.tiedup.remake.v2.bondage.component.ChokingComponent; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.GpsComponent; +import com.tiedup.remake.v2.bondage.component.OwnershipComponent; +import com.tiedup.remake.v2.bondage.component.ShockComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +/** + * Static utility for collar operations bridging V1 (ItemCollar subclasses) + * and V2 (data-driven items with OwnershipComponent). + */ +public final class CollarHelper { + + private CollarHelper() {} + + // ===== DETECTION ===== + + // True if the stack is any kind of collar (V2 ownership component or V1 ItemCollar) + public static boolean isCollar(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemCollar; + } + + // ===== OWNERSHIP (NBT: "owners") ===== + + // Returns all owner UUIDs stored in the collar's "owners" ListTag + public static List getOwners(ItemStack stack) { + return getListUUIDs(stack, "owners"); + } + + // True if the given UUID is in the owners list + public static boolean isOwner(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "owners", uuid); + } + + // True if the given player is an owner + public static boolean isOwner(ItemStack stack, Player player) { + return isOwner(stack, player.getUUID()); + } + + // True if the collar has at least one owner + public static boolean hasOwner(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("owners", Tag.TAG_LIST)) return false; + return !tag.getList("owners", Tag.TAG_COMPOUND).isEmpty(); + } + + // Adds an owner entry {uuid, name} to the "owners" ListTag + public static void addOwner(ItemStack stack, UUID uuid, String name) { + addToList(stack, "owners", uuid, name); + } + + // Convenience: add a player as owner + public static void addOwner(ItemStack stack, Player player) { + addOwner(stack, player.getUUID(), player.getGameProfile().getName()); + } + + // Removes an owner by UUID + public static void removeOwner(ItemStack stack, UUID uuid) { + removeFromList(stack, "owners", uuid); + } + + // ===== BLACKLIST (NBT: "blacklist") ===== + + public static List getBlacklist(ItemStack stack) { + return getListUUIDs(stack, "blacklist"); + } + + public static boolean isBlacklisted(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "blacklist", uuid); + } + + public static void addToBlacklist(ItemStack stack, UUID uuid, String name) { + addToList(stack, "blacklist", uuid, name); + } + + public static void removeFromBlacklist(ItemStack stack, UUID uuid) { + removeFromList(stack, "blacklist", uuid); + } + + // ===== WHITELIST (NBT: "whitelist") ===== + + public static List getWhitelist(ItemStack stack) { + return getListUUIDs(stack, "whitelist"); + } + + public static boolean isWhitelisted(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "whitelist", uuid); + } + + public static void addToWhitelist(ItemStack stack, UUID uuid, String name) { + addToList(stack, "whitelist", uuid, name); + } + + public static void removeFromWhitelist(ItemStack stack, UUID uuid) { + removeFromList(stack, "whitelist", uuid); + } + + // ===== LIST INTERNALS ===== + + private static List getListUUIDs(ItemStack stack, String listKey) { + List result = new ArrayList<>(); + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return result; + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + CompoundTag entry = list.getCompound(i); + if (entry.contains("uuid")) { + try { + result.add(UUID.fromString(entry.getString("uuid"))); + } catch (IllegalArgumentException ignored) { + // Malformed UUID in NBT, skip + } + } + } + return result; + } + + private static boolean hasUUIDInList(ItemStack stack, String listKey, UUID uuid) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return false; + String uuidStr = uuid.toString(); + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + if (uuidStr.equals(list.getCompound(i).getString("uuid"))) { + return true; + } + } + return false; + } + + private static void addToList(ItemStack stack, String listKey, UUID uuid, String name) { + CompoundTag tag = stack.getOrCreateTag(); + ListTag list = tag.contains(listKey, Tag.TAG_LIST) + ? tag.getList(listKey, Tag.TAG_COMPOUND) + : new ListTag(); + // Prevent duplicates + String uuidStr = uuid.toString(); + for (int i = 0; i < list.size(); i++) { + if (uuidStr.equals(list.getCompound(i).getString("uuid"))) return; + } + CompoundTag entry = new CompoundTag(); + entry.putString("uuid", uuidStr); + entry.putString("name", name); + list.add(entry); + tag.put(listKey, list); + } + + private static void removeFromList(ItemStack stack, String listKey, UUID uuid) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return; + String uuidStr = uuid.toString(); + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + list.removeIf(element -> + uuidStr.equals(((CompoundTag) element).getString("uuid")) + ); + } + + // ===== FEATURES ===== + + @Nullable + public static String getNickname(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("nickname")) return null; + return tag.getString("nickname"); + } + + public static void setNickname(ItemStack stack, String nickname) { + stack.getOrCreateTag().putString("nickname", nickname); + } + + public static boolean hasNickname(ItemStack stack) { + return stack.hasTag() && stack.getTag().contains("nickname"); + } + + @Nullable + public static UUID getCellId(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("cellId")) return null; + try { + return UUID.fromString(tag.getString("cellId")); + } catch (IllegalArgumentException e) { + return null; + } + } + + public static void setCellId(ItemStack stack, UUID cellId) { + stack.getOrCreateTag().putString("cellId", cellId.toString()); + } + + public static boolean hasCellAssigned(ItemStack stack) { + return getCellId(stack) != null; + } + + public static boolean isKidnappingModeEnabled(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("kidnappingMode"); + } + + public static void setKidnappingModeEnabled(ItemStack stack, boolean enabled) { + stack.getOrCreateTag().putBoolean("kidnappingMode", enabled); + } + + // Kidnapping mode is ready when enabled AND a cell is assigned + public static boolean isKidnappingModeReady(ItemStack stack) { + return isKidnappingModeEnabled(stack) && hasCellAssigned(stack); + } + + public static boolean shouldTieToPole(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("tieToPole"); + } + + public static void setShouldTieToPole(ItemStack stack, boolean value) { + stack.getOrCreateTag().putBoolean("tieToPole", value); + } + + // Default true when tag is absent or key is missing + public static boolean shouldWarnMasters(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("warnMasters")) return true; + return tag.getBoolean("warnMasters"); + } + + public static void setShouldWarnMasters(ItemStack stack, boolean value) { + stack.getOrCreateTag().putBoolean("warnMasters", value); + } + + public static boolean isBondageServiceEnabled(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("bondageservice"); + } + + public static void setBondageServiceEnabled(ItemStack stack, boolean enabled) { + stack.getOrCreateTag().putBoolean("bondageservice", enabled); + } + + @Nullable + public static String getServiceSentence(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("servicesentence")) return null; + return tag.getString("servicesentence"); + } + + public static void setServiceSentence(ItemStack stack, String sentence) { + stack.getOrCreateTag().putString("servicesentence", sentence); + } + + // ===== SHOCK ===== + + // True if the collar can shock (V2 ShockComponent or V1 ItemShockCollar) + public static boolean canShock(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemShockCollar; + } + + public static boolean isPublicShock(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("public_mode"); + } + + public static void setPublicShock(ItemStack stack, boolean publicMode) { + stack.getOrCreateTag().putBoolean("public_mode", publicMode); + } + + // V2: from ShockComponent auto interval, V1: from ItemShockCollarAuto field, else 0 + public static int getShockInterval(ItemStack stack) { + ShockComponent comp = DataDrivenBondageItem.getComponent( + stack, ComponentType.SHOCK, ShockComponent.class + ); + if (comp != null) return comp.getAutoInterval(); + if (stack.getItem() instanceof ItemShockCollarAuto auto) { + return auto.getInterval(); + } + return 0; + } + + // ===== GPS ===== + + // True if the collar has GPS capabilities + public static boolean hasGPS(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemGpsCollar; + } + + public static boolean hasPublicTracking(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("publicTracking"); + } + + public static void setPublicTracking(ItemStack stack, boolean publicTracking) { + stack.getOrCreateTag().putBoolean("publicTracking", publicTracking); + } + + // GPS active defaults to true when absent + public static boolean isActive(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("gpsActive")) return true; + return tag.getBoolean("gpsActive"); + } + + public static void setActive(ItemStack stack, boolean active) { + stack.getOrCreateTag().putBoolean("gpsActive", active); + } + + // ===== CHOKE ===== + + // True if the collar is a choke collar + public static boolean isChokeCollar(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemChokeCollar; + } + + public static boolean isChoking(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("choking"); + } + + public static void setChoking(ItemStack stack, boolean choking) { + stack.getOrCreateTag().putBoolean("choking", choking); + } + + public static boolean isPetPlayMode(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("petPlayMode"); + } + + public static void setPetPlayMode(ItemStack stack, boolean petPlay) { + stack.getOrCreateTag().putBoolean("petPlayMode", petPlay); + } + + // ===== ALERT SUPPRESSION ===== + + // Executes the action with collar removal alerts suppressed + public static void runWithSuppressedAlert(Runnable action) { + ItemCollar.runWithSuppressedAlert(action); + } + + // True if removal alerts are currently suppressed (ThreadLocal state) + public static boolean isRemovalAlertSuppressed() { + return ItemCollar.isRemovalAlertSuppressed(); + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java new file mode 100644 index 0000000..5804147 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java @@ -0,0 +1,35 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.base.ItemBind; +import com.tiedup.remake.items.base.PoseType; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; +import net.minecraft.world.item.ItemStack; + +/** + * Resolves the {@link PoseType} for any bondage item stack (V1 or V2). + * + *

V2 items read from the data-driven definition's {@code pose_type} field. + * V1 items fall back to {@code ItemBind.getPoseType()}.

+ */ +public final class PoseTypeHelper { + + private PoseTypeHelper() {} + + public static PoseType getPoseType(ItemStack stack) { + // V2: read from data-driven definition + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def != null && def.poseType() != null) { + try { + return PoseType.valueOf(def.poseType().toUpperCase()); + } catch (IllegalArgumentException e) { + return PoseType.STANDARD; + } + } + // V1 fallback + if (stack.getItem() instanceof ItemBind bind) { + return bind.getPoseType(); + } + return PoseType.STANDARD; + } +} 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..5117419 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -0,0 +1,123 @@ +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 com.tiedup.remake.v2.bondage.V2EquipResult; +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 (check both V1 state and V2 region to prevent dual-bind) + if (targetState.isTiedUp() || V2EquipmentHelper.isRegionOccupied(target, BodyRegionV2.ARMS)) { + if (stack.isEmpty()) return InteractionResult.PASS; + ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS); + if (!oldBind.isEmpty()) { + V2EquipResult result = V2EquipmentHelper.equipItem(target, stack.copy()); + if (result.isSuccess()) { + stack.shrink(1); + target.spawnAtLocation(oldBind); + TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString()); + return InteractionResult.SUCCESS; + } else { + // Equip failed — rollback: re-equip old bind + V2EquipResult rollback = V2EquipmentHelper.equipItem(target, oldBind); + if (!rollback.isSuccess()) { + // Rollback also failed — drop old bind as safety net + target.spawnAtLocation(oldBind); + TiedUpMod.LOGGER.warn("[TyingInteraction] Swap AND rollback failed, dropped old bind for {}", target.getName().getString()); + } else { + TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString()); + } + return InteractionResult.PASS; + } + } + 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 instanceof V2TyingPlayerTask) + || !currentTask.isSameTarget(target) + || currentTask.isOutdated() + || !ItemStack.matches(currentTask.getBind(), stack)) { + // Start new task (also handles case where existing task is V1 TyingPlayerTask) + playerState.setCurrentTyingTask(newTask); + newTask.start(); + } else { + newTask = (V2TyingPlayerTask) currentTask; + } + + newTask.update(); + + if (newTask.isStopped()) { + // Item already consumed by V2TyingPlayerTask.onComplete() — don't shrink again + 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/component/ChokingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java index 5860159..5cc4d74 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: choking effect for data-driven items. @@ -43,4 +50,9 @@ public class ChokingComponent implements IItemComponent { public boolean isNonLethalForMaster() { return nonLethalForMaster; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.choking").withStyle(ChatFormatting.DARK_PURPLE)); + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java index 6a465ae..d6fcdfb 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java @@ -2,9 +2,13 @@ package com.tiedup.remake.v2.bondage.component; import java.util.Collections; import java.util.EnumMap; +import java.util.List; import java.util.Map; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; public final class ComponentHolder { @@ -58,6 +62,12 @@ public final class ComponentHolder { return false; } + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + for (IItemComponent c : components.values()) { + c.appendTooltip(stack, level, tooltip, flag); + } + } + public boolean isEmpty() { return components.isEmpty(); } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java index 3964db4..198159e 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java @@ -15,7 +15,8 @@ public enum ComponentType { SHOCK("shock", ShockComponent::fromJson), GPS("gps", GpsComponent::fromJson), CHOKING("choking", ChokingComponent::fromJson), - ADJUSTABLE("adjustable", AdjustableComponent::fromJson); + ADJUSTABLE("adjustable", AdjustableComponent::fromJson), + OWNERSHIP("ownership", OwnershipComponent::fromJson); private final String jsonKey; private final Function factory; diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java index 0e401a5..07e664b 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java @@ -1,45 +1,100 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.util.GagMaterial; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: gagging behavior for data-driven items. * - * JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}} + *

Config-driven: {@code "gagging": {"material": "ball"}} delegates to + * {@link GagMaterial} for comprehension/range from ModConfig at runtime.

+ * + *

Override: {@code "gagging": {"comprehension": 0.15, "range": 8.0}} uses + * explicit values that take priority over the material lookup.

*/ public class GaggingComponent implements IItemComponent { - private final double comprehension; - private final double range; + private final @Nullable String material; + private final @Nullable GagMaterial cachedMaterial; + private final double comprehensionOverride; + private final double rangeOverride; - private GaggingComponent(double comprehension, double range) { - this.comprehension = comprehension; - this.range = range; + private GaggingComponent(@Nullable String material, @Nullable GagMaterial cachedMaterial, + double comprehensionOverride, double rangeOverride) { + this.material = material; + this.cachedMaterial = cachedMaterial; + this.comprehensionOverride = comprehensionOverride; + this.rangeOverride = rangeOverride; } public static IItemComponent fromJson(JsonObject config) { - double comprehension = 0.2; - double range = 10.0; + String material = null; + double comprehension = -1; + double range = -1; if (config != null) { + if (config.has("material")) { + material = config.get("material").getAsString(); + } if (config.has("comprehension")) { - comprehension = config.get("comprehension").getAsDouble(); + comprehension = Math.max(0.0, Math.min(1.0, config.get("comprehension").getAsDouble())); } if (config.has("range")) { - range = config.get("range").getAsDouble(); + range = Math.max(0.0, config.get("range").getAsDouble()); } } - comprehension = Math.max(0.0, Math.min(1.0, comprehension)); - range = Math.max(0.0, range); - return new GaggingComponent(comprehension, range); + // Resolve and cache GagMaterial at load time to avoid valueOf() on every chat message + GagMaterial resolved = null; + if (material != null) { + try { + resolved = GagMaterial.valueOf(material.toUpperCase()); + } catch (IllegalArgumentException e) { + TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material '{}' — using defaults", material); + } + } + return new GaggingComponent(material, resolved, comprehension, range); } /** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */ public double getComprehension() { - return comprehension; + if (comprehensionOverride >= 0) return comprehensionOverride; + GagMaterial gag = getMaterial(); + if (gag != null) return gag.getComprehension(); + return 0.2; } /** Maximum range in blocks where muffled speech can be heard. */ public double getRange() { - return range; + if (rangeOverride >= 0) return rangeOverride; + GagMaterial gag = getMaterial(); + if (gag != null) return gag.getTalkRange(); + return 10.0; + } + + /** The gag material enum, or null if not configured or invalid. Cached at load time. */ + public @Nullable GagMaterial getMaterial() { + return cachedMaterial; + } + + /** The raw material string from JSON, or null. */ + public @Nullable String getMaterialName() { + return material; + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (material != null) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gag_material", material) + .withStyle(ChatFormatting.RED)); + } else { + tooltip.add(Component.translatable("item.tiedup.tooltip.gagging").withStyle(ChatFormatting.RED)); + } } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java index 272792a..3eaf778 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: GPS tracking and safe zone for data-driven items. @@ -42,4 +49,14 @@ public class GpsComponent implements IItemComponent { public boolean isPublicTracking() { return publicTracking; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_tracking") + .withStyle(ChatFormatting.AQUA)); + if (safeZoneRadius > 0) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_zone_radius", safeZoneRadius) + .withStyle(ChatFormatting.DARK_AQUA)); + } + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java index 4af99c0..f051abc 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java @@ -1,7 +1,12 @@ package com.tiedup.remake.v2.bondage.component; +import java.util.List; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * A reusable behavior module for data-driven bondage items. @@ -16,4 +21,6 @@ public interface IItemComponent { default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { return false; } + + default void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {} } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java index 3a57c71..ef8ddcd 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: lockable behavior for data-driven items. @@ -41,4 +48,13 @@ public class LockableComponent implements IItemComponent { public int getLockResistance() { return lockResistance; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.lockable").withStyle(ChatFormatting.GOLD)); + if (flag.isAdvanced()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.lock_resistance", lockResistance) + .withStyle(ChatFormatting.DARK_GRAY)); + } + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java new file mode 100644 index 0000000..07be4a8 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java @@ -0,0 +1,122 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.items.base.ItemCollar; +import com.tiedup.remake.state.CollarRegistry; +import com.tiedup.remake.v2.bondage.CollarHelper; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * Component: collar ownership behavior for data-driven items. + * + *

Marks an item as a collar with ownership capabilities. + * Lifecycle hooks handle CollarRegistry registration/unregistration.

+ * + *

JSON config: {@code "ownership": {}}

+ */ +public class OwnershipComponent implements IItemComponent { + + private OwnershipComponent() {} + + public static IItemComponent fromJson(JsonObject config) { + return new OwnershipComponent(); + } + + @Override + public void onEquipped(ItemStack stack, LivingEntity entity) { + if (entity.level().isClientSide()) return; + if (!(entity.level() instanceof ServerLevel serverLevel)) return; + + List owners = CollarHelper.getOwners(stack); + if (owners.isEmpty()) { + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Collar equipped on {} with no owners in NBT — skipping registry", + entity.getName().getString() + ); + return; + } + + try { + CollarRegistry registry = CollarRegistry.get(serverLevel); + registry.registerCollar(entity.getUUID(), new HashSet<>(owners)); + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Registered collar for {} with {} owner(s)", + entity.getName().getString(), owners.size() + ); + } catch (Exception e) { + TiedUpMod.LOGGER.warn( + "[OwnershipComponent] Failed to register collar for {}: {}", + entity.getName().getString(), e.getMessage() + ); + } + } + + @Override + public void onUnequipped(ItemStack stack, LivingEntity entity) { + if (entity.level().isClientSide()) return; + if (!(entity.level() instanceof ServerLevel serverLevel)) return; + + // Alert kidnappers + unregister from CollarRegistry + // onCollarRemoved handles both the alert AND the unregister call internally, + // so we do NOT call registry.unregisterWearer() separately to avoid double unregister. + if (!CollarHelper.isRemovalAlertSuppressed()) { + ItemCollar.onCollarRemoved(entity, true); + } else { + // Suppressed alert path: still need to unregister, just skip the alert + try { + CollarRegistry registry = CollarRegistry.get(serverLevel); + registry.unregisterWearer(entity.getUUID()); + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Unregistered collar for {} (alert suppressed)", + entity.getName().getString() + ); + } catch (Exception e) { + TiedUpMod.LOGGER.warn( + "[OwnershipComponent] Failed to unregister collar for {}: {}", + entity.getName().getString(), e.getMessage() + ); + } + } + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + String nickname = CollarHelper.getNickname(stack); + if (nickname != null && !nickname.isEmpty()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.nickname", nickname) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + } + + List owners = CollarHelper.getOwners(stack); + if (!owners.isEmpty()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.owners", owners.size()) + .withStyle(ChatFormatting.GOLD)); + } + + if (CollarHelper.canShock(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_capable") + .withStyle(ChatFormatting.DARK_RED)); + } + + if (CollarHelper.hasGPS(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_capable") + .withStyle(ChatFormatting.AQUA)); + } + + if (CollarHelper.isChokeCollar(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.choke_capable") + .withStyle(ChatFormatting.DARK_PURPLE)); + } + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java index d59f838..4288596 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java @@ -1,33 +1,70 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.core.SettingsAccessor; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: struggle resistance for data-driven items. * - * JSON config: {@code "resistance": {"base": 150}} + *

Config-driven: {@code "resistance": {"id": "rope"}} delegates to + * {@link SettingsAccessor#getBindResistance(String)} at runtime.

+ * + *

Legacy/override: {@code "resistance": {"base": 150}} uses a hardcoded value.

*/ public class ResistanceComponent implements IItemComponent { - private final int baseResistance; + private final @Nullable String resistanceId; + private final int fallbackBase; - private ResistanceComponent(int baseResistance) { - this.baseResistance = baseResistance; + private ResistanceComponent(@Nullable String resistanceId, int fallbackBase) { + this.resistanceId = resistanceId; + this.fallbackBase = fallbackBase; } public static IItemComponent fromJson(JsonObject config) { + String id = null; int base = 100; - if (config != null && config.has("base")) { - base = config.get("base").getAsInt(); + if (config != null) { + if (config.has("id")) { + id = config.get("id").getAsString(); + } + if (config.has("base")) { + base = config.get("base").getAsInt(); + } } base = Math.max(0, base); - return new ResistanceComponent(base); + return new ResistanceComponent(id, base); } /** * Get the base resistance for this item. + * If a {@code resistanceId} is configured, delegates to server config at runtime. + * Otherwise returns the hardcoded fallback value. */ public int getBaseResistance() { - return baseResistance; + if (resistanceId != null) { + return SettingsAccessor.getBindResistance(resistanceId); + } + return fallbackBase; + } + + /** The config key used for runtime resistance lookup, or null if hardcoded. */ + public @Nullable String getResistanceId() { + return resistanceId; + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (flag.isAdvanced()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.resistance", getBaseResistance()) + .withStyle(ChatFormatting.DARK_GRAY)); + } } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java index 1c6d867..e54b8de 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: shock collar behavior for data-driven items. @@ -49,4 +56,16 @@ public class ShockComponent implements IItemComponent { public boolean hasAutoShock() { return autoInterval > 0; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (hasAutoShock()) { + float seconds = autoInterval / 20.0f; + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_auto", String.format("%.1f", seconds)) + .withStyle(ChatFormatting.DARK_RED)); + } else { + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_manual") + .withStyle(ChatFormatting.DARK_RED)); + } + } } 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 56d6738..3bf2e04 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,60 @@ 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: blocked until Branch C wires the full collar equip flow + // (add owner to NBT, register in CollarRegistry, play sound, sync). + // Without this, V2 collars equip without ownership — breaking GPS, shock, alerts. + if (regions.contains(BodyRegionV2.NECK)) { + TiedUpMod.LOGGER.debug("[DataDrivenBondageItem] NECK equip blocked — collar flow not wired yet"); + return InteractionResult.PASS; + } + + // All other regions (MOUTH, EYES, EARS, HANDS): instant equip via parent + return super.interactLivingEntity(stack, player, target, hand); + } + // ===== IHasResistance IMPLEMENTATION ===== @Override @@ -246,6 +309,12 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { } } + // Component tooltips + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder != null) { + holder.appendTooltip(stack, level, tooltip, flag); + } + // Lock status + escape difficulty (from AbstractV2BondageItem) super.appendHoverText(stack, level, tooltip, flag); @@ -285,6 +354,17 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { if (holder != null) { holder.onEquipped(stack, entity); } + + // NPC speed reduction (players use MovementStyleManager, not this legacy path) + if (!(entity instanceof Player)) { + Set regions = getOccupiedRegions(stack); + if (regions.contains(BodyRegionV2.ARMS) && BindModeHelper.hasLegsBound(stack)) { + com.tiedup.remake.items.base.PoseType pose = com.tiedup.remake.v2.bondage.PoseTypeHelper.getPoseType(stack); + boolean fullImmobilization = pose == com.tiedup.remake.items.base.PoseType.WRAP + || pose == com.tiedup.remake.items.base.PoseType.LATEX_SACK; + com.tiedup.remake.util.RestraintEffectUtils.applyBindSpeedReduction(entity, fullImmobilization); + } + } } @Override @@ -293,6 +373,14 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { if (holder != null) { holder.onUnequipped(stack, entity); } + + // NPC speed cleanup + if (!(entity instanceof Player)) { + Set regions = getOccupiedRegions(stack); + if (regions.contains(BodyRegionV2.ARMS)) { + com.tiedup.remake.util.RestraintEffectUtils.removeBindSpeedReduction(entity); + } + } } @Override diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java index 8d3e972..fa5d9e4 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java @@ -46,6 +46,9 @@ public record DataDrivenItemDefinition( /** Body regions this item blocks. Defaults to occupiedRegions if not specified. */ Set blockedRegions, + /** Optional pose type identifier (e.g., "STANDARD", "STRAITJACKET", "DOG"). */ + @Nullable String poseType, + /** Pose priority for conflict resolution. Higher wins. */ int posePriority, diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java index 221b2ae..9c4dd1c 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java @@ -188,6 +188,9 @@ public final class DataDrivenItemParser { blockedRegions = occupiedRegions; } + // Optional: pose_type (e.g., "STANDARD", "STRAITJACKET", "DOG") + String poseType = getStringOrNull(root, "pose_type"); + // Optional: pose_priority (default 0) int posePriority = getIntOrDefault(root, "pose_priority", 0); @@ -328,6 +331,7 @@ public final class DataDrivenItemParser { animationSource, occupiedRegions, blockedRegions, + poseType, posePriority, escapeDifficulty, lockable,