Files
TiedUp-/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java
NotEvil b97bdf367e fix(D-01/A): V2 bind/collar resistance completely broken (CRITICAL)
PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and
getCurrentCollarResistance/setCurrentCollarResistance all checked
instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently
returned 0, making V2 items escapable in 1 struggle roll.

Fix: use instanceof IHasResistance which both V1 and V2 implement.

Also fix StruggleCollar.tighten() to read ResistanceComponent directly
for V2 collars instead of IHasResistance.getBaseResistance(entity)
which triggers the singleton MAX-scan across all equipped items.

Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug
(x10 locked penalty never applied) — tracked for separate fix.
2026-04-14 16:44:59 +02:00

518 lines
16 KiB
Java

package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for bondage equipment management.
* Handles putting on, taking off, and replacing all bondage items.
*
* Single Responsibility: Equipment lifecycle management
* Complexity: MEDIUM (V2 equipment coupling)
* Risk: MEDIUM (core equipment system)
*
* Epic 5F: Uses V2EquipmentHelper.
* Uses low-level setInRegion for equips because V1 items (IBondageItem) do not
* implement IV2BondageItem, so V2EquipmentHelper.equipItem() would reject them.
*/
public class PlayerEquipment {
private final IPlayerBindStateHost host;
public PlayerEquipment(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Put On Methods ==========
/** Equips a bind item and applies speed reduction. */
public void putBindOn(ItemStack bind) {
equipInRegion(BodyRegionV2.ARMS, bind);
}
/** Equips a gag (enables gag talk if implemented). Issue #14 fix: now calls onEquipped. */
public void putGagOn(ItemStack gag) {
equipInRegion(BodyRegionV2.MOUTH, gag);
}
/** Equips a blindfold (restricts vision). Issue #14 fix: now calls onEquipped. */
public void putBlindfoldOn(ItemStack blindfold) {
equipInRegion(BodyRegionV2.EYES, blindfold);
}
/** Equips a collar (starts GPS/Shock monitoring). Issue #14 fix: now calls onEquipped. */
public void putCollarOn(ItemStack collar) {
equipInRegion(BodyRegionV2.NECK, collar);
}
/** Issue #14 fix: now calls onEquipped. Syncs clothes config to all clients. */
public void putClothesOn(ItemStack clothes) {
equipInRegion(BodyRegionV2.TORSO, clothes);
// Sync clothes config (dynamic textures, etc.) to all tracking clients
host.syncClothesConfig();
}
/** Equips mittens (blocks hand interactions). Mittens system. Issue #14 fix: now calls onEquipped. */
public void putMittensOn(ItemStack mittens) {
equipInRegion(BodyRegionV2.HANDS, mittens);
checkMittensAfterApply();
}
/** Equips earplugs (muffles sounds). Issue #14 fix: now calls onEquipped. */
public void putEarplugsOn(ItemStack earplugs) {
equipInRegion(BodyRegionV2.EARS, earplugs);
checkEarplugsAfterApply();
}
// ========== Take Off Methods ==========
/** Removes binds and restores speed. */
public ItemStack takeBindOff() {
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.ARMS
);
}
public ItemStack takeGagOff() {
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.MOUTH
);
}
public ItemStack takeBlindfoldOff() {
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.EYES
);
}
public ItemStack takeCollarOff() {
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.NECK
);
}
/** Removes mittens. Mittens system */
public ItemStack takeMittensOff() {
ItemStack mittens = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.HANDS
);
if (isLocked(mittens, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.HANDS
);
}
public ItemStack takeEarplugsOff() {
ItemStack earplugs = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.EARS
);
if (isLocked(earplugs, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.EARS
);
}
public ItemStack takeClothesOff() {
ItemStack clothes = V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.TORSO
);
// Sync clothes removal to all tracking clients
host.syncClothesConfig();
return clothes;
}
/**
* Tries to remove the collar. Fails if locked unless forced.
*/
public ItemStack takeCollarOff(boolean force) {
Player player = host.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
if (collar.isEmpty()) return ItemStack.EMPTY;
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!force && collarItem.isLocked(collar)) return ItemStack.EMPTY;
collarItem.setLocked(collar, false);
}
return V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.NECK);
}
// ========== Replacement Methods ==========
/** Replaces the blindfold and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceBlindfold(ItemStack newBlindfold) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.EYES
);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.EYES, newBlindfold);
}
/** Replaces the gag and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceGag(ItemStack newGag) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.MOUTH
);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.MOUTH, newGag);
}
/** Replaces the collar and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceCollar(ItemStack newCollar) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.NECK
);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.NECK, newCollar);
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(ItemStack newBind) {
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(
ItemStack newBind,
boolean force
) {
// Safety: Don't remove current bind if newBind is empty
if (newBind.isEmpty()) {
return ItemStack.EMPTY;
}
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.ARMS
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
public ItemStack replaceGag(ItemStack newGag, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.MOUTH
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.MOUTH
);
putGagOn(newGag);
return old;
}
public ItemStack replaceBlindfold(ItemStack newBlindfold, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.EYES
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(
host.getPlayer(),
BodyRegionV2.EYES
);
putBlindfoldOn(newBlindfold);
return old;
}
public ItemStack replaceCollar(ItemStack newCollar, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.NECK
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeCollarOff(force);
putCollarOn(newCollar);
return old;
}
public ItemStack replaceEarplugs(ItemStack newEarplugs) {
return replaceEarplugs(newEarplugs, false);
}
public ItemStack replaceEarplugs(ItemStack newEarplugs, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.EARS
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeEarplugsOff();
putEarplugsOn(newEarplugs);
return old;
}
public ItemStack replaceMittens(ItemStack newMittens) {
return replaceMittens(newMittens, false);
}
public ItemStack replaceMittens(ItemStack newMittens, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(
host.getPlayer(),
BodyRegionV2.HANDS
);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeMittensOff();
if (!old.isEmpty() || !hasMittens()) {
putMittensOn(newMittens);
}
return old;
}
// ========== Resistance Methods ==========
/**
*/
public synchronized int getCurrentBindResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.ARMS
);
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;
}
/**
*/
public synchronized void setCurrentBindResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.ARMS
);
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);
}
}
/**
*/
public synchronized int getCurrentCollarResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
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;
}
/**
*/
public synchronized void setCurrentCollarResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
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 ==========
/** Helper to drop an item at the kidnapped player's feet. */
public void kidnappedDropItem(ItemStack stack) {
if (stack.isEmpty() || host.getPlayer() == null) return;
host.getPlayer().drop(stack, false);
}
/**
* Helper to take a bondage item if it's unlocked, and drop it.
*/
public void takeBondageItemIfUnlocked(
ItemStack item,
Supplier<ItemStack> takeOffMethod
) {
if (isLocked(item, false)) {
return; // Item is locked, cannot remove
}
ItemStack removed = takeOffMethod.get();
if (!removed.isEmpty()) {
kidnappedDropItem(removed);
}
}
/**
* Check if an item is locked.
* @param stack The item to check
* @param force If true, returns false (bypasses lock)
* @return true if locked and not forced
*/
public boolean isLocked(ItemStack stack, boolean force) {
if (force) return false;
if (stack.isEmpty()) return false;
if (stack.getItem() instanceof ILockable lockable) {
return lockable.isLocked(stack);
}
return false;
}
private boolean hasMittens() {
return V2EquipmentHelper.isRegionOccupied(
host.getPlayer(),
BodyRegionV2.HANDS
);
}
// ========== Low-level V2 equipment operations ==========
/**
* Low-level equip: writes to V2 capability, calls IBondageItem lifecycle hooks, syncs.
* Uses setInRegion directly because V1 items do not implement IV2BondageItem.
*/
private void equipInRegion(BodyRegionV2 region, ItemStack stack) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return;
if (stack.isEmpty()) return;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return;
// Check if already occupied
if (equip.isRegionOccupied(region)) return;
// Check canEquip via V1 IBondageItem interface
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
if (!bondageItem.canEquip(stack, player)) return;
}
equip.setInRegion(region, stack.copy());
// Fire lifecycle hook
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
bondageItem.onEquipped(stack, player);
}
V2EquipmentHelper.sync(player);
}
/**
* Low-level replace: unequips old item with lifecycle hooks, equips new item.
* Unequips old item (with onUnequipped hook), sets new item, fires onEquipped.
*/
private ItemStack replaceInRegion(BodyRegionV2 region, ItemStack newStack) {
Player player = host.getPlayer();
if (
player == null || player.level().isClientSide
) return ItemStack.EMPTY;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return ItemStack.EMPTY;
ItemStack oldStack = equip.getInRegion(region);
// Call onUnequipped for the old item
if (
!oldStack.isEmpty() &&
oldStack.getItem() instanceof IV2BondageItem oldItem
) {
oldItem.onUnequipped(oldStack, player);
}
// Set the new item
equip.setInRegion(region, newStack.copy());
// Call onEquipped for the new item
if (
!newStack.isEmpty() &&
newStack.getItem() instanceof IV2BondageItem newItem
) {
newItem.onEquipped(newStack, player);
}
V2EquipmentHelper.sync(player);
return oldStack;
}
// ========== Callbacks ==========
public void checkGagAfterApply() {
// Gag talk handled via ChatEventHandler + GagTalkManager (event-based)
// No initialization needed here - effects apply automatically on chat
}
public void checkBlindfoldAfterApply() {
// Effects applied client-side via rendering
}
public void checkEarplugsAfterApply() {
// Effects applied client-side via sound system
}
public void checkCollarAfterApply() {
// Collar timers/GPS handled elsewhere
}
public void checkMittensAfterApply() {
// Mittens effects handled elsewhere
}
}