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)
500 lines
16 KiB
Java
500 lines
16 KiB
Java
package com.tiedup.remake.items;
|
|
|
|
import com.tiedup.remake.core.TiedUpMod;
|
|
import com.tiedup.remake.items.base.IKnife;
|
|
import com.tiedup.remake.items.base.ILockable;
|
|
import com.tiedup.remake.items.base.KnifeVariant;
|
|
import com.tiedup.remake.v2.bondage.BindModeHelper;
|
|
import com.tiedup.remake.network.sync.SyncManager;
|
|
import com.tiedup.remake.state.IBondageState;
|
|
import com.tiedup.remake.state.PlayerBindState;
|
|
import com.tiedup.remake.util.KidnappedHelper;
|
|
import com.tiedup.remake.v2.BodyRegionV2;
|
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
|
import java.util.List;
|
|
import net.minecraft.ChatFormatting;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.sounds.SoundEvents;
|
|
import net.minecraft.sounds.SoundSource;
|
|
import net.minecraft.world.InteractionHand;
|
|
import net.minecraft.world.InteractionResultHolder;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
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.item.UseAnim;
|
|
import net.minecraft.world.level.Level;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
/**
|
|
* Generic knife item created from KnifeVariant enum.
|
|
* Replaces individual knife classes (ItemStoneKnife, ItemIronKnife, ItemGoldenKnife).
|
|
*
|
|
* v2.5 Changes:
|
|
* - Added active cutting mechanic (hold right-click)
|
|
* - Per-tier cutting speed: Stone=5, Iron=8, Golden=12 resistance/second
|
|
* - Durability consumed per second = cutting speed (1 dura = 1 resistance)
|
|
* - Can cut binds directly or locked accessories
|
|
*/
|
|
public class GenericKnife extends Item implements IKnife {
|
|
|
|
private final KnifeVariant variant;
|
|
|
|
public GenericKnife(KnifeVariant variant) {
|
|
super(
|
|
new Item.Properties()
|
|
.stacksTo(1)
|
|
.durability(variant.getDurability())
|
|
);
|
|
this.variant = variant;
|
|
}
|
|
|
|
/**
|
|
* Get the variant this knife was created from.
|
|
*/
|
|
public KnifeVariant getVariant() {
|
|
return variant;
|
|
}
|
|
|
|
// ==================== TOOLTIP ====================
|
|
|
|
@Override
|
|
public void appendHoverText(
|
|
ItemStack stack,
|
|
@Nullable Level level,
|
|
List<Component> tooltip,
|
|
TooltipFlag flag
|
|
) {
|
|
super.appendHoverText(stack, level, tooltip, flag);
|
|
|
|
int remaining = stack.getMaxDamage() - stack.getDamageValue();
|
|
int speed = variant.getCuttingSpeed();
|
|
int cuttingSeconds = remaining / speed;
|
|
|
|
// Show cutting speed
|
|
tooltip.add(
|
|
Component.literal("Cutting speed: " + speed + " res/s").withStyle(
|
|
ChatFormatting.GRAY
|
|
)
|
|
);
|
|
|
|
// Show cutting time remaining
|
|
tooltip.add(
|
|
Component.literal(
|
|
"Cutting time: " +
|
|
cuttingSeconds +
|
|
"s (" +
|
|
remaining +
|
|
" total res)"
|
|
).withStyle(ChatFormatting.DARK_GRAY)
|
|
);
|
|
}
|
|
|
|
// ==================== USE MECHANICS ====================
|
|
|
|
@Override
|
|
public int getUseDuration(ItemStack stack) {
|
|
// Max use time: 5 minutes (very long, will stop naturally when bind breaks)
|
|
return 20 * 60 * 5;
|
|
}
|
|
|
|
@Override
|
|
public UseAnim getUseAnimation(ItemStack stack) {
|
|
return UseAnim.BOW; // Shows a "using" animation
|
|
}
|
|
|
|
/**
|
|
* Called when player right-clicks with knife.
|
|
* Starts cutting if:
|
|
* - Player is tied up (cuts bind)
|
|
* - Player has a knife cut target set (cuts accessory lock)
|
|
*/
|
|
@Override
|
|
public InteractionResultHolder<ItemStack> use(
|
|
Level level,
|
|
Player player,
|
|
InteractionHand hand
|
|
) {
|
|
ItemStack stack = player.getItemInHand(hand);
|
|
|
|
// Only check on server side for actual state
|
|
if (!level.isClientSide) {
|
|
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
if (state == null) {
|
|
return InteractionResultHolder.pass(stack);
|
|
}
|
|
|
|
// v2.5: Block knife usage if wearing mittens
|
|
if (state.hasMittens()) {
|
|
TiedUpMod.LOGGER.debug(
|
|
"[GenericKnife] {} cannot use knife - wearing mittens",
|
|
player.getName().getString()
|
|
);
|
|
return InteractionResultHolder.fail(stack);
|
|
}
|
|
|
|
// Priority 1: If tied up, cut the bind
|
|
if (state.isTiedUp()) {
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
|
|
// Priority 2: If accessory target selected (via StruggleChoiceScreen)
|
|
if (state.getKnifeCutTarget() != null) {
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
|
|
// Priority 3: If wearing a collar (not tied), auto-target the collar
|
|
if (state.hasCollar()) {
|
|
state.setKnifeCutTarget(BodyRegionV2.NECK);
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
|
|
// Priority 4: Check other accessories (gag, blindfold, etc.)
|
|
if (state.isGagged()) {
|
|
state.setKnifeCutTarget(BodyRegionV2.MOUTH);
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
if (state.isBlindfolded()) {
|
|
state.setKnifeCutTarget(BodyRegionV2.EYES);
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
if (state.hasEarplugs()) {
|
|
state.setKnifeCutTarget(BodyRegionV2.EARS);
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
// Note: Don't auto-target mittens since you need hands to use knife
|
|
|
|
// Nothing to cut
|
|
return InteractionResultHolder.pass(stack);
|
|
}
|
|
|
|
// Client side - check mittens and allow use if valid target or has accessories
|
|
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
if (
|
|
state != null &&
|
|
!state.hasMittens() &&
|
|
(state.isTiedUp() ||
|
|
state.getKnifeCutTarget() != null ||
|
|
state.hasCollar() ||
|
|
state.isGagged() ||
|
|
state.isBlindfolded() ||
|
|
state.hasEarplugs())
|
|
) {
|
|
player.startUsingItem(hand);
|
|
return InteractionResultHolder.consume(stack);
|
|
}
|
|
|
|
return InteractionResultHolder.pass(stack);
|
|
}
|
|
|
|
/**
|
|
* Called every tick while player holds right-click.
|
|
* Performs the actual cutting logic.
|
|
*/
|
|
@Override
|
|
public void onUseTick(
|
|
Level level,
|
|
LivingEntity entity,
|
|
ItemStack stack,
|
|
int remainingTicks
|
|
) {
|
|
if (level.isClientSide || !(entity instanceof ServerPlayer player)) {
|
|
return;
|
|
}
|
|
|
|
// Calculate how many ticks have been used
|
|
int usedTicks = getUseDuration(stack) - remainingTicks;
|
|
|
|
// Only process every 20 ticks (1 second)
|
|
if (usedTicks > 0 && usedTicks % 20 == 0) {
|
|
performCutTick(player, stack);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when player releases right-click or item breaks.
|
|
*/
|
|
@Override
|
|
public void releaseUsing(
|
|
ItemStack stack,
|
|
Level level,
|
|
LivingEntity entity,
|
|
int remainingTicks
|
|
) {
|
|
if (!level.isClientSide && entity instanceof ServerPlayer player) {
|
|
TiedUpMod.LOGGER.debug(
|
|
"[GenericKnife] {} stopped cutting",
|
|
player.getName().getString()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform one "tick" of cutting (called every second while held).
|
|
* Consumes durability and removes resistance based on variant's cutting speed.
|
|
*/
|
|
private void performCutTick(ServerPlayer player, ItemStack stack) {
|
|
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
if (state == null) {
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
int speed = variant.getCuttingSpeed();
|
|
|
|
// Determine what to cut
|
|
if (state.isTiedUp()) {
|
|
// Cut BIND
|
|
cutBind(player, state, stack, speed);
|
|
} else if (state.getKnifeCutTarget() != null) {
|
|
// Cut ACCESSORY
|
|
cutAccessory(player, state, stack, speed);
|
|
} else {
|
|
// Nothing to cut
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
// Play cutting sound
|
|
player
|
|
.level()
|
|
.playSound(
|
|
null,
|
|
player.blockPosition(),
|
|
SoundEvents.SHEEP_SHEAR,
|
|
SoundSource.PLAYERS,
|
|
0.5f,
|
|
1.2f
|
|
);
|
|
|
|
// Consume durability equal to cutting speed
|
|
stack.hurtAndBreak(speed, player, p ->
|
|
p.broadcastBreakEvent(p.getUsedItemHand())
|
|
);
|
|
|
|
// Notify nearby guards (Kidnappers, Maids, Traders) about cutting noise
|
|
com.tiedup.remake.minigame.GuardNotificationHelper.notifyNearbyGuards(
|
|
player
|
|
);
|
|
|
|
// Force inventory sync so durability bar updates in real-time
|
|
player.inventoryMenu.broadcastChanges();
|
|
|
|
// Sync state to clients
|
|
SyncManager.syncBindState(player);
|
|
}
|
|
|
|
/**
|
|
* Cut the bind directly.
|
|
*/
|
|
private void cutBind(
|
|
ServerPlayer player,
|
|
PlayerBindState state,
|
|
ItemStack knifeStack,
|
|
int speed
|
|
) {
|
|
// Get bind stack for ILockable check
|
|
ItemStack bindStack = V2EquipmentHelper.getInRegion(
|
|
player,
|
|
BodyRegionV2.ARMS
|
|
);
|
|
if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
// Reduce resistance by cutting speed
|
|
int currentRes = state.getCurrentBindResistance();
|
|
int newRes = Math.max(0, currentRes - speed);
|
|
state.setCurrentBindResistance(newRes);
|
|
|
|
TiedUpMod.LOGGER.debug(
|
|
"[GenericKnife] {} cutting bind: resistance {} -> {}",
|
|
player.getName().getString(),
|
|
currentRes,
|
|
newRes
|
|
);
|
|
|
|
// Check if escaped
|
|
if (newRes <= 0) {
|
|
state.getStruggleBinds().successActionExternal(state);
|
|
player.stopUsingItem();
|
|
TiedUpMod.LOGGER.info(
|
|
"[GenericKnife] {} escaped by cutting bind!",
|
|
player.getName().getString()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cut an accessory - either removes lock resistance (if locked) or removes the item directly (if unlocked).
|
|
*/
|
|
private void cutAccessory(
|
|
ServerPlayer player,
|
|
PlayerBindState state,
|
|
ItemStack knifeStack,
|
|
int speed
|
|
) {
|
|
BodyRegionV2 target = state.getKnifeCutTarget();
|
|
if (target == null) {
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
ItemStack accessory = V2EquipmentHelper.getInRegion(player, target);
|
|
if (accessory.isEmpty()) {
|
|
// Target doesn't exist
|
|
state.clearKnifeCutTarget();
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
// Check if the accessory is locked
|
|
boolean isLocked = false;
|
|
if (accessory.getItem() instanceof ILockable lockable) {
|
|
isLocked = lockable.isLocked(accessory);
|
|
}
|
|
|
|
if (!isLocked) {
|
|
// NOT locked - directly cut and remove the accessory
|
|
IBondageState kidnapped = KidnappedHelper.getKidnappedState(player);
|
|
if (kidnapped != null) {
|
|
ItemStack removed = removeAccessory(kidnapped, target);
|
|
if (!removed.isEmpty()) {
|
|
// Drop the removed accessory
|
|
kidnapped.kidnappedDropItem(removed);
|
|
TiedUpMod.LOGGER.info(
|
|
"[GenericKnife] {} cut off unlocked {}",
|
|
player.getName().getString(),
|
|
target
|
|
);
|
|
}
|
|
}
|
|
|
|
state.clearKnifeCutTarget();
|
|
player.stopUsingItem();
|
|
return;
|
|
}
|
|
|
|
// Accessory IS locked - reduce lock resistance via knife cut progress
|
|
ILockable lockable = (ILockable) accessory.getItem();
|
|
int cutProgress = com.tiedup.remake.util.ItemNBTHelper.getInt(accessory, "knifeCutProgress");
|
|
int newProgress = cutProgress + speed;
|
|
int lockResistance = com.tiedup.remake.core.SettingsAccessor.getPadlockResistance(null);
|
|
com.tiedup.remake.util.ItemNBTHelper.setInt(accessory, "knifeCutProgress", newProgress);
|
|
|
|
TiedUpMod.LOGGER.debug(
|
|
"[GenericKnife] {} cutting {} lock: progress {} / {}",
|
|
player.getName().getString(),
|
|
target,
|
|
newProgress,
|
|
lockResistance
|
|
);
|
|
|
|
// Check if lock is destroyed
|
|
if (newProgress >= lockResistance) {
|
|
// Destroy the lock (remove padlock, clear lock state)
|
|
lockable.setLockedByKeyUUID(accessory, null); // Unlocks and clears locked state
|
|
lockable.setLockable(accessory, false); // Remove padlock entirely
|
|
com.tiedup.remake.util.ItemNBTHelper.remove(accessory, "knifeCutProgress");
|
|
lockable.setJammed(accessory, false);
|
|
|
|
state.clearKnifeCutTarget();
|
|
player.stopUsingItem();
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[GenericKnife] {} cut through {} lock!",
|
|
player.getName().getString(),
|
|
target
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove an accessory from the player and return it.
|
|
*/
|
|
private ItemStack removeAccessory(
|
|
IBondageState kidnapped,
|
|
BodyRegionV2 target
|
|
) {
|
|
switch (target) {
|
|
case NECK -> {
|
|
ItemStack collar = kidnapped.getEquipment(BodyRegionV2.NECK);
|
|
if (collar != null && !collar.isEmpty()) {
|
|
kidnapped.unequip(BodyRegionV2.NECK);
|
|
return collar;
|
|
}
|
|
}
|
|
case MOUTH -> {
|
|
ItemStack gag = kidnapped.getEquipment(BodyRegionV2.MOUTH);
|
|
if (gag != null && !gag.isEmpty()) {
|
|
kidnapped.unequip(BodyRegionV2.MOUTH);
|
|
return gag;
|
|
}
|
|
}
|
|
case EYES -> {
|
|
ItemStack blindfold = kidnapped.getEquipment(BodyRegionV2.EYES);
|
|
if (blindfold != null && !blindfold.isEmpty()) {
|
|
kidnapped.unequip(BodyRegionV2.EYES);
|
|
return blindfold;
|
|
}
|
|
}
|
|
case EARS -> {
|
|
ItemStack earplugs = kidnapped.getEquipment(BodyRegionV2.EARS);
|
|
if (earplugs != null && !earplugs.isEmpty()) {
|
|
kidnapped.unequip(BodyRegionV2.EARS);
|
|
return earplugs;
|
|
}
|
|
}
|
|
case HANDS -> {
|
|
ItemStack mittens = kidnapped.getEquipment(BodyRegionV2.HANDS);
|
|
if (mittens != null && !mittens.isEmpty()) {
|
|
kidnapped.unequip(BodyRegionV2.HANDS);
|
|
return mittens;
|
|
}
|
|
}
|
|
default -> {
|
|
}
|
|
}
|
|
return ItemStack.EMPTY;
|
|
}
|
|
|
|
/**
|
|
* Find a knife in the player's inventory.
|
|
*
|
|
* @param player The player to search
|
|
* @return The knife ItemStack, or empty if not found
|
|
*/
|
|
public static ItemStack findKnifeInInventory(Player player) {
|
|
// Check main hand first
|
|
ItemStack mainHand = player.getMainHandItem();
|
|
if (mainHand.getItem() instanceof IKnife) {
|
|
return mainHand;
|
|
}
|
|
|
|
// Check off hand
|
|
ItemStack offHand = player.getOffhandItem();
|
|
if (offHand.getItem() instanceof IKnife) {
|
|
return offHand;
|
|
}
|
|
|
|
// Check inventory
|
|
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
|
|
ItemStack stack = player.getInventory().getItem(i);
|
|
if (stack.getItem() instanceof IKnife) {
|
|
return stack;
|
|
}
|
|
}
|
|
|
|
return ItemStack.EMPTY;
|
|
}
|
|
}
|