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)
390 lines
12 KiB
Java
390 lines
12 KiB
Java
package com.tiedup.remake.items;
|
|
|
|
import com.tiedup.remake.core.ModConfig;
|
|
import com.tiedup.remake.core.SystemMessageManager;
|
|
import com.tiedup.remake.core.TiedUpMod;
|
|
import com.tiedup.remake.items.base.ILockable;
|
|
import com.tiedup.remake.state.PlayerBindState;
|
|
import com.tiedup.remake.v2.BodyRegionV2;
|
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
import java.util.UUID;
|
|
import net.minecraft.ChatFormatting;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.world.InteractionHand;
|
|
import net.minecraft.world.InteractionResultHolder;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.item.Item;
|
|
import net.minecraft.world.item.ItemStack;
|
|
import net.minecraft.world.item.TooltipFlag;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraftforge.api.distmarker.Dist;
|
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
/**
|
|
* Lockpick item for picking locks on bondage restraints.
|
|
*
|
|
*
|
|
* Behavior:
|
|
* - 25% chance of success per attempt
|
|
* - SUCCESS: Instant unlock, padlock PRESERVED (lockable=true)
|
|
* - FAIL:
|
|
* - 2.5% chance to JAM the lock (blocks future lockpick attempts)
|
|
* - 15% chance to break the lockpick
|
|
* - If shock collar equipped: SHOCK + notify owners
|
|
* - Cannot be used while wearing mittens
|
|
* - Durability: 10 uses
|
|
*/
|
|
public class ItemLockpick extends Item {
|
|
|
|
private static final Random random = new Random();
|
|
|
|
public ItemLockpick() {
|
|
super(new Item.Properties().durability(5)); // 5 tentatives max
|
|
}
|
|
|
|
@Override
|
|
public void appendHoverText(
|
|
ItemStack stack,
|
|
@Nullable Level level,
|
|
List<Component> tooltip,
|
|
TooltipFlag flag
|
|
) {
|
|
super.appendHoverText(stack, level, tooltip, flag);
|
|
|
|
tooltip.add(
|
|
Component.translatable("item.tiedup.lockpick.tooltip").withStyle(
|
|
ChatFormatting.GRAY
|
|
)
|
|
);
|
|
|
|
int remaining = stack.getMaxDamage() - stack.getDamageValue();
|
|
tooltip.add(
|
|
Component.literal(
|
|
"Uses: " + remaining + "/" + stack.getMaxDamage()
|
|
).withStyle(ChatFormatting.DARK_GRAY)
|
|
);
|
|
|
|
// LOW FIX: Removed server config access from client tooltip (desync issue)
|
|
// Success/break chances depend on server config, not client config
|
|
// Displaying client config values here would be misleading in multiplayer
|
|
tooltip.add(
|
|
Component.literal("Success/break chances: Check server config")
|
|
.withStyle(ChatFormatting.GRAY)
|
|
.withStyle(ChatFormatting.ITALIC)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* v2.5: Right-click with lockpick opens the struggle choice screen.
|
|
* This allows the player to choose which locked item to pick.
|
|
*/
|
|
@Override
|
|
public InteractionResultHolder<ItemStack> use(
|
|
Level level,
|
|
Player player,
|
|
InteractionHand hand
|
|
) {
|
|
ItemStack stack = player.getItemInHand(hand);
|
|
|
|
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
if (state == null) {
|
|
return InteractionResultHolder.pass(stack);
|
|
}
|
|
|
|
// Block mittens
|
|
if (state.hasMittens()) {
|
|
if (!level.isClientSide) {
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS
|
|
);
|
|
}
|
|
return InteractionResultHolder.fail(stack);
|
|
}
|
|
|
|
// Client side: open the unified bondage screen
|
|
if (level.isClientSide) {
|
|
openUnifiedBondageScreen();
|
|
return InteractionResultHolder.success(stack);
|
|
}
|
|
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
|
|
/**
|
|
* Client-only method to open the unified bondage screen.
|
|
* Separated to avoid classloading issues on server.
|
|
* Uses fully qualified names to prevent class loading on server.
|
|
*/
|
|
@OnlyIn(Dist.CLIENT)
|
|
private void openUnifiedBondageScreen() {
|
|
net.minecraft.client.Minecraft.getInstance().setScreen(
|
|
new com.tiedup.remake.client.gui.screens.UnifiedBondageScreen()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if this lockpick can be used (has durability remaining).
|
|
*/
|
|
public static boolean canUse(ItemStack stack) {
|
|
if (stack.isEmpty() || !(stack.getItem() instanceof ItemLockpick)) {
|
|
return false;
|
|
}
|
|
return stack.getDamageValue() < stack.getMaxDamage();
|
|
}
|
|
|
|
/**
|
|
* Result of a lockpick attempt.
|
|
*/
|
|
public enum PickResult {
|
|
/** Successfully picked the lock - item unlocked, padlock preserved */
|
|
SUCCESS,
|
|
/** Failed but lock still pickable */
|
|
FAIL,
|
|
/** Failed and jammed the lock - lockpick no longer usable on this item */
|
|
JAMMED,
|
|
/** Lockpick broke during attempt */
|
|
BROKE,
|
|
/** Cannot attempt - mittens equipped */
|
|
BLOCKED_MITTENS,
|
|
/** Cannot attempt - lock is jammed */
|
|
BLOCKED_JAMMED,
|
|
/** Cannot attempt - item not locked */
|
|
NOT_LOCKED,
|
|
}
|
|
|
|
/**
|
|
* Attempt to pick a lock on a target item.
|
|
*
|
|
* @param player The player attempting to pick
|
|
* @param state The player's bind state
|
|
* @param lockpickStack The lockpick being used
|
|
* @param targetStack The item to pick
|
|
* @param targetRegion The V2 body region of the target item
|
|
* @return The result of the pick attempt
|
|
*/
|
|
public static PickResult attemptPick(
|
|
Player player,
|
|
PlayerBindState state,
|
|
ItemStack lockpickStack,
|
|
ItemStack targetStack,
|
|
BodyRegionV2 targetRegion
|
|
) {
|
|
// Check if lockpick is usable
|
|
if (!canUse(lockpickStack)) {
|
|
return PickResult.BROKE;
|
|
}
|
|
|
|
// Check if wearing mittens
|
|
if (state.hasMittens()) {
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS
|
|
);
|
|
return PickResult.BLOCKED_MITTENS;
|
|
}
|
|
|
|
// Check if target is lockable and locked
|
|
if (!(targetStack.getItem() instanceof ILockable lockable)) {
|
|
return PickResult.NOT_LOCKED;
|
|
}
|
|
|
|
if (!lockable.isLocked(targetStack)) {
|
|
return PickResult.NOT_LOCKED;
|
|
}
|
|
|
|
// Check if lock is jammed
|
|
if (lockable.isJammed(targetStack)) {
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.ERROR,
|
|
"This lock is jammed! Use struggle instead."
|
|
);
|
|
return PickResult.BLOCKED_JAMMED;
|
|
}
|
|
|
|
// Roll for success
|
|
boolean success =
|
|
random.nextInt(100) < ModConfig.SERVER.lockpickSuccessChance.get();
|
|
|
|
if (success) {
|
|
// SUCCESS: Unlock the item, PRESERVE the padlock
|
|
lockable.setLockedByKeyUUID(targetStack, null); // Unlock
|
|
// lockable stays true - padlock preserved!
|
|
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
"Lock picked!",
|
|
ChatFormatting.GREEN
|
|
);
|
|
|
|
// Damage lockpick
|
|
damageLockpick(lockpickStack);
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[LOCKPICK] {} successfully picked lock on {} ({})",
|
|
player.getName().getString(),
|
|
targetStack.getDisplayName().getString(),
|
|
targetRegion
|
|
);
|
|
|
|
return PickResult.SUCCESS;
|
|
} else {
|
|
// FAIL: Various bad things can happen
|
|
|
|
// 1. Check for shock collar and trigger shock
|
|
triggerShockIfCollar(player, state);
|
|
|
|
// 2. Check for jam
|
|
boolean jammed =
|
|
random.nextDouble() * 100 <
|
|
ModConfig.SERVER.lockpickJamChance.get();
|
|
if (jammed) {
|
|
lockable.setJammed(targetStack, true);
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.ERROR,
|
|
"The lock jammed! Only struggle can open it now."
|
|
);
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[LOCKPICK] {} jammed the lock on {} ({})",
|
|
player.getName().getString(),
|
|
targetStack.getDisplayName().getString(),
|
|
targetRegion
|
|
);
|
|
|
|
// Damage lockpick
|
|
boolean broke = damageLockpick(lockpickStack);
|
|
return broke ? PickResult.BROKE : PickResult.JAMMED;
|
|
}
|
|
|
|
// 3. Check for break
|
|
boolean broke =
|
|
random.nextInt(100) <
|
|
ModConfig.SERVER.lockpickBreakChance.get();
|
|
if (broke) {
|
|
lockpickStack.shrink(1);
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.ERROR,
|
|
"Lockpick broke!"
|
|
);
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[LOCKPICK] {}'s lockpick broke while picking {} ({})",
|
|
player.getName().getString(),
|
|
targetStack.getDisplayName().getString(),
|
|
targetRegion
|
|
);
|
|
|
|
return PickResult.BROKE;
|
|
}
|
|
|
|
// 4. Normal fail - just damage lockpick
|
|
damageLockpick(lockpickStack);
|
|
SystemMessageManager.sendToPlayer(
|
|
player,
|
|
SystemMessageManager.MessageCategory.WARNING,
|
|
"Lockpick slipped..."
|
|
);
|
|
|
|
TiedUpMod.LOGGER.debug(
|
|
"[LOCKPICK] {} failed to pick lock on {} ({})",
|
|
player.getName().getString(),
|
|
targetStack.getDisplayName().getString(),
|
|
targetRegion
|
|
);
|
|
|
|
return PickResult.FAIL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Damage the lockpick by 1 use.
|
|
* @return true if the lockpick broke (ran out of durability)
|
|
*/
|
|
private static boolean damageLockpick(ItemStack stack) {
|
|
stack.setDamageValue(stack.getDamageValue() + 1);
|
|
if (stack.getDamageValue() >= stack.getMaxDamage()) {
|
|
stack.shrink(1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Trigger shock collar if player has one equipped.
|
|
* Also notifies the collar owners.
|
|
*/
|
|
private static void triggerShockIfCollar(
|
|
Player player,
|
|
PlayerBindState state
|
|
) {
|
|
ItemStack collar = V2EquipmentHelper.getInRegion(
|
|
player,
|
|
BodyRegionV2.NECK
|
|
);
|
|
if (collar.isEmpty()) return;
|
|
|
|
if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) {
|
|
// Shock the player
|
|
state.shockKidnapped(" (Failed lockpick attempt)", 2.0f);
|
|
|
|
// Notify owners
|
|
notifyOwnersLockpickAttempt(player, collar);
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[LOCKPICK] {} was shocked for failed lockpick attempt",
|
|
player.getName().getString()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify shock collar owners about the lockpick attempt.
|
|
*/
|
|
private static void notifyOwnersLockpickAttempt(
|
|
Player player,
|
|
ItemStack collar
|
|
) {
|
|
if (player.getServer() == null) return;
|
|
|
|
Component warning = Component.literal("ALERT: ")
|
|
.withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
|
|
.append(
|
|
Component.literal(
|
|
player.getName().getString() + " tried to pick a lock!"
|
|
).withStyle(ChatFormatting.GOLD)
|
|
);
|
|
|
|
List<UUID> owners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
|
|
for (UUID ownerId : owners) {
|
|
ServerPlayer owner = player
|
|
.getServer()
|
|
.getPlayerList()
|
|
.getPlayer(ownerId);
|
|
if (owner != null) {
|
|
owner.sendSystemMessage(warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a lockpick in the player's inventory.
|
|
* @return The first usable lockpick found, or EMPTY if none
|
|
*/
|
|
public static ItemStack findLockpickInInventory(Player player) {
|
|
for (ItemStack stack : player.getInventory().items) {
|
|
if (canUse(stack)) {
|
|
return stack;
|
|
}
|
|
}
|
|
return ItemStack.EMPTY;
|
|
}
|
|
}
|