feat(D-01/E): resistance & lock system rework (E1-E7)

E1: Initialize currentResistance in NBT at equip time from
    ResistanceComponent — eliminates MAX-scan fallback bug

E2: BuiltInLockComponent for organic items (already committed)

E3: canStruggle refactor — new model:
    - ARMS: always struggle-able (no lock gating)
    - Non-ARMS: only if locked OR built-in lock
    - Removed dead isItemLocked() from StruggleState + overrides

E4: canUnequip already handled by BuiltInLockComponent.blocksUnequip()
    via ComponentHolder delegation

E5: Help/assist mechanic deferred (needs UI design)

E6: Removed lock resistance from ILockable (5 methods + NBT key deleted)
    - GenericKnife: new knifeCutProgress NBT for cutting locks
    - StruggleAccessory: accessoryStruggleResistance NBT replaces lock resistance
    - PacketV2StruggleStart: uses config-based padlock resistance
    - All lock/unlock packets cleaned of initializeLockResistance/clearLockResistance

E7: Fixed 3 pre-existing bugs:
    - B2: DataDrivenItemRegistry.clear() synchronized on RELOAD_LOCK
    - B3: V2TyingPlayerTask validates heldStack before equip (prevents duplication)
    - B5: EntityKidnapperMerchant.remove() cleans playerToMerchant map (memory leak)
This commit is contained in:
NotEvil
2026-04-15 03:23:49 +02:00
parent 199bf00aef
commit d6bb030ad7
17 changed files with 76 additions and 228 deletions

View File

@@ -1,12 +1,15 @@
package com.tiedup.remake.state.struggle;
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.ILockable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.ItemNBTHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -32,28 +35,36 @@ public class StruggleAccessory extends StruggleState {
this.accessoryRegion = accessoryRegion;
}
/** NBT key for accessory struggle progress (replaces removed ILockable lock resistance). */
private static final String NBT_ACCESSORY_STRUGGLE = "accessoryStruggleResistance";
/**
* Get the current resistance for this accessory.
* Accessories have 0 base resistance - only lock resistance exists.
* Resistance is stored in the item's NBT to persist between attempts.
* Accessories have 0 base resistance -- when locked, resistance comes from
* {@link SettingsAccessor#getPadlockResistance}. Stored in a dedicated NBT
* key on the accessory stack to persist between attempts.
*
* @param state The player's bind state
* @return Current resistance value (0 if not locked, 250 if locked and not yet struggled)
* @return Current resistance value (0 if not locked)
*/
@Override
protected int getResistanceState(PlayerBindState state) {
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) return 0;
if (stack.getItem() instanceof ILockable lockable) {
return lockable.getCurrentLockResistance(stack);
if (stack.getItem() instanceof ILockable lockable && lockable.isLocked(stack)) {
// If no struggle progress stored yet, initialize from config
if (!ItemNBTHelper.contains(stack, NBT_ACCESSORY_STRUGGLE)) {
return SettingsAccessor.getPadlockResistance(null);
}
return ItemNBTHelper.getInt(stack, NBT_ACCESSORY_STRUGGLE);
}
return 0;
}
/**
* Set the current resistance for this accessory struggle.
* Stored in the item's NBT to persist between attempts.
* Stored in a dedicated NBT key on the accessory stack.
*
* @param state The player's bind state
* @param resistance The new resistance value
@@ -63,9 +74,7 @@ public class StruggleAccessory extends StruggleState {
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) return;
if (stack.getItem() instanceof ILockable lockable) {
lockable.setCurrentLockResistance(stack, resistance);
}
ItemNBTHelper.setInt(stack, NBT_ACCESSORY_STRUGGLE, resistance);
}
/**
@@ -87,29 +96,15 @@ public class StruggleAccessory extends StruggleState {
return false;
}
// Only locked accessories can be struggled against
if (!(stack.getItem() instanceof ILockable lockable)) {
// Only require struggle if LOCKED or has BUILT_IN_LOCK.
// Unlocked accessories without built-in lock can be freely removed (no struggle needed).
boolean isLocked = stack.getItem() instanceof ILockable lockable && lockable.isLocked(stack);
boolean hasBuiltInLock = DataDrivenBondageItem.hasBuiltInLock(stack);
if (!isLocked && !hasBuiltInLock) {
return false;
}
return lockable.isLocked(stack);
}
/**
* Check if the accessory is locked.
*
* @param state The player's bind state
* @return true if the accessory is locked
*/
@Override
protected boolean isItemLocked(PlayerBindState state) {
ItemStack stack = getAccessoryStack(state);
if (
stack.isEmpty() || !(stack.getItem() instanceof ILockable lockable)
) {
return false;
}
return lockable.isLocked(stack);
return true;
}
/**

View File

@@ -1,6 +1,5 @@
package com.tiedup.remake.state.struggle;
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;
@@ -93,31 +92,6 @@ public class StruggleBinds extends StruggleState {
return true;
}
/**
* Check if the bind item is locked.
* Used by StruggleState to apply x10 resistance penalty.
*
* @param state The player's bind state
* @return true if the bind is locked
*/
@Override
protected boolean isItemLocked(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) return false;
ItemStack bindStack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.ARMS
);
if (bindStack.isEmpty()) return false;
// Works for both V1 (ItemBind) and V2 (DataDrivenBondageItem) via ILockable
if (bindStack.getItem() instanceof ILockable lockable) {
return lockable.isLocked(bindStack);
}
return false;
}
/**
* Called when the player successfully escapes from their binds.
* Drops all bondage items and completely unties the player.

View File

@@ -107,13 +107,12 @@ public class StruggleCollar extends StruggleState {
return false;
}
// 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 {
// Only require struggle if LOCKED or has BUILT_IN_LOCK.
// Unlocked collars without built-in lock can be freely removed (no struggle needed).
boolean isLocked = collar.getItem() instanceof ILockable lockable && lockable.isLocked(collar);
boolean hasBuiltInLock = DataDrivenBondageItem.hasBuiltInLock(collar);
if (!isLocked && !hasBuiltInLock) {
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked and has no built-in lock — no struggle needed");
return false;
}

View File

@@ -212,16 +212,6 @@ public abstract class StruggleState {
*/
protected abstract boolean canStruggle(PlayerBindState state);
/**
* Check if the item being struggled against is locked.
*
* @param state The player's bind state
* @return true if the item is locked
*/
protected boolean isItemLocked(PlayerBindState state) {
return false; // Default: not locked, subclasses override
}
/**
* Check if debug logging is enabled.
*