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.
518 lines
16 KiB
Java
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
|
|
}
|
|
}
|